Skip to content

Commit 359274c

Browse files
authored
fix(download): add 7z extraction support and expand Python manifest (#161)
1 parent c0a0ca3 commit 359274c

7 files changed

Lines changed: 953 additions & 59 deletions

File tree

.github/workflows/integration-test-migrate-python-windows-system.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ jobs:
2121
go-version: '1.23'
2222
cache: true
2323

24+
# Install a Python version that's available in our manifest for Windows
25+
# python-build-standalone only has Windows builds starting from 3.10.14
26+
- name: Set up Python
27+
uses: actions/setup-python@v5
28+
with:
29+
python-version: '3.12'
30+
2431
- name: Build dtvem
2532
shell: bash
2633
run: |

go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/dtvem/dtvem
33
go 1.23
44

55
require (
6+
github.com/bodgit/sevenzip v1.6.1
67
github.com/briandowns/spinner v1.23.2
78
github.com/charmbracelet/lipgloss v1.1.0
89
github.com/fatih/color v1.18.0
@@ -13,19 +14,29 @@ require (
1314
)
1415

1516
require (
17+
github.com/andybalholm/brotli v1.1.1 // indirect
1618
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
19+
github.com/bodgit/plumbing v1.3.0 // indirect
20+
github.com/bodgit/windows v1.0.1 // indirect
1721
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
1822
github.com/charmbracelet/x/ansi v0.8.0 // indirect
1923
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
2024
github.com/charmbracelet/x/term v0.2.1 // indirect
25+
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
2126
github.com/inconshreveable/mousetrap v1.1.0 // indirect
27+
github.com/klauspost/compress v1.17.11 // indirect
2228
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
2329
github.com/mattn/go-colorable v0.1.13 // indirect
2430
github.com/mattn/go-isatty v0.0.20 // indirect
2531
github.com/mattn/go-runewidth v0.0.16 // indirect
2632
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
33+
github.com/pierrec/lz4/v4 v4.1.22 // indirect
2734
github.com/rivo/uniseg v0.4.7 // indirect
35+
github.com/spf13/afero v1.11.0 // indirect
2836
github.com/spf13/pflag v1.0.10 // indirect
37+
github.com/ulikunitz/xz v0.5.12 // indirect
2938
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
39+
go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
3040
golang.org/x/term v0.28.0 // indirect
41+
golang.org/x/text v0.21.0 // indirect
3142
)

go.sum

Lines changed: 252 additions & 2 deletions
Large diffs are not rendered by default.

scripts/generate-manifests/python.go

Lines changed: 158 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ package main
22

33
import (
44
"fmt"
5+
"io"
6+
"net/http"
57
"regexp"
68
"strings"
79
)
810

911
const (
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
2629
var 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

92229
func fetchPythonReleases() ([]githubRelease, error) {

0 commit comments

Comments
 (0)