Files
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

171 lines
3.8 KiB
Go

package download
import (
"regexp"
"sort"
"strings"
)
// Index maps a lowercased package id to every variant sharing that id,
// sorted by arch/language priority (best match first).
type Index map[string][]*Package
// BuildIndex indexes every package in m, prioritizing variants matching
// hostArch and language ("" language means no language preference beyond
// the default "en").
func BuildIndex(m *Manifest, hostArch, language string) Index {
if language == "" {
language = "en"
}
idx := Index{}
for i := range m.Packages {
p := &m.Packages[i]
key := strings.ToLower(p.ID)
idx[key] = append(idx[key], p)
}
for key, list := range idx {
list := list
sort.SliceStable(list, func(i, j int) bool {
return comparePackages(hostArch, language, list[i], list[j]) < 0
})
idx[key] = list
}
return idx
}
// comparePackages ranks arch match first, then language match.
func comparePackages(arch, language string, a, b *Package) int {
archOrd := func(field string, x *Package) int {
if arch == "" {
return 0
}
var v string
switch field {
case "chip":
v = x.Chip
case "machineArch":
v = x.MachineArch
case "productArch":
v = x.ProductArch
}
v = strings.ToLower(v)
if v == "" || v == "neutral" {
return 0
}
if v == arch {
return -1
}
return 1
}
for _, field := range []string{"chip", "machineArch", "productArch"} {
if r := archOrd(field, a) - archOrd(field, b); r != 0 {
return r
}
}
lang := strings.ToLower(language)
countryLang := strings.Contains(lang, "-")
langPriority := func(x *Package) int {
xl := strings.ToLower(x.Language)
if xl == "" {
return 0
}
if (countryLang && xl == lang) || (!countryLang && strings.HasPrefix(xl, lang+"-")) {
return 2
}
if strings.HasPrefix(xl, "en-") {
return 1
}
return 0
}
ap, bp := langPriority(a), langPriority(b)
if ap > bp {
return -1
}
if ap < bp {
return 1
}
return 0
}
// Find looks up id (case-insensitive), preferring a candidate matching
// constraints' chip/machineArch, falling back to the highest-priority
// variant. Returns nil if id isn't in the index at all.
func (idx Index) Find(id string, constraints map[string]string) *Package {
candidates, ok := idx[strings.ToLower(id)]
if !ok || len(candidates) == 0 {
return nil
}
for _, p := range candidates {
matched := true
for _, k := range []string{"chip", "machineArch"} {
want, has := constraints[k]
if !has {
continue
}
var got string
if k == "chip" {
got = p.Chip
} else {
got = p.MachineArch
}
if !strings.EqualFold(got, want) {
matched = false
break
}
}
if matched {
return p
}
}
return candidates[0]
}
var knownHostArchs = []string{"x86", "x64", "arm64"}
// HostArchCompatible reports whether p can run on host: some packages
// encode their host arch in the id itself (e.g.
// Microsoft.VisualCpp.Tools.HostARM64.*).
func HostArchCompatible(p *Package, host string) bool {
if host == "" {
return true
}
id := strings.ToLower(p.ID)
for _, a := range knownHostArchs {
if strings.Contains(id, "host"+a) {
return a == host
}
}
for _, v := range []string{p.Chip, p.MachineArch, p.ProductArch} {
v = strings.ToLower(v)
if v == "" || v == "neutral" {
continue
}
if v != host {
return false
}
}
return true
}
var reTargetArch = regexp.MustCompile(`\.target(x86|x64|arm64|arm)(\W|$)`)
// TargetArchCompatible reports whether p is wanted for archs: packages
// naming a target arch in their id (e.g. ...HostX64.TargetX64) are only
// wanted when that arch was requested.
func TargetArchCompatible(p *Package, archs []string) bool {
if archs == nil {
return true
}
m := reTargetArch.FindStringSubmatch(strings.ToLower(p.ID))
if m == nil {
return true
}
for _, a := range archs {
if a == m[1] {
return true
}
}
return false
}