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,211 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/internal/download"
|
||||
)
|
||||
|
||||
func runDownload(args []string) int {
|
||||
fs := flag.NewFlagSet("download", flag.ContinueOnError)
|
||||
dest := fs.String("dest", "", "directory to install into (required unless --only-download)")
|
||||
cacheDir := fs.String("cache", "", "directory to use as a persistent download cache (default: a temp dir, removed afterwards)")
|
||||
major := fs.Int("major", 18, "the major VS version to download")
|
||||
preview := fs.Bool("preview", false, "download the preview/insiders channel instead of release/stable")
|
||||
manifestFile := fs.String("manifest", "", "use a predownloaded installer manifest file instead of fetching one")
|
||||
acceptLicense := fs.Bool("accept-license", false, "don't prompt for accepting the license")
|
||||
msvcVersion := fs.String("msvc-version", "", "install a specific MSVC toolchain version, e.g. 17.10")
|
||||
sdkVersion := fs.String("sdk-version", "", "install a specific Windows SDK version")
|
||||
hostArch := fs.String("host-arch", "", "host architecture of packages to install (x86, x64, arm64; auto-detected)")
|
||||
onlyHost := fs.Bool("only-host", true, "only download packages matching the host architecture")
|
||||
language := fs.String("language", "en", "preferred language code for packages available in multiple languages")
|
||||
includeOptional := fs.Bool("include-optional", false, "include all optional dependencies")
|
||||
skipRecommended := fs.Bool("skip-recommended", false, "don't include recommended dependencies")
|
||||
onlyDownload := fs.Bool("only-download", false, "stop after downloading package files")
|
||||
onlyUnpack := fs.Bool("only-unpack", false, "unpack selected packages and keep everything, without pruning to just the CLI tools")
|
||||
keepUnpack := fs.Bool("keep-unpack", false, "keep the scratch unpack dir instead of removing it after moving files into place")
|
||||
skipPatch := fs.Bool("skip-patch", false, "don't apply the Wine compatibility patches")
|
||||
var archsFlag stringList
|
||||
fs.Var(&archsFlag, "architecture", "target architecture to include (x86, x64, arm, arm64, host); repeatable")
|
||||
var ignoreFlag stringList
|
||||
fs.Var(&ignoreFlag, "ignore", "package id to skip; repeatable")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
packages := fs.Args()
|
||||
|
||||
opts := &download.Options{
|
||||
Package: packages,
|
||||
Ignore: []string(ignoreFlag),
|
||||
Architecture: []string(archsFlag),
|
||||
HostArch: *hostArch,
|
||||
OnlyHost: *onlyHost,
|
||||
MSVCVersion: *msvcVersion,
|
||||
SDKVersion: *sdkVersion,
|
||||
IncludeOptional: *includeOptional,
|
||||
SkipRecommended: *skipRecommended,
|
||||
Language: *language,
|
||||
}
|
||||
|
||||
manifestURL := *manifestFile
|
||||
if manifestURL == "" {
|
||||
url, err := download.FetchChannelManifest(*major, *preview)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
return 1
|
||||
}
|
||||
manifestURL = url
|
||||
} else {
|
||||
manifestURL = "file:" + manifestURL
|
||||
}
|
||||
|
||||
manifest, err := download.FetchInstallerManifest(manifestURL)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if opts.HostArch == "" {
|
||||
opts.HostArch = detectHostArch()
|
||||
}
|
||||
fmt.Println("Install packages for", opts.HostArch, "host architecture")
|
||||
|
||||
idx := download.BuildIndex(manifest, opts.HostArch, opts.Language)
|
||||
|
||||
if !*acceptLicense {
|
||||
license := "the Visual Studio Build Tools license"
|
||||
if p := idx.Find("Microsoft.VisualStudio.Product.BuildTools", nil); p != nil && len(p.LocalizedResources) > 0 {
|
||||
license = p.LocalizedResources[0].License
|
||||
}
|
||||
if !promptAcceptLicense(license) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
if err := download.ResolveSelection(opts, idx); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
selected, err := download.ExpandSelection(idx, opts)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
return 1
|
||||
}
|
||||
var downloadSize, installSize int64
|
||||
for _, p := range selected {
|
||||
downloadSize += p.DownloadSize()
|
||||
installSize += p.InstalledSize()
|
||||
}
|
||||
fmt.Printf("Selected %d packages, for a total download size of %s, install size of %s\n",
|
||||
len(selected), download.HumanizeBytes(downloadSize), download.HumanizeBytes(installSize))
|
||||
|
||||
cache := *cacheDir
|
||||
removeCache := false
|
||||
if cache == "" {
|
||||
tmp, err := os.MkdirTemp("", "msvc-go-wine-cache-")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
return 1
|
||||
}
|
||||
cache = tmp
|
||||
removeCache = true
|
||||
}
|
||||
if removeCache {
|
||||
defer os.RemoveAll(cache)
|
||||
}
|
||||
|
||||
if !*onlyDownload && *dest == "" {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download: --dest is required unless --only-download is set")
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := download.FetchPayloads(selected, cache, *onlyDownload); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
return 1
|
||||
}
|
||||
if *onlyDownload {
|
||||
return 0
|
||||
}
|
||||
|
||||
destAbs, err := filepath.Abs(*dest)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
unpack := destAbs
|
||||
if !*onlyUnpack {
|
||||
unpack = filepath.Join(destAbs, "unpack")
|
||||
}
|
||||
if err := download.UnpackSelectedPackages(selected, cache, unpack); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Wine doesn't honor .exe.config <dependentAssembly> redirects, so copy
|
||||
// MSBuild's redirected assemblies next to it directly.
|
||||
for _, hostArch := range []string{"amd64", "arm64"} {
|
||||
msbuildExe := filepath.Join(unpack, "MSBuild", "Current", "Bin", hostArch, "MSBuild.exe")
|
||||
if err := download.CopyRedirectedAssemblies(msbuildExe); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
if !*onlyUnpack {
|
||||
if err := download.RelocateBuildTools(unpack, destAbs); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
return 1
|
||||
}
|
||||
if !*keepUnpack {
|
||||
os.RemoveAll(unpack)
|
||||
}
|
||||
if !*skipPatch && *major == 18 {
|
||||
if err := download.ApplyCompatibilityFixes(destAbs); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Done. Next: msvc-go-wine install", destAbs)
|
||||
return 0
|
||||
}
|
||||
|
||||
func detectHostArch() string {
|
||||
if runtime.GOARCH == "arm64" {
|
||||
return "arm64"
|
||||
}
|
||||
return "x64"
|
||||
}
|
||||
|
||||
func promptAcceptLicense(license string) bool {
|
||||
fmt.Printf("Do you accept the license at %s (yes/no)? ", license)
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
switch scanner.Text() {
|
||||
case "yes":
|
||||
return true
|
||||
case "no":
|
||||
return false
|
||||
}
|
||||
fmt.Print("Do you accept the license? Answer \"yes\" or \"no\": ")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// stringList implements flag.Value to collect a repeatable string flag.
|
||||
type stringList []string
|
||||
|
||||
func (s *stringList) String() string { return fmt.Sprint([]string(*s)) }
|
||||
func (s *stringList) Set(v string) error {
|
||||
*s = append(*s, v)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/internal/wineenv"
|
||||
)
|
||||
|
||||
// runEnv prints shell `export` statements for INCLUDE/LIB (converted from
|
||||
// wine's "z:\..." notation to plain unix paths) and TARGET_TRIPLE, for
|
||||
// driving clang-cl/lld-link directly without Wine.
|
||||
// Usage: eval "$(msvc-go-wine env --bin <dest>/bin/<arch>)"
|
||||
func runEnv(args []string) int {
|
||||
fs := flag.NewFlagSet("env", flag.ContinueOnError)
|
||||
bin := fs.String("bin", "", "the <dest>/bin/<arch> directory produced by `msvc-go-wine install`")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
if *bin == "" {
|
||||
fmt.Fprintln(os.Stderr, "usage: msvc-go-wine env --bin <dest>/bin/<arch>")
|
||||
return 1
|
||||
}
|
||||
|
||||
cfg, err := wineenv.Load(*bin)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine env:", err)
|
||||
return 1
|
||||
}
|
||||
baseUnix, err := wineenv.FindBaseUnix(*bin)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine env:", err)
|
||||
return 1
|
||||
}
|
||||
paths := wineenv.NewPaths(cfg, baseUnix)
|
||||
|
||||
triple, ok := targetTriples[cfg.Arch]
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "msvc-go-wine env: unknown arch %q\n", cfg.Arch)
|
||||
return 1
|
||||
}
|
||||
|
||||
fmt.Printf("export INCLUDE=%q\n", toUnixPathList(paths.Include))
|
||||
fmt.Printf("export LIB=%q\n", toUnixPathList(paths.Lib))
|
||||
fmt.Printf("export TARGET_TRIPLE=%q\n", triple)
|
||||
return 0
|
||||
}
|
||||
|
||||
var targetTriples = map[string]string{
|
||||
"x86": "i686-windows-msvc",
|
||||
"x64": "x86_64-windows-msvc",
|
||||
"arm": "armv7-windows-msvc",
|
||||
"arm64": "aarch64-windows-msvc",
|
||||
}
|
||||
|
||||
// toUnixPathList does a blanket removal of the "z:" drive prefix and
|
||||
// backslash->slash conversion across the whole semicolon-joined path list.
|
||||
func toUnixPathList(s string) string {
|
||||
s = strings.ReplaceAll(s, "z:", "")
|
||||
s = strings.ReplaceAll(s, `\`, "/")
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/internal/install"
|
||||
)
|
||||
|
||||
func runInstall(args []string) int {
|
||||
if len(args) != 1 || args[0] == "-h" || args[0] == "--help" {
|
||||
fmt.Fprintln(os.Stderr, "usage: msvc-go-wine install <dest>")
|
||||
return 1
|
||||
}
|
||||
dest := args[0]
|
||||
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine install:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := install.Install(dest, self); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine install:", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Println("Done. Add", dest+"/bin/<arch> to PATH to use cl, link, lib, ...")
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Command msvc-go-wine cross compiles with the real MSVC toolchain on Linux
|
||||
// via Wine. It's a multi-call binary that behaves as `cl`, `link`, `lib`,
|
||||
// `rc`, `midl`, `mt`, `dumpbin`, `msbuild`, etc. when invoked under one of
|
||||
// those names (via symlinks set up by `msvc-go-wine install`), and
|
||||
// otherwise exposes the `download`/`install`/`env`/`version` management
|
||||
// subcommands.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/internal/wrapper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
base := filepath.Base(os.Args[0])
|
||||
name := strings.TrimSuffix(strings.ToLower(base), ".exe")
|
||||
|
||||
if _, ok := wrapper.Tools[name]; ok {
|
||||
os.Exit(wrapper.Run(name, os.Args[1:]))
|
||||
}
|
||||
if name == "cmd" || name == "findstr" {
|
||||
os.Exit(wrapper.Run(name, os.Args[1:]))
|
||||
}
|
||||
|
||||
os.Exit(runCLI(os.Args[1:]))
|
||||
}
|
||||
|
||||
func runCLI(args []string) int {
|
||||
if len(args) == 0 {
|
||||
printUsage()
|
||||
return 1
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "download":
|
||||
return runDownload(args[1:])
|
||||
case "install":
|
||||
return runInstall(args[1:])
|
||||
case "env":
|
||||
return runEnv(args[1:])
|
||||
case "version":
|
||||
fmt.Println("msvc-go-wine dev")
|
||||
return 0
|
||||
case "-h", "--help", "help":
|
||||
printUsage()
|
||||
return 0
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "msvc-go-wine: unknown subcommand %q\n\n", args[0])
|
||||
printUsage()
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Fprint(os.Stderr, `msvc-go-wine - cross compile with MSVC on Linux via Wine
|
||||
|
||||
Usage:
|
||||
msvc-go-wine download --dest <dir> [options] fetch and unpack MSVC/WinSDK
|
||||
msvc-go-wine install <dir> wire up wrappers for a downloaded MSVC
|
||||
msvc-go-wine env --bin <dir/bin/arch> print INCLUDE/LIB for native clang-cl/lld-link use
|
||||
msvc-go-wine version print the version
|
||||
|
||||
Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
|
||||
cl, link, lib, ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd, findstr
|
||||
`)
|
||||
}
|
||||
Reference in New Issue
Block a user