@@ -2,12 +2,15 @@ package main
22
33import (
44 "fmt"
5+ "io"
6+ "net/http"
57 "regexp"
68 "strings"
79)
810
911const (
1012 pythonReleasesURL = "https://api.github.com/repos/astral-sh/python-build-standalone/releases"
13+ pythonOrgFTPURL = "https://www.python.org/ftp/python/"
1114 pythonSchemaURL = "https://raw.githubusercontent.com/dtvem/dtvem/main/schemas/manifest.schema.json"
1215)
1316
@@ -25,56 +28,71 @@ var pythonPlatformMap = map[string]string{
2528// Regex to parse asset names like: cpython-3.13.1+20251209-x86_64-unknown-linux-gnu-install_only.tar.gz
2629var pythonAssetRegex = regexp .MustCompile (`^cpython-(\d+\.\d+\.\d+)\+\d+-(.+)-install_only\.tar\.gz$` )
2730
28- func generatePythonManifest ( outputDir string ) error {
29- fmt . Println ( "Generating Python manifest..." )
31+ // Regex to parse python.org version directories
32+ var pythonOrgVersionRegex = regexp . MustCompile ( `href="(\d+\.\d+\.\d+)/"` )
3033
31- // Fetch releases from GitHub API
32- releases , err := fetchPythonReleases ()
33- if err != nil {
34- return fmt .Errorf ("failed to fetch releases: %w" , err )
35- }
34+ // Regex to parse python.org embeddable package names
35+ var pythonOrgEmbedRegex = regexp .MustCompile (`href="(python-(\d+\.\d+\.\d+)-embed-(amd64|arm64)\.zip)"` )
3636
37- fmt .Printf ("Found %d releases\n " , len (releases ))
37+ func generatePythonManifest (outputDir string ) error {
38+ fmt .Println ("Generating Python manifest..." )
3839
3940 manifest := & Manifest {
4041 Schema : pythonSchemaURL ,
4142 Version : 1 ,
4243 Versions : make (map [string ]map [string ]* Download ),
4344 }
4445
45- // Process each release
46- for _ , release := range releases {
47- fmt .Printf ("Processing release %s (%d assets)...\n " , release .TagName , len (release .Assets ))
46+ // First, fetch from python-build-standalone (primary source for Linux/macOS, newer Windows)
47+ if err := fetchPythonBuildStandalone (manifest ); err != nil {
48+ fmt .Printf ("Warning: failed to fetch python-build-standalone: %v\n " , err )
49+ }
4850
51+ // Then, fetch from python.org for Windows (fills gaps for older versions)
52+ if err := fetchPythonOrg (manifest ); err != nil {
53+ fmt .Printf ("Warning: failed to fetch python.org: %v\n " , err )
54+ }
55+
56+ fmt .Printf ("Generated manifest with %d versions\n " , len (manifest .Versions ))
57+
58+ return writeManifest (manifest , outputDir , "python.json" )
59+ }
60+
61+ // fetchPythonBuildStandalone fetches releases from astral-sh/python-build-standalone
62+ func fetchPythonBuildStandalone (manifest * Manifest ) error {
63+ fmt .Println ("Fetching from python-build-standalone..." )
64+
65+ releases , err := fetchPythonReleases ()
66+ if err != nil {
67+ return fmt .Errorf ("failed to fetch releases: %w" , err )
68+ }
69+
70+ fmt .Printf ("Found %d releases from python-build-standalone\n " , len (releases ))
71+
72+ for _ , release := range releases {
4973 for _ , asset := range release .Assets {
50- // Parse asset name to extract version and platform
5174 matches := pythonAssetRegex .FindStringSubmatch (asset .Name )
5275 if matches == nil {
5376 continue
5477 }
5578
56- version := matches [1 ] // e.g., "3.13.1"
57- pbsPlatform := matches [2 ] // e.g., "x86_64-unknown-linux-gnu"
79+ version := matches [1 ]
80+ pbsPlatform := matches [2 ]
5881
59- // Map to our platform key
6082 platform , ok := pythonPlatformMap [pbsPlatform ]
6183 if ! ok {
6284 continue
6385 }
6486
65- // Extract SHA256 from digest if available (format: "sha256:abc123...")
6687 sha256 := ""
6788 if strings .HasPrefix (asset .Digest , "sha256:" ) {
6889 sha256 = strings .TrimPrefix (asset .Digest , "sha256:" )
6990 }
7091
71- // Initialize version map if needed
7292 if manifest .Versions [version ] == nil {
7393 manifest .Versions [version ] = make (map [string ]* Download )
7494 }
7595
76- // Only add if we don't already have this version/platform
77- // (prefer newer releases which come first from the API)
7896 if manifest.Versions [version ][platform ] == nil {
7997 manifest.Versions [version ][platform ] = & Download {
8098 URL : asset .BrowserDownloadURL ,
@@ -84,9 +102,128 @@ func generatePythonManifest(outputDir string) error {
84102 }
85103 }
86104
87- fmt .Printf ("Generated manifest with %d versions\n " , len (manifest .Versions ))
105+ return nil
106+ }
88107
89- return writeManifest (manifest , outputDir , "python.json" )
108+ // fetchPythonOrg fetches Windows embeddable packages from python.org
109+ func fetchPythonOrg (manifest * Manifest ) error {
110+ fmt .Println ("Fetching Windows packages from python.org..." )
111+
112+ // Get list of versions from python.org FTP
113+ versions , err := listPythonOrgVersions ()
114+ if err != nil {
115+ return fmt .Errorf ("failed to list versions: %w" , err )
116+ }
117+
118+ fmt .Printf ("Found %d versions on python.org\n " , len (versions ))
119+
120+ addedCount := 0
121+ for _ , version := range versions {
122+ // Skip if we already have Windows builds for this version
123+ if manifest .Versions [version ] != nil && manifest .Versions [version ]["windows-amd64" ] != nil {
124+ continue
125+ }
126+
127+ // Check for embeddable packages
128+ packages , err := listPythonOrgPackages (version )
129+ if err != nil {
130+ continue // Skip versions without packages
131+ }
132+
133+ for _ , pkg := range packages {
134+ if manifest .Versions [version ] == nil {
135+ manifest .Versions [version ] = make (map [string ]* Download )
136+ }
137+
138+ // Only add if we don't already have this platform
139+ if manifest.Versions [version ][pkg.Platform ] == nil {
140+ manifest.Versions [version ][pkg.Platform ] = & Download {
141+ URL : pkg .URL ,
142+ SHA256 : "" , // python.org doesn't provide easy SHA256 access
143+ }
144+ addedCount ++
145+ }
146+ }
147+ }
148+
149+ fmt .Printf ("Added %d Windows packages from python.org\n " , addedCount )
150+ return nil
151+ }
152+
153+ // listPythonOrgVersions lists available Python versions from python.org FTP
154+ func listPythonOrgVersions () ([]string , error ) {
155+ resp , err := http .Get (pythonOrgFTPURL )
156+ if err != nil {
157+ return nil , err
158+ }
159+ defer resp .Body .Close ()
160+
161+ body , err := io .ReadAll (resp .Body )
162+ if err != nil {
163+ return nil , err
164+ }
165+
166+ matches := pythonOrgVersionRegex .FindAllStringSubmatch (string (body ), - 1 )
167+ versions := make ([]string , 0 , len (matches ))
168+ for _ , m := range matches {
169+ version := m [1 ]
170+ // Only include Python 3.x versions (skip 2.x)
171+ if strings .HasPrefix (version , "3." ) {
172+ versions = append (versions , version )
173+ }
174+ }
175+
176+ return versions , nil
177+ }
178+
179+ // pythonOrgPackage represents a package from python.org
180+ type pythonOrgPackage struct {
181+ URL string
182+ Platform string
183+ }
184+
185+ // listPythonOrgPackages lists embeddable packages for a specific version
186+ func listPythonOrgPackages (version string ) ([]pythonOrgPackage , error ) {
187+ url := pythonOrgFTPURL + version + "/"
188+ resp , err := http .Get (url )
189+ if err != nil {
190+ return nil , err
191+ }
192+ defer resp .Body .Close ()
193+
194+ if resp .StatusCode != 200 {
195+ return nil , fmt .Errorf ("HTTP %d" , resp .StatusCode )
196+ }
197+
198+ body , err := io .ReadAll (resp .Body )
199+ if err != nil {
200+ return nil , err
201+ }
202+
203+ matches := pythonOrgEmbedRegex .FindAllStringSubmatch (string (body ), - 1 )
204+ packages := make ([]pythonOrgPackage , 0 , len (matches ))
205+
206+ for _ , m := range matches {
207+ filename := m [1 ]
208+ arch := m [3 ]
209+
210+ platform := ""
211+ switch arch {
212+ case "amd64" :
213+ platform = "windows-amd64"
214+ case "arm64" :
215+ platform = "windows-arm64"
216+ default :
217+ continue
218+ }
219+
220+ packages = append (packages , pythonOrgPackage {
221+ URL : url + filename ,
222+ Platform : platform ,
223+ })
224+ }
225+
226+ return packages , nil
90227}
91228
92229func fetchPythonReleases () ([]githubRelease , error ) {
0 commit comments