mirror of
https://github.com/Cheviiot/Vintner.git
synced 2026-08-03 15:57:24 +00:00
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.
238 lines
6.8 KiB
Go
238 lines
6.8 KiB
Go
// 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)
|
|
}
|
|
}
|