mirror of
https://github.com/Cheviiot/Vintner.git
synced 2026-08-03 15:57:24 +00:00
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.
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user