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:
Cheviiot
2026-07-25 00:57:41 +10:00
commit 6464da7847
41 changed files with 4207 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
// Package download fetches and unpacks MSVC/WinSDK using the same installer
// manifests Visual Studio's own installer uses.
package download
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// Payload is one downloadable file belonging to a Package.
type Payload struct {
FileName string `json:"fileName"`
URL string `json:"url"`
SHA256 string `json:"sha256"`
Size int64 `json:"size"`
}
// Name returns the payload's bare file name, stripping any directory
// components the manifest's fileName might carry.
func (p Payload) Name() string {
name := p.FileName
if i := strings.LastIndexByte(name, '\\'); i >= 0 {
name = name[i+1:]
}
if i := strings.LastIndexByte(name, '/'); i >= 0 {
name = name[i+1:]
}
return name
}
// Dependency is a normalized package dependency: manifests encode these
// either as a bare version string or as an object with version/type/id.
type Dependency struct {
TargetID string // the id to depend on (overrides the map key if set)
Version string
Type string // "", "Optional" or "Recommended"
}
// LocalizedResource carries the license URL shown before accepting a
// package's terms.
type LocalizedResource struct {
Language string `json:"language"`
License string `json:"license"`
}
// Package is one entry from the installer manifest's "packages" array.
type Package struct {
ID string `json:"id"`
Type string `json:"type"`
Version string `json:"version"`
Chip string `json:"chip"`
MachineArch string `json:"machineArch"`
ProductArch string `json:"productArch"`
Language string `json:"language"`
Payloads []Payload `json:"payloads"`
InstallSizes map[string]int64 `json:"installSizes"`
LocalizedResources []LocalizedResource `json:"localizedResources"`
DependenciesRaw map[string]json.RawMessage `json:"dependencies"`
dependencies map[string]Dependency
}
// Dependencies lazily normalizes DependenciesRaw into Dependency values.
func (p *Package) Dependencies() map[string]Dependency {
if p.dependencies != nil {
return p.dependencies
}
p.dependencies = map[string]Dependency{}
for key, raw := range p.DependenciesRaw {
var version string
if err := json.Unmarshal(raw, &version); err == nil {
p.dependencies[key] = Dependency{Version: version}
continue
}
var d struct {
Version string `json:"version"`
Type string `json:"type"`
ID string `json:"id"`
}
if err := json.Unmarshal(raw, &d); err == nil {
p.dependencies[key] = Dependency{TargetID: d.ID, Version: d.Version, Type: d.Type}
}
}
return p.dependencies
}
// Key uniquely identifies a specific package variant (id + version + arch),
// used to dedupe an already-included package during dependency resolution
// and to name its cache directory. Mirrors getPackageKey.
func (p *Package) Key() string {
key := p.ID
if p.Version != "" {
key += "-" + p.Version
}
if p.Chip != "" {
key += "-chip." + p.Chip
}
if p.MachineArch != "" {
key += "-machineArch." + p.MachineArch
}
if p.ProductArch != "" {
key += "-productArch." + p.ProductArch
}
return key
}
func (p *Package) InstalledSize() int64 {
var sum int64
for _, v := range p.InstallSizes {
sum += v
}
return sum
}
func (p *Package) DownloadSize() int64 {
var sum int64
for _, pl := range p.Payloads {
sum += pl.Size
}
return sum
}
// ChannelItem is one entry of a channel manifest's "channelItems", used only
// to locate the installer manifest URL.
type ChannelItem struct {
Type string `json:"type"`
Payloads []Payload `json:"payloads"`
}
// Manifest is the top-level installer manifest (or channel manifest, which
// shares the "info" field used for logging).
type Manifest struct {
Info struct {
ProductDisplayVersion string `json:"productDisplayVersion"`
} `json:"info"`
Packages []Package `json:"packages"`
ChannelItems []ChannelItem `json:"channelItems"`
}
// Manifests (particularly the installer manifest, a single JSON file
// listing every package) can be tens of MB, so give this a generous timeout
// and a few retries - transient network hiccups shouldn't need a full
// restart of `download`.
var httpClient = &http.Client{Timeout: 5 * time.Minute}
const maxManifestAttempts = 5
func httpGet(url string) ([]byte, error) {
var lastErr error
for attempt := 0; attempt < maxManifestAttempts; attempt++ {
data, err := tryHTTPGet(url)
if err == nil {
return data, nil
}
lastErr = err
fmt.Printf("GET %s: %v (retrying)\n", url, err)
}
return nil, fmt.Errorf("GET %s: giving up after %d attempts: %w", url, maxManifestAttempts, lastErr)
}
func tryHTTPGet(url string) ([]byte, error) {
resp, err := httpClient.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s", resp.Status)
}
return io.ReadAll(resp.Body)
}
// FetchChannelManifest downloads the top-level channel manifest for the
// given major VS version ("18" and up use the "stable"/"insiders" channel
// naming, earlier ones use "release"/"pre"), and returns the URL of the
// installer manifest it references.
func FetchChannelManifest(major int, preview bool) (string, error) {
kind := "stable"
if major < 18 {
kind = "release"
}
if preview {
kind = "insiders"
if major < 18 {
kind = "pre"
}
}
url := fmt.Sprintf("https://aka.ms/vs/%d/%s/channel", major, kind)
fmt.Println("Fetching", url)
data, err := httpGet(url)
if err != nil {
return "", err
}
var m Manifest
if err := json.Unmarshal(data, &m); err != nil {
return "", fmt.Errorf("parsing channel manifest: %w", err)
}
fmt.Printf("Got toplevel manifest for %s\n", m.Info.ProductDisplayVersion)
for _, item := range m.ChannelItems {
if item.Type == "Manifest" && len(item.Payloads) > 0 {
return item.Payloads[0].URL, nil
}
}
return "", fmt.Errorf("unable to find an installer manifest")
}
// FetchInstallerManifest downloads and parses the installer manifest at url.
func FetchInstallerManifest(url string) (*Manifest, error) {
data, err := httpGet(url)
if err != nil {
return nil, err
}
var m Manifest
if err := json.Unmarshal(data, &m); err != nil {
return nil, fmt.Errorf("parsing installer manifest: %w", err)
}
fmt.Printf("Loaded installer manifest for %s\n", m.Info.ProductDisplayVersion)
return &m, nil
}
// HumanizeBytes renders a byte count as a friendly "1.2 GB" style string.
func HumanizeBytes(s int64) string {
switch {
case s > 900*1024*1024:
return fmt.Sprintf("%.1f GB", float64(s)/(1024*1024*1024))
case s > 900*1024:
return fmt.Sprintf("%.1f MB", float64(s)/(1024*1024))
case s > 1024:
return fmt.Sprintf("%.1f KB", float64(s)/1024)
default:
return fmt.Sprintf("%d bytes", s)
}
}