2 Commits
Author SHA1 Message Date
Cheviiot 867c915596 Fix MSBuild toolset/SDK detection under Wine
msbuild <project>.vcxproj previously failed with MSB8020 ("build tools for
vNNN cannot be found") because MSBuild's own toolset/SDK resolution reads
a different set of environment variables than cl/link/lib do directly
(VCInstallDir_<N>, VCToolsInstallDir_<N>, VsInstallRoot,
WindowsSdkDir_10, WindowsTargetPlatformVersion, DisableRegistryUse, etc) -
none of which the generic INCLUDE/LIB/WINEPATH env covered.

Added msbuildEnv, populated for every MSBuild toolset generation actually
present under MSBuild/Microsoft/VC/v*, and wired into the msbuild wrapper.

Verified end-to-end: msbuild successfully builds ocornut/imgui's
example_win32_directx11.vcxproj (retargeted from its original v141
PlatformToolset to this install's v145) - compiles all 8 sources and
links against d3d11.lib/d3dcompiler.lib/dxgi.lib, producing a valid
PE32+ executable.
2026-07-25 01:58:18 +10:00
Cheviiot 8f52e5861a Default download/install to ~/.msvc-go-wine when no dir is given
--dest (download) and the positional dir (install) were previously
required, forcing every user to pick and remember a location. Both now
default to a single hidden ~/.msvc-go-wine, matching the convention most
CLI tools use for their own data dir - still overridable for anyone who
wants a different location.
2026-07-25 01:38:47 +10:00
8 changed files with 165 additions and 20 deletions
+11 -8
View File
@@ -27,15 +27,16 @@ name it's invoked as (a "multi-call binary", like busybox):
## Quick start ## Quick start
```bash ```bash
# 1. Download and unpack MSVC + Windows SDK (requires accepting Microsoft's # 1. Download and unpack MSVC + Windows SDK into ~/.msvc-go-wine (requires
# Visual Studio Build Tools license, and msitools for unpacking .msi payloads) # accepting Microsoft's Visual Studio Build Tools license, and msitools
msvc-go-wine download --accept-license --dest ~/my_msvc # for unpacking .msi payloads). Pass --dest <dir> for a different location.
msvc-go-wine download --accept-license
# 2. Wire up the tool wrappers # 2. Wire up the tool wrappers
msvc-go-wine install ~/my_msvc msvc-go-wine install
# 3. Add the toolchain to PATH and build # 3. Add the toolchain to PATH and build
export PATH=~/my_msvc/bin/x64:$PATH export PATH=~/.msvc-go-wine/bin/x64:$PATH
cl /nologo /EHsc hello.cpp cl /nologo /EHsc hello.cpp
``` ```
@@ -55,12 +56,14 @@ pkcon install wine msitools
## Commands ## Commands
``` ```
msvc-go-wine download --dest <dir> [options] fetch and unpack MSVC/WinSDK msvc-go-wine download --accept-license [--dest <dir>] [options] fetch and unpack MSVC/WinSDK
msvc-go-wine install <dir> wire up wrappers for a downloaded MSVC 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 env --bin <dir>/bin/<arch> print INCLUDE/LIB for native clang-cl/lld-link use
msvc-go-wine version print the version msvc-go-wine version print the version
``` ```
`--dest`/`[dir]` both default to `~/.msvc-go-wine` when omitted.
`download` supports `--msvc-version`, `--sdk-version`, `--architecture`, `download` supports `--msvc-version`, `--sdk-version`, `--architecture`,
`--host-arch`, `--with-*` component toggles, `--ignore`, `--only-download`, `--host-arch`, `--with-*` component toggles, `--ignore`, `--only-download`,
`--only-unpack`, `--keep-unpack`, `--cache`, `--language`, `--only-unpack`, `--keep-unpack`, `--cache`, `--language`,
@@ -73,7 +76,7 @@ You don't need Wine at all if you drive the (nonredistributable) MSVC/WinSDK
headers and libraries with Clang/LLD in MSVC-compatible mode: headers and libraries with Clang/LLD in MSVC-compatible mode:
```bash ```bash
eval "$(msvc-go-wine env --bin ~/my_msvc/bin/x64)" eval "$(msvc-go-wine env --bin ~/.msvc-go-wine/bin/x64)"
clang-cl -c hello.c clang-cl -c hello.c
lld-link hello.obj -out:hello.exe lld-link hello.obj -out:hello.exe
``` ```
+7 -2
View File
@@ -13,7 +13,7 @@ import (
func runDownload(args []string) int { func runDownload(args []string) int {
fs := flag.NewFlagSet("download", flag.ContinueOnError) fs := flag.NewFlagSet("download", flag.ContinueOnError)
dest := fs.String("dest", "", "directory to install into (required unless --only-download)") dest := fs.String("dest", "", "directory to install into (default: ~/.msvc-go-wine)")
cacheDir := fs.String("cache", "", "directory to use as a persistent download cache (default: a temp dir, removed afterwards)") 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") 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") preview := fs.Bool("preview", false, "download the preview/insiders channel instead of release/stable")
@@ -122,9 +122,14 @@ func runDownload(args []string) int {
} }
if !*onlyDownload && *dest == "" { if !*onlyDownload && *dest == "" {
fmt.Fprintln(os.Stderr, "msvc-go-wine download: --dest is required unless --only-download is set") def, err := defaultToolchainDir()
if err != nil {
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
return 1 return 1
} }
*dest = def
fmt.Println("--dest not set, using default:", *dest)
}
if err := download.FetchPayloads(selected, cache, *onlyDownload); err != nil { if err := download.FetchPayloads(selected, cache, *onlyDownload); err != nil {
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err) fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
+15 -3
View File
@@ -8,11 +8,23 @@ import (
) )
func runInstall(args []string) int { func runInstall(args []string) int {
if len(args) != 1 || args[0] == "-h" || args[0] == "--help" { if len(args) > 1 || (len(args) == 1 && (args[0] == "-h" || args[0] == "--help")) {
fmt.Fprintln(os.Stderr, "usage: msvc-go-wine install <dest>") fmt.Fprintln(os.Stderr, "usage: msvc-go-wine install [dest] (default: ~/.msvc-go-wine)")
return 1 return 1
} }
dest := args[0]
var dest string
if len(args) == 1 {
dest = args[0]
} else {
def, err := defaultToolchainDir()
if err != nil {
fmt.Fprintln(os.Stderr, "msvc-go-wine install:", err)
return 1
}
dest = def
fmt.Println("No directory given, using default:", dest)
}
self, err := os.Executable() self, err := os.Executable()
if err != nil { if err != nil {
+5 -2
View File
@@ -63,11 +63,14 @@ func printUsage() {
fmt.Fprint(os.Stderr, `msvc-go-wine - cross compile with MSVC on Linux via Wine fmt.Fprint(os.Stderr, `msvc-go-wine - cross compile with MSVC on Linux via Wine
Usage: Usage:
msvc-go-wine download --dest <dir> [options] fetch and unpack MSVC/WinSDK msvc-go-wine download --accept-license [--dest <dir>] [options]
msvc-go-wine install <dir> wire up wrappers for a downloaded MSVC 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 env --bin <dir/bin/arch> print INCLUDE/LIB for native clang-cl/lld-link use
msvc-go-wine version print the version msvc-go-wine version print the version
--dest/[dir] default to ~/.msvc-go-wine if omitted.
Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly: 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 cl, link, lib, ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd, findstr
`) `)
+17
View File
@@ -0,0 +1,17 @@
package main
import (
"os"
"path/filepath"
)
// defaultToolchainDir is where `download`/`install` operate when the user
// doesn't specify a directory: a hidden ~/.msvc-go-wine, so it doesn't
// clutter a plain `ls ~`.
func defaultToolchainDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".msvc-go-wine"), nil
}
+13
View File
@@ -16,6 +16,14 @@ type Paths struct {
SDKBinDir string // <dest>/kits/10/bin/<sdkver>/<host> - mc/midl/mt/rc live here SDKBinDir string // <dest>/kits/10/bin/<sdkver>/<host> - mc/midl/mt/rc live here
MSBuildBinDir string // <dest>/MSBuild/Current/Bin/<dotnetHost> - MSBuild.exe lives here MSBuildBinDir string // <dest>/MSBuild/Current/Bin/<dotnetHost> - MSBuild.exe lives here
// Windows-notation ("z:\...") equivalents of the paths above, needed to
// populate the MSBuild-specific environment variables its toolset/SDK
// detection props read (see msbuildEnv in the wrapper package).
BaseWin string // z:\<dest>
MSVCBaseWin string // z:\<dest>\vc
MSVCDirWin string // z:\<dest>\vc\tools\msvc\<ver>
SDKBaseWin string // z:\<dest>\kits\10
Include string Include string
Lib string Lib string
LibPath string LibPath string
@@ -89,6 +97,11 @@ func NewPaths(cfg *Config, baseUnix string) *Paths {
SDKBinDir: sdkBinDir, SDKBinDir: sdkBinDir,
MSBuildBinDir: msbuildBinDir, MSBuildBinDir: msbuildBinDir,
BaseWin: winBase,
MSVCBaseWin: msvcBase,
MSVCDirWin: msvcDirWin,
SDKBaseWin: sdkBase,
Include: include, Include: include,
Lib: lib, Lib: lib,
LibPath: lib, LibPath: lib,
+86
View File
@@ -0,0 +1,86 @@
package wrapper
import (
"path/filepath"
"regexp"
"strings"
"github.com/Cheviiot/msvc-go-wine/internal/wineenv"
)
var reToolsetDir = regexp.MustCompile(`^v(\d+)$`)
// msbuildEnv returns the extra environment variables MSBuild's own
// SDK/toolset-detection property sheets need. The generic INCLUDE/LIB/
// WINEPATH set by buildEnv are enough for cl/link/lib invoked directly, but
// MSBuild resolves the compiler location and Windows SDK through a
// different, registry-oriented mechanism - DisableRegistryUse=true
// redirects that lookup to these variables instead of a (nonexistent)
// Windows Registry.
func msbuildEnv(cfg *wineenv.Config, paths *wineenv.Paths) map[string]string {
env := map[string]string{
"DisableRegistryUse": "true",
"VCToolsVersion": cfg.MSVCVer,
"VsInstallRoot": paths.BaseWin + `\`,
"VSInstallDir": paths.BaseWin + `\`,
"MicrosoftKitRoot": paths.BaseWin + `\`,
"SDKReferenceDirectoryRoot": paths.BaseWin + `\`,
"SDKExtensionDirectoryRoot": paths.BaseWin + `\`,
"MSBUILDSDKREFERENCEDIRECTORY": paths.BaseWin + `\`,
"MSBUILDMULTIPLATFORMSDKREFERENCEDIRECTORY": paths.BaseWin + `\`,
"WindowsSdkDir_10": paths.SDKBaseWin + `\`,
"UniversalCRTSdkDir_10": paths.SDKBaseWin + `\`,
"WindowsSdkDir": paths.SDKBaseWin + `\`,
"UniversalCRTSdkDir": paths.SDKBaseWin + `\`,
"WindowsTargetPlatformVersion": cfg.SDKVer,
"UCRTContentRoot": paths.SDKBaseWin + `\`,
"NETFXKitsDir": paths.SDKBaseWin + `\`,
"NETFXSDKDir": paths.SDKBaseWin + `\`,
// WDK-specific properties; harmless when not building a driver.
"WDKKitVersion": "10",
"Driver_SpectreMitigation": "false",
"SignMode": "off",
"Inf2CatNoCatalog": "true",
"ApiValidator_Enable": "False",
"Platform": msbuildPlatform(cfg.Arch),
}
// Microsoft.Cpp.props resolves the compiler/toolset location through
// VCInstallDir_<N>/VCToolsInstallDir_<N>, where <N> is whatever numeric
// suffix the installed MSBuild toolset property sheets use (e.g.
// .../MSBuild/Microsoft/VC/v180 -> "180"). Populate every one actually
// present, so a project pinned to any of them resolves to the one real
// toolchain that's installed.
matches, _ := filepath.Glob(filepath.Join(paths.BaseUnix, "MSBuild", "Microsoft", "VC", "v*"))
for _, m := range matches {
sub := reToolsetDir.FindStringSubmatch(filepath.Base(m))
if sub == nil {
continue
}
env["VCInstallDir_"+sub[1]] = paths.MSVCBaseWin + `\`
env["VCToolsInstallDir_"+sub[1]] = paths.MSVCDirWin + `\`
}
if strings.HasSuffix(paths.MSBuildBinDir, "amd64") {
env["PreferredToolArchitecture"] = "x64"
}
return env
}
func msbuildPlatform(arch string) string {
switch arch {
case "x86":
return "Win32"
case "arm":
return "ARM"
case "arm64":
return "ARM64"
default:
return arch
}
}
+8 -2
View File
@@ -70,9 +70,15 @@ func Run(tool string, args []string) int {
var exitCode int var exitCode int
switch { switch {
case s.rawStdout: case s.rawStdout:
// MSBuild: skip all filtering/toolrelay, inherit stdio directly. // MSBuild: skip all filtering/toolrelay, inherit stdio directly, and
// add the extra environment MSBuild's own toolset/SDK-detection
// props need on top of the generic INCLUDE/LIB/WINEPATH.
cmd := exec.Command(wineBin, append([]string{toolExePath}, rewritten...)...) cmd := exec.Command(wineBin, append([]string{toolExePath}, rewritten...)...)
cmd.Env = buildEnv(paths) env := buildEnv(paths)
for k, v := range msbuildEnv(cfg, paths) {
env = append(env, k+"="+v)
}
cmd.Env = env
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr