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
+131
View File
@@ -0,0 +1,131 @@
package install
import (
"os"
"path/filepath"
"strings"
)
// LowercaseOptions controls how Lowercase renames entries.
type LowercaseOptions struct {
// Symlink adds lowercase-named symlinks alongside the original entries
// instead of renaming them in place (used for WinSDK/MSVC headers so
// both casings stay available; renaming would break other packages
// that reference the original casing).
Symlink bool
// MapWinSDK is shorthand for path-keyed overrides that keep the "GL"
// header directory's canonical uppercase spelling.
MapWinSDK bool
}
// Lowercase recursively lowercases every file/dir name under root, merging
// into an existing same-named lowercase directory on collision.
func Lowercase(root string, opts LowercaseOptions) error {
mapPaths := map[string]string{}
if opts.MapWinSDK {
mapPaths["gl"] = "GL"
}
remap := func(relPath string) string {
rp := strings.TrimSuffix(relPath, "/")
base := rp
if idx := strings.LastIndexByte(rp, '/'); idx >= 0 {
base = rp[idx+1:]
}
if opts.MapWinSDK {
if v, ok := mapPaths[strings.ToLower(rp)]; ok {
return v
}
}
return strings.ToLower(base)
}
var doDir func(dir, relPath string) error
doDir = func(dir, relPath string) error {
entries, err := os.ReadDir(dir)
if err != nil {
return err
}
for _, e := range entries {
name := e.Name()
childPath := filepath.Join(dir, name)
if e.IsDir() {
if err := doDir(childPath, relPath+name+"/"); err != nil {
return err
}
continue
}
newName := remap(relPath + name)
if newName != name {
if err := renameOrSymlink(childPath, dir, newName, opts.Symlink); err != nil {
return err
}
}
}
var newName string
if relPath == "" {
newName = strings.ToLower(filepath.Base(dir))
} else {
newName = remap(relPath)
}
oldName := filepath.Base(dir)
if oldName == newName {
return nil
}
parent := filepath.Dir(dir)
newPath := filepath.Join(parent, newName)
if fi, err := os.Stat(newPath); err == nil && fi.IsDir() {
return combineIntoDir(dir, newPath, opts.Symlink)
}
return renameOrSymlink(dir, parent, newName, opts.Symlink)
}
return doDir(root, "")
}
func renameOrSymlink(src, destDir, destName string, symlink bool) error {
dest := filepath.Join(destDir, destName)
if symlink {
if _, err := os.Lstat(dest); err == nil {
// A conflicting entry already exists at dest - this happens on
// case-insensitive filesystems where dest and src are the same
// path, so treat it as already done rather than failing.
return nil
}
rel, err := filepath.Rel(destDir, src)
if err != nil {
rel = src
}
return os.Symlink(rel, dest)
}
return os.Rename(src, dest)
}
// combineIntoDir moves (or symlinks) every entry of src into the already-existing
// dest, recursing into same-named subdirectories, then removes src (unless
// symlink mode, where src's real content must be left in place).
func combineIntoDir(src, dest string, symlink bool) error {
entries, err := os.ReadDir(src)
if err != nil {
return err
}
for _, e := range entries {
name := e.Name()
srcChild := filepath.Join(src, name)
destChild := filepath.Join(dest, name)
if fi, err := os.Stat(destChild); err == nil && fi.IsDir() && e.IsDir() {
if err := combineIntoDir(srcChild, destChild, symlink); err != nil {
return err
}
continue
}
if err := renameOrSymlink(srcChild, dest, name, symlink); err != nil {
return err
}
}
if !symlink {
return os.Remove(src)
}
return nil
}