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,13 @@
|
||||
// Package assets embeds vendored files that don't belong in Go source: the
|
||||
// Wine compatibility patches applied after unpacking MSVC/WinSDK, and the
|
||||
// toolrelay.cpp native launcher helper (see internal/install and
|
||||
// internal/wrapper).
|
||||
package assets
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed patches
|
||||
var Patches embed.FS
|
||||
|
||||
//go:embed vendor/toolrelay.cpp
|
||||
var ToolRelaySource []byte
|
||||
@@ -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>
|
||||
|
||||
Vendored
+126
@@ -0,0 +1,126 @@
|
||||
// toolrelay.exe - a thin native launcher for a Wine-hosted MSVC tool
|
||||
// invocation, built and used by msvc-go-wine (see internal/install's
|
||||
// buildToolRelay and internal/wrapper's runViaToolRelay).
|
||||
//
|
||||
// It exists to solve two problems that are only solvable from inside a
|
||||
// real Windows process, not from the Unix side of Wine:
|
||||
//
|
||||
// 1. Wine truncates a Windows process's exit code down to a single byte
|
||||
// when reporting it back to the host OS. mt.exe (the manifest tool)
|
||||
// can legitimately exit with 0x41020001 ("manifest unchanged"), which
|
||||
// CMake's own build driver specifically checks for and translates to
|
||||
// 0xbb before treating it as success - see cmcmd.cxx upstream. If
|
||||
// Wine has already truncated 0x41020001 down to 0x01 by the time our
|
||||
// Go wrapper observes it, we can no longer make that translation
|
||||
// ourselves. Only a native Windows process calling
|
||||
// GetExitCodeProcess() can see the real, untruncated value - so this
|
||||
// helper does the translation itself, then exits with the already-
|
||||
// small 0xbb, which fits in a byte and survives Wine's truncation
|
||||
// intact.
|
||||
//
|
||||
// 2. It gives the launched process its own Job Object (with
|
||||
// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE), so an aborted build doesn't
|
||||
// leave orphaned child processes running under Wine.
|
||||
//
|
||||
// Usage: toolrelay.exe <real-tool.exe> [args...]
|
||||
//
|
||||
// Optional environment variables MSVCGOWINE_STDIN / MSVCGOWINE_STDOUT /
|
||||
// MSVCGOWINE_STDERR name files to redirect the child's standard handles to
|
||||
// (used by runViaToolRelay to relay output through named pipes back to the
|
||||
// Unix side); any that aren't set fall back to this process's own
|
||||
// inherited handles.
|
||||
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr DWORD kMtManifestUnchanged = 0x41020001;
|
||||
constexpr DWORD kMtManifestUnchangedForCMake = 0xbb;
|
||||
|
||||
HANDLE OpenRedirectOrFallback(const wchar_t *envVar, DWORD access,
|
||||
DWORD disposition, HANDLE fallback) {
|
||||
wchar_t path[32768];
|
||||
DWORD n = GetEnvironmentVariableW(envVar, path, ARRAYSIZE(path));
|
||||
if (n == 0 || n >= ARRAYSIZE(path)) {
|
||||
return fallback;
|
||||
}
|
||||
SECURITY_ATTRIBUTES sa{sizeof(sa), nullptr, TRUE};
|
||||
HANDLE h = CreateFileW(path, access,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
&sa, disposition, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
return h == INVALID_HANDLE_VALUE ? fallback : h;
|
||||
}
|
||||
|
||||
// CreateProcessW wants a single mutable command line string, quoting each
|
||||
// argument and escaping embedded quotes.
|
||||
std::wstring BuildCommandLine(int argc, wchar_t **argv) {
|
||||
std::wstring cmd;
|
||||
for (int i = 0; i < argc; i++) {
|
||||
if (i > 0) cmd += L' ';
|
||||
cmd += L'"';
|
||||
for (const wchar_t *c = argv[i]; *c; c++) {
|
||||
if (*c == L'"') cmd += L'\\';
|
||||
cmd += *c;
|
||||
}
|
||||
cmd += L'"';
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
HANDLE MakeKillOnCloseJob() {
|
||||
HANDLE job = CreateJobObjectW(nullptr, nullptr);
|
||||
if (!job) return nullptr;
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{};
|
||||
info.BasicLimitInformation.LimitFlags =
|
||||
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
|
||||
SetInformationJobObject(job, JobObjectExtendedLimitInformation, &info, sizeof(info));
|
||||
return job;
|
||||
}
|
||||
|
||||
bool IsMtExe(const wchar_t *path) {
|
||||
const wchar_t *name = wcsrchr(path, L'\\');
|
||||
name = name ? name + 1 : path;
|
||||
return _wcsicmp(name, L"mt.exe") == 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int wmain(int argc, wchar_t **argv) {
|
||||
if (argc < 2) {
|
||||
return 1;
|
||||
}
|
||||
const wchar_t *targetExe = argv[1];
|
||||
|
||||
STARTUPINFOW si{sizeof(si)};
|
||||
si.dwFlags = STARTF_USESTDHANDLES;
|
||||
si.hStdInput = OpenRedirectOrFallback(L"MSVCGOWINE_STDIN", GENERIC_READ,
|
||||
OPEN_EXISTING, GetStdHandle(STD_INPUT_HANDLE));
|
||||
si.hStdOutput = OpenRedirectOrFallback(L"MSVCGOWINE_STDOUT", GENERIC_WRITE,
|
||||
CREATE_ALWAYS, GetStdHandle(STD_OUTPUT_HANDLE));
|
||||
si.hStdError = OpenRedirectOrFallback(L"MSVCGOWINE_STDERR", GENERIC_WRITE,
|
||||
CREATE_ALWAYS, GetStdHandle(STD_ERROR_HANDLE));
|
||||
|
||||
HANDLE job = MakeKillOnCloseJob();
|
||||
std::wstring cmdLine = BuildCommandLine(argc - 1, argv + 1);
|
||||
|
||||
PROCESS_INFORMATION pi{};
|
||||
DWORD exitCode;
|
||||
if (CreateProcessW(targetExe, cmdLine.empty() ? nullptr : &cmdLine[0], nullptr,
|
||||
nullptr, TRUE, 0, nullptr, nullptr, &si, &pi)) {
|
||||
if (job) AssignProcessToJobObject(job, pi.hProcess);
|
||||
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||
if (!GetExitCodeProcess(pi.hProcess, &exitCode)) {
|
||||
exitCode = GetLastError();
|
||||
}
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
} else {
|
||||
exitCode = GetLastError();
|
||||
}
|
||||
|
||||
if (IsMtExe(targetExe) && exitCode == kMtManifestUnchanged) {
|
||||
exitCode = kMtManifestUnchangedForCMake;
|
||||
}
|
||||
return static_cast<int>(exitCode);
|
||||
}
|
||||
Reference in New Issue
Block a user