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
@@ -0,0 +1,18 @@
msvc-go-wine: skip VsDevCmd's telemetry upload.
There's no telemetry endpoint reachable (or wanted) under Wine. Rather than
touching the conditional logic below, just set the opt-out switch the
script already checks for on its own.
diff --git a/Common7/Tools/VsDevCmd.bat b/Common7/Tools/VsDevCmd.bat
index f925e95..f3710ad 100644
--- a/Common7/Tools/VsDevCmd.bat
+++ b/Common7/Tools/VsDevCmd.bat
@@ -247,6 +247,7 @@ exit /B 0
:end
@REM Send Telemetry if user's VS is opted-in
+set "VSCMD_SKIP_SENDTELEMETRY=1"
if "%VSCMD_SKIP_SENDTELEMETRY%"=="" (
if "%VSCMD_DEBUG%" NEQ "" (
@echo [DEBUG:%~nx0] Sending telemetry
@@ -0,0 +1,20 @@
msvc-go-wine: always record the starting directory.
vsdevcmd_start.bat only saved __VSCMD_CURRENT_DIR when the caller passed
-startdir=none, which doesn't hold up under Wine's cmd.exe (see
https://bugs.winehq.org/show_bug.cgi?id=57137). Just save it
unconditionally instead of gating on the flag.
diff --git a/Common7/Tools/vsdevcmd/core/vsdevcmd_start.bat b/Common7/Tools/vsdevcmd/core/vsdevcmd_start.bat
index 21caae9..fc18228 100644
--- a/Common7/Tools/vsdevcmd/core/vsdevcmd_start.bat
+++ b/Common7/Tools/vsdevcmd/core/vsdevcmd_start.bat
@@ -8,7 +8,7 @@ call "%~dp0parse_cmd.bat" %*
if "%ERRORLEVEL%" NEQ "0" set /A __vscmd_vsdevcmd_errcount=__vscmd_vsdevcmd_errcount+1
@REM Save the current directory, if -startdir=none was specified by the user.
-if /I "%VSCMD_ARG_STARTDIR%" == "none" set "__VSCMD_CURRENT_DIR=%CD%"
+set "__VSCMD_CURRENT_DIR=%CD%"
if "%VSCMD_DEBUG%" GEQ "2" (
@echo [DEBUG:%~n0] Parsing results...
@@ -0,0 +1,66 @@
msvc-go-wine: find the Windows SDK/UCRT dirs on disk before trying the registry.
winsdk.bat locates the Windows 10/11 SDK and Universal CRT SDK by querying
the Windows Registry, which doesn't have real SDK installation entries
under Wine. msvc-wine's own install layout always puts the SDK directly
under "%VSINSTALLDIR%Windows Kits\10", so check that location first and
only fall back to the registry-based lookups when it's not there.
diff --git a/Common7/Tools/vsdevcmd/core/winsdk.bat b/Common7/Tools/vsdevcmd/core/winsdk.bat
index af09494..c303775 100644
--- a/Common7/Tools/vsdevcmd/core/winsdk.bat
+++ b/Common7/Tools/vsdevcmd/core/winsdk.bat
@@ -64,6 +64,11 @@ exit /B 0
if "%VSCMD_DEBUG%" GEQ "3" goto :GetWin10SdkDirVerbose
+if exist "%VSINSTALLDIR%Windows Kits\10\include\" (
+ SET "WindowsSdkDir=%VSINSTALLDIR%Windows Kits\10\"
+ exit /B 0
+)
+
call :GetWin10SdkDirHelper HKLM\SOFTWARE\Wow6432Node > nul 2>&1
if errorlevel 1 call :GetWin10SdkDirHelper HKCU\SOFTWARE\Wow6432Node > nul 2>&1
if errorlevel 1 call :GetWin10SdkDirHelper HKLM\SOFTWARE > nul 2>&1
@@ -73,6 +78,11 @@ exit /B 0
:GetWin10SdkDirVerbose
+if exist "%VSINSTALLDIR%Windows Kits\10\include\" (
+ SET "WindowsSdkDir=%VSINSTALLDIR%Windows Kits\10\"
+ exit /B 0
+)
+
call :GetWin10SdkDirHelper HKLM\SOFTWARE\Wow6432Node
if errorlevel 1 call :GetWin10SdkDirHelper HKCU\SOFTWARE\Wow6432Node
if errorlevel 1 call :GetWin10SdkDirHelper HKLM\SOFTWARE
@@ -231,6 +241,14 @@ set UniversalCRTSdkDir=
if "%VSCMD_DEBUG%" GEQ "3" goto :GetUniversalCRTSdkDirVerbose
+if exist "%WindowsSdkDir%Lib\%WindowsSDKVersion%ucrt\" (
+ SET "UniversalCRTSdkDir=%WindowsSdkDir%"
+ exit /B 0
+) else if exist "%VSINSTALLDIR%Windows Kits\10\Lib\" (
+ SET "UniversalCRTSdkDir=%VSINSTALLDIR%Windows Kits\10\"
+ exit /B 0
+)
+
call :GetUniversalCRTSdkDirHelper HKLM\SOFTWARE\Wow6432Node > nul 2>&1
if errorlevel 1 call :GetUniversalCRTSdkDirHelper HKCU\SOFTWARE\Wow6432Node > nul 2>&1
if errorlevel 1 call :GetUniversalCRTSdkDirHelper HKLM\SOFTWARE > nul 2>&1
@@ -240,6 +258,14 @@ exit /B 0
:GetUniversalCRTSdkDirVerbose
+if exist "%WindowsSdkDir%Lib\%WindowsSDKVersion%ucrt\" (
+ SET "UniversalCRTSdkDir=%WindowsSdkDir%"
+ exit /B 0
+) else if exist "%VSINSTALLDIR%Windows Kits\10\Lib\" (
+ SET "UniversalCRTSdkDir=%VSINSTALLDIR%Windows Kits\10\"
+ exit /B 0
+)
+
call :GetUniversalCRTSdkDirHelper HKLM\SOFTWARE\Wow6432Node
if errorlevel 1 call :GetUniversalCRTSdkDirHelper HKCU\SOFTWARE\Wow6432Node
if errorlevel 1 call :GetUniversalCRTSdkDirHelper HKLM\SOFTWARE
@@ -0,0 +1,19 @@
msvc-go-wine: don't hard-fail devcmd setup when ConnectionManagerExe is missing.
Not every MSVC/WinSDK download selection includes the Linux
ConnectionManagerExe component; a missing optional component shouldn't
abort the whole Developer Command Prompt setup.
diff --git a/Common7/Tools/vsdevcmd/ext/ConnectionManagerExe.bat b/Common7/Tools/vsdevcmd/ext/ConnectionManagerExe.bat
index 871b9af..f069a25 100644
--- a/Common7/Tools/vsdevcmd/ext/ConnectionManagerExe.bat
+++ b/Common7/Tools/vsdevcmd/ext/ConnectionManagerExe.bat
@@ -10,7 +10,7 @@ set "PATH=%PATH%;%VSINSTALLDIR%Common7\IDE\VC\Linux\bin\ConnectionManagerExe"
goto :end
:error_setting_path
-exit /B 1
+exit /B 0
goto :end
@@ -0,0 +1,18 @@
msvc-go-wine: don't hard-fail devcmd setup when the bundled CMake/Ninja is missing.
Same reasoning as the ConnectionManagerExe fix: a missing optional
component shouldn't abort the whole Developer Command Prompt setup.
diff --git a/Common7/Tools/vsdevcmd/ext/cmake.bat b/Common7/Tools/vsdevcmd/ext/cmake.bat
index d9a1088..584f141 100644
--- a/Common7/Tools/vsdevcmd/ext/cmake.bat
+++ b/Common7/Tools/vsdevcmd/ext/cmake.bat
@@ -10,7 +10,7 @@ set "PATH=%PATH%;%VSINSTALLDIR%Common7\IDE\CommonExtensions\Microsoft\CMake\CMak
goto :end
:error_setting_path
-exit /B 1
+exit /B 0
goto :end
@@ -0,0 +1,24 @@
<!--
msvc-go-wine: make MSBuild resolve the Windows SDK / UCRT / preferred tool
architecture from the VS install layout directly, since there's no
Windows Registry under Wine for the stock .props files to query.
Populates MsvcGoWine_ExtraSdkRoots, which
Microsoft.Cpp.WindowsSDK.props.patch (see the assets/patches directory)
passes into GetLatestSDKTargetPlatformVersion as an extra search root.
-->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(WindowsSdkDir_10)' == '' and (
'$(WindowsTargetPlatformVersion)' == '' or
'$(WindowsTargetPlatformVersion)' == '10.0' or
Exists('$(VSInstallRoot)\Windows Kits\10\Include\$(WindowsTargetPlatformVersion)\shared\sdkddkver.h'))">
<MsvcGoWine_ExtraSdkRoots>$(VSInstallRoot)</MsvcGoWine_ExtraSdkRoots>
<WindowsSdkDir_10>$(VSInstallRoot)\Windows Kits\10\</WindowsSdkDir_10>
<UniversalCRTSdkDir_10 Condition="'$(UniversalCRTSdkDir_10)' == ''">$(WindowsSdkDir_10)</UniversalCRTSdkDir_10>
<UCRTContentRoot Condition="'$(UCRTContentRoot)' == ''">$(WindowsSdkDir_10)</UCRTContentRoot>
</PropertyGroup>
<PropertyGroup>
<PreferredToolArchitecture Condition="'$(PROCESSOR_ARCHITECTURE)' == 'AMD64'">x64</PreferredToolArchitecture>
</PropertyGroup>
</Project>
@@ -0,0 +1,20 @@
msvc-go-wine: let MSBuild find the Windows SDK without the registry.
GetLatestSDKTargetPlatformVersion normally resolves the SDK via the
registry, which has nothing to find under Wine. Pass it an extra list of
roots (populated by msvc-go-wine.props, see the ImportBefore directory next
to this file) so it can also look directly under the VS install root.
diff --git a/MSBuild/Microsoft/VC/v180/Microsoft.Cpp.WindowsSDK.props b/MSBuild/Microsoft/VC/v180/Microsoft.Cpp.WindowsSDK.props
index 5003a04..2a785a4 100644
--- a/MSBuild/Microsoft/VC/v180/Microsoft.Cpp.WindowsSDK.props
+++ b/MSBuild/Microsoft/VC/v180/Microsoft.Cpp.WindowsSDK.props
@@ -110,7 +110,7 @@ Copyright (C) Microsoft Corporation. All rights reserved.
<SDKVersion Condition="'$(SDKVersion)' == ''">10.0</SDKVersion>
<SDKDisplayName Condition="'$(SDKDisplayName)' == ''">Windows 10</SDKDisplayName>
- <_LatestWindowsTargetPlatformVersion>$([Microsoft.Build.Utilities.ToolLocationHelper]::GetLatestSDKTargetPlatformVersion($(SDKIdentifier), $(SDKVersion)))</_LatestWindowsTargetPlatformVersion>
+ <_LatestWindowsTargetPlatformVersion>$([Microsoft.Build.Utilities.ToolLocationHelper]::GetLatestSDKTargetPlatformVersion($(SDKIdentifier), $(SDKVersion), $(MsvcGoWine_ExtraSdkRoots.Split(';'))))</_LatestWindowsTargetPlatformVersion>
<WindowsTargetPlatformVersion Condition="'$(WindowsTargetPlatformVersion)' == '' and '$(_LatestWindowsTargetPlatformVersion)' != ''">10.0</WindowsTargetPlatformVersion>
</PropertyGroup>