Files
Vintner/internal/download/fetch.go
T
Cheviiot 6464da7847 Initial implementation of msvc-go-wine
A single-binary Go tool for cross compiling with the real MSVC toolchain
on Linux via Wine. Behaves as cl/link/lib/rc/midl/mt/dumpbin/msbuild/
nmake/ml/ml64/armasm/armasm64/cmd/findstr depending on the name it's
invoked as, plus download/install/env/version management subcommands.

- download: fetches the MSVC/WinSDK installer manifest, resolves package
  selection and dependencies, downloads and verifies payloads, unpacks
  VSIX/MSI packages, and applies a handful of compatibility patches so
  VsDevCmd.bat and MSBuild's SDK detection work without a Windows
  Registry (which doesn't exist under Wine).
- install: locates the installed toolchain/SDK versions, normalizes
  header/library name casing, lays out per-architecture tool symlinks
  with an env.json config each, and compiles a small native launcher
  (toolrelay.exe) that lets mt.exe's CMake-compatibility exit code
  survive Wine's own exit-code truncation.
- The wrapper runtime rewrites absolute unix paths in tool arguments into
  Wine's z:\... form, runs the real .exe under wine, and rewrites the
  tool's output back to plain unix paths.

Verified end-to-end against a real MSVC/WinSDK download: cl, link, mt and
the resulting hello.exe all work under Wine, including through the
toolrelay.exe relay path and with paths containing non-ASCII characters.

Offline unit tests cover the wrapper's path-rewrite/output-filter logic,
install-time header lowercasing, and download package-selection/
dependency-resolution.
2026-07-25 00:57:41 +10:00

195 lines
4.3 KiB
Go

package download
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
)
const maxConcurrentDownloads = 5
const maxDownloadAttempts = 5
// FetchPayloads fetches every payload of every selected package into
// cacheDir/<packageKey>/<payloadName>, verifying sha256 and skipping files
// already present and correct. allowHashMismatch (used for --only-download)
// warns instead of failing on a hash mismatch.
func FetchPayloads(selected []*Package, cacheDir string, allowHashMismatch bool) error {
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return err
}
type task struct {
payload Payload
dest string
fileID string
}
var tasks []task
for _, p := range selected {
if len(p.Payloads) == 0 {
continue
}
dir := filepath.Join(cacheDir, p.Key())
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
for _, pl := range p.Payloads {
name := pl.Name()
tasks = append(tasks, task{
payload: pl,
dest: filepath.Join(dir, name),
fileID: filepath.Join(p.Key(), name),
})
}
}
sem := make(chan struct{}, maxConcurrentDownloads)
var wg sync.WaitGroup
var totalDownloaded int64
errCh := make(chan error, len(tasks))
for _, t := range tasks {
t := t
wg.Add(1)
sem <- struct{}{}
go func() {
defer wg.Done()
defer func() { <-sem }()
n, err := fetchOnePayloadWithRetries(t.payload, t.dest, t.fileID, allowHashMismatch)
if err != nil {
errCh <- err
return
}
atomic.AddInt64(&totalDownloaded, n)
}()
}
wg.Wait()
close(errCh)
for err := range errCh {
if err != nil {
return err
}
}
fmt.Printf("Downloaded %s in total\n", HumanizeBytes(totalDownloaded))
return nil
}
func fetchOnePayloadWithRetries(payload Payload, dest, fileID string, allowHashMismatch bool) (int64, error) {
var lastErr error
for attempt := 0; attempt < maxDownloadAttempts; attempt++ {
n, err := tryDownloadPayload(payload, dest, fileID, allowHashMismatch)
if err == nil {
return n, nil
}
lastErr = err
fmt.Printf("%v\n", err)
}
return 0, fmt.Errorf("giving up on %s after %d attempts: %w", fileID, maxDownloadAttempts, lastErr)
}
func tryDownloadPayload(payload Payload, dest, fileID string, allowHashMismatch bool) (int64, error) {
if fi, err := os.Stat(dest); err == nil && fi.Mode().IsRegular() {
if payload.SHA256 != "" {
sum, err := sha256File(dest)
if err != nil {
return 0, err
}
if !equalFoldHex(sum, payload.SHA256) {
fmt.Printf("Incorrect existing file %s, removing\n", fileID)
os.Remove(dest)
} else {
fmt.Printf("Using existing file %s\n", fileID)
return 0, nil
}
} else {
return 0, nil
}
}
fmt.Printf("Downloading %s (%s)\n", fileID, HumanizeBytes(payload.Size))
if err := httpDownloadFile(payload.URL, dest); err != nil {
return 0, err
}
if payload.SHA256 != "" {
sum, err := sha256File(dest)
if err != nil {
return 0, err
}
if !equalFoldHex(sum, payload.SHA256) {
if allowHashMismatch {
fmt.Printf("WARNING: incorrect hash for downloaded file %s\n", fileID)
} else {
return 0, fmt.Errorf("incorrect hash for downloaded file %s, aborting", fileID)
}
}
}
return payload.Size, nil
}
var downloadHTTPClient = &http.Client{Timeout: 30 * time.Minute}
func httpDownloadFile(url, dest string) error {
resp, err := downloadHTTPClient.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("GET %s: %s", url, resp.Status)
}
tmp := dest + ".part"
out, err := os.Create(tmp)
if err != nil {
return err
}
if _, err := io.Copy(out, resp.Body); err != nil {
out.Close()
os.Remove(tmp)
return err
}
if err := out.Close(); err != nil {
os.Remove(tmp)
return err
}
return os.Rename(tmp, dest)
}
func sha256File(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func equalFoldHex(a, b string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
ca, cb := a[i], b[i]
if ca >= 'A' && ca <= 'Z' {
ca += 'a' - 'A'
}
if cb >= 'A' && cb <= 'Z' {
cb += 'a' - 'A'
}
if ca != cb {
return false
}
}
return true
}