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,3 @@
|
|||||||
|
/msvc-go-wine
|
||||||
|
*.test
|
||||||
|
.claude/
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Cheviiot
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
This license covers msvc-go-wine's own source code. It does not cover the
|
||||||
|
Microsoft Visual C++ Build Tools / Windows SDK that `msvc-go-wine download`
|
||||||
|
fetches and unpacks, which remain governed by Microsoft's own license terms.
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# msvc-go-wine
|
||||||
|
|
||||||
|
Cross compile with MSVC on Linux, using Wine — a single-binary Go tool
|
||||||
|
inspired by [mstorsjo/msvc-wine](https://github.com/mstorsjo/msvc-wine)'s
|
||||||
|
approach (download the real MSVC/WinSDK, wrap the compiler under Wine),
|
||||||
|
implemented independently.
|
||||||
|
|
||||||
|
Once installed, you invoke the real Microsoft toolchain exactly like on
|
||||||
|
Windows: `cl`, `link`, `lib`, `rc`, `midl`, `mt`, `dumpbin`, `msbuild`,
|
||||||
|
`nmake`, `ml`, `ml64`, `armasm`, `armasm64` all just work from your `PATH`.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
`msvc-go-wine` is one Go binary that behaves differently depending on the
|
||||||
|
name it's invoked as (a "multi-call binary", like busybox):
|
||||||
|
|
||||||
|
- Invoked as `cl`, `link`, `lib`, ... → it loads a small per-architecture
|
||||||
|
`env.json`, builds the `INCLUDE`/`LIB`/`WINEPATH` environment Wine needs,
|
||||||
|
rewrites absolute unix paths in the arguments into Wine's `z:\...` form
|
||||||
|
(working around [a Wine/cl.exe include-path bug](https://bugs.winehq.org/show_bug.cgi?id=55200)),
|
||||||
|
runs the real `.exe` under `wine`/`wine64`, and rewrites the tool's output
|
||||||
|
back from `z:\...` paths to plain unix paths so your build system's error
|
||||||
|
parsing keeps working.
|
||||||
|
- Invoked as `msvc-go-wine` → it exposes the `download`, `install`, `env` and
|
||||||
|
`version` management subcommands described below.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Download and unpack MSVC + Windows SDK (requires accepting Microsoft's
|
||||||
|
# Visual Studio Build Tools license, and msitools for unpacking .msi payloads)
|
||||||
|
msvc-go-wine download --accept-license --dest ~/my_msvc
|
||||||
|
|
||||||
|
# 2. Wire up the tool wrappers
|
||||||
|
msvc-go-wine install ~/my_msvc
|
||||||
|
|
||||||
|
# 3. Add the toolchain to PATH and build
|
||||||
|
export PATH=~/my_msvc/bin/x64:$PATH
|
||||||
|
cl /nologo /EHsc hello.cpp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- `wine` (or `wine64`) — runs the real `cl.exe`/`link.exe`/etc.
|
||||||
|
- `msitools` (`msiextract`) — unpacks the `.msi` payloads MSVC/WinSDK ship as.
|
||||||
|
- `git` — used to apply the small compatibility patches bundled with
|
||||||
|
`download` (see Compatibility patches below).
|
||||||
|
|
||||||
|
On ALT Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pkcon install wine msitools
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
`download` supports `--msvc-version`, `--sdk-version`, `--architecture`,
|
||||||
|
`--host-arch`, `--with-*` component toggles, `--ignore`, `--only-download`,
|
||||||
|
`--only-unpack`, `--keep-unpack`, `--cache`, `--language`,
|
||||||
|
`--include-optional`, `--skip-recommended`, `--major`, `--preview`,
|
||||||
|
`--manifest`. Run `msvc-go-wine download -h` for the full list.
|
||||||
|
|
||||||
|
### Using clang-cl/lld-link instead of Wine
|
||||||
|
|
||||||
|
You don't need Wine at all if you drive the (nonredistributable) MSVC/WinSDK
|
||||||
|
headers and libraries with Clang/LLD in MSVC-compatible mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
eval "$(msvc-go-wine env --bin ~/my_msvc/bin/x64)"
|
||||||
|
clang-cl -c hello.c
|
||||||
|
lld-link hello.obj -out:hello.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o msvc-go-wine ./cmd/msvc-go-wine
|
||||||
|
```
|
||||||
|
|
||||||
|
Go 1.23+ is all you need to build it; `wine`/`msitools` are only needed at
|
||||||
|
run time (`install`/tool invocation and `download` respectively).
|
||||||
|
|
||||||
|
## toolrelay.exe
|
||||||
|
|
||||||
|
`install` compiles `assets/vendor/toolrelay.cpp` (a small native Windows
|
||||||
|
launcher, original to this project) with the freshly-installed host-arch
|
||||||
|
`cl.exe` (best-effort: if `wine` isn't present yet, or the compile fails,
|
||||||
|
install still succeeds and the wrapper runtime just falls back to invoking
|
||||||
|
tools directly through wine). When present, every non-MSBuild tool
|
||||||
|
invocation is routed through it via two named FIFOs. This is what lets
|
||||||
|
`mt.exe`'s CMake-compatibility exit-code translation (`0x41020001` → `0xbb`)
|
||||||
|
survive Wine's own exit-code truncation: only a native Windows process
|
||||||
|
observing the untranslated code via `GetExitCodeProcess()` can catch it
|
||||||
|
before Wine marshals the process exit back to Unix and drops everything but
|
||||||
|
the low byte.
|
||||||
|
|
||||||
|
## Compatibility patches
|
||||||
|
|
||||||
|
`download` applies a handful of small patches (`assets/patches`) to the
|
||||||
|
downloaded MSVC/WinSDK tree - independently written for this project - that
|
||||||
|
make `VsDevCmd.bat` and MSBuild's SDK-detection props work without a
|
||||||
|
Windows Registry (which doesn't exist under Wine): they check the SDK
|
||||||
|
directly under the VS install root instead of querying the registry, skip
|
||||||
|
telemetry, and don't hard-fail devcmd setup when an optional component
|
||||||
|
(ConnectionManagerExe, bundled CMake/Ninja) wasn't downloaded.
|
||||||
|
|
||||||
|
## Known gaps
|
||||||
|
|
||||||
|
- `download` doesn't yet support printing the dependency/reverse-dependency
|
||||||
|
tree, listing available workloads/components/packages, or installing the
|
||||||
|
Windows Driver Kit via `--with-wdk-installers`; the core selection/
|
||||||
|
download/unpack/install pipeline is fully implemented.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT, see [LICENSE.txt](LICENSE.txt) - covers msvc-go-wine's own source only.
|
||||||
|
The MSVC Build Tools / Windows SDK that `download` fetches remain governed
|
||||||
|
by Microsoft's own license (accepted via `--accept-license`), same as with
|
||||||
|
any other way of obtaining them.
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
package download
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnpackSelectedPackages unpacks every selected package's cached payloads
|
||||||
|
// into unpack (VSIX packages as plain zips, Win10SDK/Win11SDK via the
|
||||||
|
// external msiextract).
|
||||||
|
func UnpackSelectedPackages(selected []*Package, cacheDir, unpack string) error {
|
||||||
|
if err := os.MkdirAll(unpack, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Join(unpack, "MSBuild"), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, p := range selected {
|
||||||
|
dir := filepath.Join(cacheDir, p.Key())
|
||||||
|
switch p.Type {
|
||||||
|
case "Component", "Workload", "Group":
|
||||||
|
continue
|
||||||
|
case "Vsix":
|
||||||
|
fmt.Println("Unpacking", p.ID)
|
||||||
|
for _, pl := range p.Payloads {
|
||||||
|
listing := filepath.Join(unpack, p.Key()+"-listing.txt")
|
||||||
|
if err := extractVSIXPackage(filepath.Join(dir, pl.Name()), unpack, listing); err != nil {
|
||||||
|
return fmt.Errorf("unpacking %s: %w", p.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if strings.HasPrefix(p.ID, "Win10SDK") || strings.HasPrefix(p.ID, "Win11SDK") {
|
||||||
|
fmt.Println("Unpacking", p.ID)
|
||||||
|
if err := extractWindowsSDKPackage(dir, p.Payloads, unpack); err != nil {
|
||||||
|
return fmt.Errorf("unpacking %s: %w", p.ID, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Skipping unpacking of", p.ID, "of type", p.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractVSIXPackage extracts a VSIX (plain zip) into a scratch dir under
|
||||||
|
// dest, then merges its "Contents" (and WDK's "$MSBuild") subtree into
|
||||||
|
// dest.
|
||||||
|
func extractVSIXPackage(file, dest, listingPath string) error {
|
||||||
|
r, err := zip.OpenReader(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
tmp := filepath.Join(dest, "vsix")
|
||||||
|
if err := os.RemoveAll(tmp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var names []string
|
||||||
|
for _, f := range r.File {
|
||||||
|
names = append(names, f.Name)
|
||||||
|
name, err := url.PathUnescape(f.Name)
|
||||||
|
if err != nil {
|
||||||
|
name = f.Name
|
||||||
|
}
|
||||||
|
target := filepath.Join(tmp, name)
|
||||||
|
if f.FileInfo().IsDir() {
|
||||||
|
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := extractZipEntry(f, target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(listingPath, []byte(strings.Join(names, "\n")+"\n"), 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if contents := filepath.Join(tmp, "Contents"); isDir(contents) {
|
||||||
|
if err := combineDirTrees(contents, dest); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This archive directory structure is used by the WDK.vsix.
|
||||||
|
if msbuild := filepath.Join(tmp, "$MSBuild"); isDir(msbuild) {
|
||||||
|
if err := combineDirTrees(msbuild, filepath.Join(dest, "MSBuild")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return os.RemoveAll(tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractZipEntry(f *zip.File, dest string) error {
|
||||||
|
rc, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
mode := f.Mode()
|
||||||
|
if mode == 0 {
|
||||||
|
mode = 0o644
|
||||||
|
}
|
||||||
|
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
_, err = io.Copy(out, rc)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractWindowsSDKPackage extracts every .msi payload of a WinSDK package
|
||||||
|
// via msiextract, and symlinks "Program Files" to "." so files msiextract
|
||||||
|
// unpacks there land at the unpack root (matching msiexec's own behavior on
|
||||||
|
// Windows).
|
||||||
|
func extractWindowsSDKPackage(src string, payloads []Payload, dest string) error {
|
||||||
|
pf := filepath.Join(dest, "Program Files")
|
||||||
|
if !exists(pf) {
|
||||||
|
if err := os.Symlink(".", pf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, pl := range payloads {
|
||||||
|
name := pl.Name()
|
||||||
|
if !strings.HasSuffix(strings.ToLower(name), ".msi") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Println("Extracting", name)
|
||||||
|
srcFile := filepath.Join(src, name)
|
||||||
|
logPath := filepath.Join(dest, "WinSDK-"+name+"-listing.txt")
|
||||||
|
if err := runMsiExtract(srcFile, dest, logPath); err != nil {
|
||||||
|
return fmt.Errorf("msiextract %s: %w", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMsiExtract(srcFile, dest, logPath string) error {
|
||||||
|
if _, err := exec.LookPath("msiextract"); err != nil {
|
||||||
|
return fmt.Errorf("msiextract not found in PATH (install the msitools package): %w", err)
|
||||||
|
}
|
||||||
|
logFile, err := os.Create(logPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer logFile.Close()
|
||||||
|
cmd := exec.Command("msiextract", "-C", dest, srcFile)
|
||||||
|
cmd.Stdout = logFile
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RelocateBuildTools relocates the components CLI tools actually need (VC,
|
||||||
|
// Windows Kits, DIA SDK, MSBuild, Common7/Tools) from the scratch unpack
|
||||||
|
// dir into dest, so the rest of unpack can be discarded.
|
||||||
|
func RelocateBuildTools(unpack, dest string) error {
|
||||||
|
components := []string{"VC", "Windows Kits", "DIA SDK", "MSBuild", filepath.Join("Common7", "Tools")}
|
||||||
|
for _, c := range components {
|
||||||
|
if err := combineDirTrees(filepath.Join(unpack, c), filepath.Join(dest, c)); err != nil {
|
||||||
|
return fmt.Errorf("moving %s: %w", c, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// combineDirTrees moves src into dest, recursively merging where a
|
||||||
|
// same-named directory already exists in dest (matching case-insensitively)
|
||||||
|
// rather than clobbering it - MSVC/WinSDK packages aren't casing-consistent
|
||||||
|
// about where they put things.
|
||||||
|
func combineDirTrees(src, dest string) error {
|
||||||
|
if !isDir(src) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !isDir(dest) {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(src, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
destEntries, err := os.ReadDir(dest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
destNames := map[string]string{}
|
||||||
|
for _, e := range destEntries {
|
||||||
|
destNames[strings.ToLower(e.Name())] = e.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
n := e.Name()
|
||||||
|
srcName := filepath.Join(src, n)
|
||||||
|
destName := filepath.Join(dest, n)
|
||||||
|
if e.IsDir() {
|
||||||
|
if isDir(destName) {
|
||||||
|
if err := combineDirTrees(srcName, destName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if actual, ok := destNames[strings.ToLower(n)]; ok {
|
||||||
|
if err := combineDirTrees(srcName, filepath.Join(dest, actual)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := os.Rename(srcName, destName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := os.Rename(srcName, destName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyRedirectedAssemblies works around Wine not honoring <dependentAssembly>
|
||||||
|
// codeBase redirects in an .exe.config file, by copying the referenced DLL
|
||||||
|
// next to the executable directly.
|
||||||
|
func CopyRedirectedAssemblies(app string) error {
|
||||||
|
cfgPath := app + ".config"
|
||||||
|
if !isFile(cfgPath) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg struct {
|
||||||
|
Runtime struct {
|
||||||
|
AssemblyBinding struct {
|
||||||
|
DependentAssembly []struct {
|
||||||
|
CodeBase struct {
|
||||||
|
Href string `xml:"href,attr"`
|
||||||
|
} `xml:"codeBase"`
|
||||||
|
} `xml:"dependentAssembly"`
|
||||||
|
} `xml:"assemblyBinding"`
|
||||||
|
} `xml:"runtime"`
|
||||||
|
}
|
||||||
|
if err := xml.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return fmt.Errorf("parsing %s: %w", cfgPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := filepath.Dir(app)
|
||||||
|
for _, da := range cfg.Runtime.AssemblyBinding.DependentAssembly {
|
||||||
|
href := strings.ReplaceAll(da.CodeBase.Href, "\\", "/")
|
||||||
|
if href == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
src := filepath.Join(dest, href)
|
||||||
|
if isFile(src) {
|
||||||
|
if err := copyFile(src, filepath.Join(dest, filepath.Base(src)), 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDir(path string) bool {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
return err == nil && fi.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFile(path string) bool {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
return err == nil && !fi.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func exists(path string) bool {
|
||||||
|
_, err := os.Lstat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(src, dst string, mode os.FileMode) error {
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
_ = os.Remove(dst)
|
||||||
|
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
_, err = io.Copy(out, in)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
package download
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxConcurrentDownloads = 5
|
||||||
|
const maxDownloadAttempts = 5
|
||||||
|
|
||||||
|
// FetchPayloads fetches every payload of every selected package into
|
||||||
|
// cacheDir/<packageKey>/<payloadName>, verifying sha256 and skipping files
|
||||||
|
// already present and correct. allowHashMismatch (used for --only-download)
|
||||||
|
// warns instead of failing on a hash mismatch.
|
||||||
|
func FetchPayloads(selected []*Package, cacheDir string, allowHashMismatch bool) error {
|
||||||
|
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type task struct {
|
||||||
|
payload Payload
|
||||||
|
dest string
|
||||||
|
fileID string
|
||||||
|
}
|
||||||
|
var tasks []task
|
||||||
|
for _, p := range selected {
|
||||||
|
if len(p.Payloads) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dir := filepath.Join(cacheDir, p.Key())
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, pl := range p.Payloads {
|
||||||
|
name := pl.Name()
|
||||||
|
tasks = append(tasks, task{
|
||||||
|
payload: pl,
|
||||||
|
dest: filepath.Join(dir, name),
|
||||||
|
fileID: filepath.Join(p.Key(), name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sem := make(chan struct{}, maxConcurrentDownloads)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var totalDownloaded int64
|
||||||
|
errCh := make(chan error, len(tasks))
|
||||||
|
|
||||||
|
for _, t := range tasks {
|
||||||
|
t := t
|
||||||
|
wg.Add(1)
|
||||||
|
sem <- struct{}{}
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() { <-sem }()
|
||||||
|
n, err := fetchOnePayloadWithRetries(t.payload, t.dest, t.fileID, allowHashMismatch)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
atomic.AddInt64(&totalDownloaded, n)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
close(errCh)
|
||||||
|
for err := range errCh {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Downloaded %s in total\n", HumanizeBytes(totalDownloaded))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchOnePayloadWithRetries(payload Payload, dest, fileID string, allowHashMismatch bool) (int64, error) {
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 0; attempt < maxDownloadAttempts; attempt++ {
|
||||||
|
n, err := tryDownloadPayload(payload, dest, fileID, allowHashMismatch)
|
||||||
|
if err == nil {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
fmt.Printf("%v\n", err)
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("giving up on %s after %d attempts: %w", fileID, maxDownloadAttempts, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryDownloadPayload(payload Payload, dest, fileID string, allowHashMismatch bool) (int64, error) {
|
||||||
|
if fi, err := os.Stat(dest); err == nil && fi.Mode().IsRegular() {
|
||||||
|
if payload.SHA256 != "" {
|
||||||
|
sum, err := sha256File(dest)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if !equalFoldHex(sum, payload.SHA256) {
|
||||||
|
fmt.Printf("Incorrect existing file %s, removing\n", fileID)
|
||||||
|
os.Remove(dest)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Using existing file %s\n", fileID)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Downloading %s (%s)\n", fileID, HumanizeBytes(payload.Size))
|
||||||
|
if err := httpDownloadFile(payload.URL, dest); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if payload.SHA256 != "" {
|
||||||
|
sum, err := sha256File(dest)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if !equalFoldHex(sum, payload.SHA256) {
|
||||||
|
if allowHashMismatch {
|
||||||
|
fmt.Printf("WARNING: incorrect hash for downloaded file %s\n", fileID)
|
||||||
|
} else {
|
||||||
|
return 0, fmt.Errorf("incorrect hash for downloaded file %s, aborting", fileID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return payload.Size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadHTTPClient = &http.Client{Timeout: 30 * time.Minute}
|
||||||
|
|
||||||
|
func httpDownloadFile(url, dest string) error {
|
||||||
|
resp, err := downloadHTTPClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("GET %s: %s", url, resp.Status)
|
||||||
|
}
|
||||||
|
tmp := dest + ".part"
|
||||||
|
out, err := os.Create(tmp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(out, resp.Body); err != nil {
|
||||||
|
out.Close()
|
||||||
|
os.Remove(tmp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := out.Close(); err != nil {
|
||||||
|
os.Remove(tmp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(tmp, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha256File(path string) (string, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
h := sha256.New()
|
||||||
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(h.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalFoldHex(a, b string) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
ca, cb := a[i], b[i]
|
||||||
|
if ca >= 'A' && ca <= 'Z' {
|
||||||
|
ca += 'a' - 'A'
|
||||||
|
}
|
||||||
|
if cb >= 'A' && cb <= 'Z' {
|
||||||
|
cb += 'a' - 'A'
|
||||||
|
}
|
||||||
|
if ca != cb {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package download
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Index maps a lowercased package id to every variant sharing that id,
|
||||||
|
// sorted by arch/language priority (best match first).
|
||||||
|
type Index map[string][]*Package
|
||||||
|
|
||||||
|
// BuildIndex indexes every package in m, prioritizing variants matching
|
||||||
|
// hostArch and language ("" language means no language preference beyond
|
||||||
|
// the default "en").
|
||||||
|
func BuildIndex(m *Manifest, hostArch, language string) Index {
|
||||||
|
if language == "" {
|
||||||
|
language = "en"
|
||||||
|
}
|
||||||
|
idx := Index{}
|
||||||
|
for i := range m.Packages {
|
||||||
|
p := &m.Packages[i]
|
||||||
|
key := strings.ToLower(p.ID)
|
||||||
|
idx[key] = append(idx[key], p)
|
||||||
|
}
|
||||||
|
for key, list := range idx {
|
||||||
|
list := list
|
||||||
|
sort.SliceStable(list, func(i, j int) bool {
|
||||||
|
return comparePackages(hostArch, language, list[i], list[j]) < 0
|
||||||
|
})
|
||||||
|
idx[key] = list
|
||||||
|
}
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// comparePackages ranks arch match first, then language match.
|
||||||
|
func comparePackages(arch, language string, a, b *Package) int {
|
||||||
|
archOrd := func(field string, x *Package) int {
|
||||||
|
if arch == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var v string
|
||||||
|
switch field {
|
||||||
|
case "chip":
|
||||||
|
v = x.Chip
|
||||||
|
case "machineArch":
|
||||||
|
v = x.MachineArch
|
||||||
|
case "productArch":
|
||||||
|
v = x.ProductArch
|
||||||
|
}
|
||||||
|
v = strings.ToLower(v)
|
||||||
|
if v == "" || v == "neutral" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if v == arch {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
for _, field := range []string{"chip", "machineArch", "productArch"} {
|
||||||
|
if r := archOrd(field, a) - archOrd(field, b); r != 0 {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lang := strings.ToLower(language)
|
||||||
|
countryLang := strings.Contains(lang, "-")
|
||||||
|
langPriority := func(x *Package) int {
|
||||||
|
xl := strings.ToLower(x.Language)
|
||||||
|
if xl == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (countryLang && xl == lang) || (!countryLang && strings.HasPrefix(xl, lang+"-")) {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(xl, "en-") {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
ap, bp := langPriority(a), langPriority(b)
|
||||||
|
if ap > bp {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if ap < bp {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find looks up id (case-insensitive), preferring a candidate matching
|
||||||
|
// constraints' chip/machineArch, falling back to the highest-priority
|
||||||
|
// variant. Returns nil if id isn't in the index at all.
|
||||||
|
func (idx Index) Find(id string, constraints map[string]string) *Package {
|
||||||
|
candidates, ok := idx[strings.ToLower(id)]
|
||||||
|
if !ok || len(candidates) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, p := range candidates {
|
||||||
|
matched := true
|
||||||
|
for _, k := range []string{"chip", "machineArch"} {
|
||||||
|
want, has := constraints[k]
|
||||||
|
if !has {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var got string
|
||||||
|
if k == "chip" {
|
||||||
|
got = p.Chip
|
||||||
|
} else {
|
||||||
|
got = p.MachineArch
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(got, want) {
|
||||||
|
matched = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matched {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return candidates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var knownHostArchs = []string{"x86", "x64", "arm64"}
|
||||||
|
|
||||||
|
// HostArchCompatible reports whether p can run on host: some packages
|
||||||
|
// encode their host arch in the id itself (e.g.
|
||||||
|
// Microsoft.VisualCpp.Tools.HostARM64.*).
|
||||||
|
func HostArchCompatible(p *Package, host string) bool {
|
||||||
|
if host == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
id := strings.ToLower(p.ID)
|
||||||
|
for _, a := range knownHostArchs {
|
||||||
|
if strings.Contains(id, "host"+a) {
|
||||||
|
return a == host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, v := range []string{p.Chip, p.MachineArch, p.ProductArch} {
|
||||||
|
v = strings.ToLower(v)
|
||||||
|
if v == "" || v == "neutral" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v != host {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var reTargetArch = regexp.MustCompile(`\.target(x86|x64|arm64|arm)(\W|$)`)
|
||||||
|
|
||||||
|
// TargetArchCompatible reports whether p is wanted for archs: packages
|
||||||
|
// naming a target arch in their id (e.g. ...HostX64.TargetX64) are only
|
||||||
|
// wanted when that arch was requested.
|
||||||
|
func TargetArchCompatible(p *Package, archs []string) bool {
|
||||||
|
if archs == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
m := reTargetArch.FindStringSubmatch(strings.ToLower(p.ID))
|
||||||
|
if m == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, a := range archs {
|
||||||
|
if a == m[1] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
// Package download fetches and unpacks MSVC/WinSDK using the same installer
|
||||||
|
// manifests Visual Studio's own installer uses.
|
||||||
|
package download
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Payload is one downloadable file belonging to a Package.
|
||||||
|
type Payload struct {
|
||||||
|
FileName string `json:"fileName"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
SHA256 string `json:"sha256"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the payload's bare file name, stripping any directory
|
||||||
|
// components the manifest's fileName might carry.
|
||||||
|
func (p Payload) Name() string {
|
||||||
|
name := p.FileName
|
||||||
|
if i := strings.LastIndexByte(name, '\\'); i >= 0 {
|
||||||
|
name = name[i+1:]
|
||||||
|
}
|
||||||
|
if i := strings.LastIndexByte(name, '/'); i >= 0 {
|
||||||
|
name = name[i+1:]
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependency is a normalized package dependency: manifests encode these
|
||||||
|
// either as a bare version string or as an object with version/type/id.
|
||||||
|
type Dependency struct {
|
||||||
|
TargetID string // the id to depend on (overrides the map key if set)
|
||||||
|
Version string
|
||||||
|
Type string // "", "Optional" or "Recommended"
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalizedResource carries the license URL shown before accepting a
|
||||||
|
// package's terms.
|
||||||
|
type LocalizedResource struct {
|
||||||
|
Language string `json:"language"`
|
||||||
|
License string `json:"license"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package is one entry from the installer manifest's "packages" array.
|
||||||
|
type Package struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Chip string `json:"chip"`
|
||||||
|
MachineArch string `json:"machineArch"`
|
||||||
|
ProductArch string `json:"productArch"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
Payloads []Payload `json:"payloads"`
|
||||||
|
InstallSizes map[string]int64 `json:"installSizes"`
|
||||||
|
LocalizedResources []LocalizedResource `json:"localizedResources"`
|
||||||
|
|
||||||
|
DependenciesRaw map[string]json.RawMessage `json:"dependencies"`
|
||||||
|
dependencies map[string]Dependency
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependencies lazily normalizes DependenciesRaw into Dependency values.
|
||||||
|
func (p *Package) Dependencies() map[string]Dependency {
|
||||||
|
if p.dependencies != nil {
|
||||||
|
return p.dependencies
|
||||||
|
}
|
||||||
|
p.dependencies = map[string]Dependency{}
|
||||||
|
for key, raw := range p.DependenciesRaw {
|
||||||
|
var version string
|
||||||
|
if err := json.Unmarshal(raw, &version); err == nil {
|
||||||
|
p.dependencies[key] = Dependency{Version: version}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var d struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &d); err == nil {
|
||||||
|
p.dependencies[key] = Dependency{TargetID: d.ID, Version: d.Version, Type: d.Type}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p.dependencies
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key uniquely identifies a specific package variant (id + version + arch),
|
||||||
|
// used to dedupe an already-included package during dependency resolution
|
||||||
|
// and to name its cache directory. Mirrors getPackageKey.
|
||||||
|
func (p *Package) Key() string {
|
||||||
|
key := p.ID
|
||||||
|
if p.Version != "" {
|
||||||
|
key += "-" + p.Version
|
||||||
|
}
|
||||||
|
if p.Chip != "" {
|
||||||
|
key += "-chip." + p.Chip
|
||||||
|
}
|
||||||
|
if p.MachineArch != "" {
|
||||||
|
key += "-machineArch." + p.MachineArch
|
||||||
|
}
|
||||||
|
if p.ProductArch != "" {
|
||||||
|
key += "-productArch." + p.ProductArch
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Package) InstalledSize() int64 {
|
||||||
|
var sum int64
|
||||||
|
for _, v := range p.InstallSizes {
|
||||||
|
sum += v
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Package) DownloadSize() int64 {
|
||||||
|
var sum int64
|
||||||
|
for _, pl := range p.Payloads {
|
||||||
|
sum += pl.Size
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelItem is one entry of a channel manifest's "channelItems", used only
|
||||||
|
// to locate the installer manifest URL.
|
||||||
|
type ChannelItem struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Payloads []Payload `json:"payloads"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manifest is the top-level installer manifest (or channel manifest, which
|
||||||
|
// shares the "info" field used for logging).
|
||||||
|
type Manifest struct {
|
||||||
|
Info struct {
|
||||||
|
ProductDisplayVersion string `json:"productDisplayVersion"`
|
||||||
|
} `json:"info"`
|
||||||
|
Packages []Package `json:"packages"`
|
||||||
|
ChannelItems []ChannelItem `json:"channelItems"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manifests (particularly the installer manifest, a single JSON file
|
||||||
|
// listing every package) can be tens of MB, so give this a generous timeout
|
||||||
|
// and a few retries - transient network hiccups shouldn't need a full
|
||||||
|
// restart of `download`.
|
||||||
|
var httpClient = &http.Client{Timeout: 5 * time.Minute}
|
||||||
|
|
||||||
|
const maxManifestAttempts = 5
|
||||||
|
|
||||||
|
func httpGet(url string) ([]byte, error) {
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 0; attempt < maxManifestAttempts; attempt++ {
|
||||||
|
data, err := tryHTTPGet(url)
|
||||||
|
if err == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
fmt.Printf("GET %s: %v (retrying)\n", url, err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("GET %s: giving up after %d attempts: %w", url, maxManifestAttempts, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryHTTPGet(url string) ([]byte, error) {
|
||||||
|
resp, err := httpClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("%s", resp.Status)
|
||||||
|
}
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchChannelManifest downloads the top-level channel manifest for the
|
||||||
|
// given major VS version ("18" and up use the "stable"/"insiders" channel
|
||||||
|
// naming, earlier ones use "release"/"pre"), and returns the URL of the
|
||||||
|
// installer manifest it references.
|
||||||
|
func FetchChannelManifest(major int, preview bool) (string, error) {
|
||||||
|
kind := "stable"
|
||||||
|
if major < 18 {
|
||||||
|
kind = "release"
|
||||||
|
}
|
||||||
|
if preview {
|
||||||
|
kind = "insiders"
|
||||||
|
if major < 18 {
|
||||||
|
kind = "pre"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("https://aka.ms/vs/%d/%s/channel", major, kind)
|
||||||
|
fmt.Println("Fetching", url)
|
||||||
|
data, err := httpGet(url)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var m Manifest
|
||||||
|
if err := json.Unmarshal(data, &m); err != nil {
|
||||||
|
return "", fmt.Errorf("parsing channel manifest: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Got toplevel manifest for %s\n", m.Info.ProductDisplayVersion)
|
||||||
|
for _, item := range m.ChannelItems {
|
||||||
|
if item.Type == "Manifest" && len(item.Payloads) > 0 {
|
||||||
|
return item.Payloads[0].URL, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("unable to find an installer manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchInstallerManifest downloads and parses the installer manifest at url.
|
||||||
|
func FetchInstallerManifest(url string) (*Manifest, error) {
|
||||||
|
data, err := httpGet(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var m Manifest
|
||||||
|
if err := json.Unmarshal(data, &m); err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing installer manifest: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Loaded installer manifest for %s\n", m.Info.ProductDisplayVersion)
|
||||||
|
return &m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HumanizeBytes renders a byte count as a friendly "1.2 GB" style string.
|
||||||
|
func HumanizeBytes(s int64) string {
|
||||||
|
switch {
|
||||||
|
case s > 900*1024*1024:
|
||||||
|
return fmt.Sprintf("%.1f GB", float64(s)/(1024*1024*1024))
|
||||||
|
case s > 900*1024:
|
||||||
|
return fmt.Sprintf("%.1f MB", float64(s)/(1024*1024))
|
||||||
|
case s > 1024:
|
||||||
|
return fmt.Sprintf("%.1f KB", float64(s)/1024)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d bytes", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package download
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Cheviiot/msvc-go-wine/assets"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApplyCompatibilityFixes applies the embedded Wine compatibility patches
|
||||||
|
// (assets/patches) against a freshly unpacked+moved dest tree: a
|
||||||
|
// "foo.props.patch" is git-applied over dest/foo.props (skipped if already
|
||||||
|
// applied), a "foo.patch"-less file is copied verbatim, and a "foo.remove"
|
||||||
|
// marker deletes dest/foo.
|
||||||
|
func ApplyCompatibilityFixes(dest string) error {
|
||||||
|
return fs.WalkDir(assets.Patches, "patches", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rel := strings.TrimPrefix(path, "patches/")
|
||||||
|
ext := filepath.Ext(rel)
|
||||||
|
target := strings.TrimSuffix(rel, ext)
|
||||||
|
|
||||||
|
switch ext {
|
||||||
|
case ".patch":
|
||||||
|
return applyGitPatch(dest, path, target)
|
||||||
|
case ".remove":
|
||||||
|
full := filepath.Join(dest, target)
|
||||||
|
if isFile(full) {
|
||||||
|
fmt.Println("Removing", target)
|
||||||
|
return os.Remove(full)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
fmt.Println("Copying", rel)
|
||||||
|
data, err := assets.Patches.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
full := filepath.Join(dest, rel)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(full, data, 0o644)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyGitPatch(dest, embeddedPath, target string) error {
|
||||||
|
full := filepath.Join(dest, target)
|
||||||
|
if !isFile(full) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := assets.Patches.ReadFile(embeddedPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := os.CreateTemp("", "msvc-go-wine-*.patch")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(tmp.Name())
|
||||||
|
if _, err := tmp.Write(data); err != nil {
|
||||||
|
tmp.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmp.Close()
|
||||||
|
|
||||||
|
// Skip if the patch has already been applied (reverse-apply check),
|
||||||
|
// so re-running download/install stays idempotent.
|
||||||
|
check := exec.Command("git", "--work-tree=.", "apply", "--quiet", "--reverse", "--check", tmp.Name())
|
||||||
|
check.Dir = dest
|
||||||
|
if check.Run() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Patching", target)
|
||||||
|
apply := exec.Command("git", "--work-tree=.", "apply", tmp.Name())
|
||||||
|
apply.Dir = dest
|
||||||
|
apply.Stderr = os.Stderr
|
||||||
|
return apply.Run()
|
||||||
|
}
|
||||||
@@ -0,0 +1,369 @@
|
|||||||
|
package download
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var reSDKVersion = regexp.MustCompile(`^\d+\.\d+\.\d+`)
|
||||||
|
|
||||||
|
// TriState represents a `--with-*` style flag: nil means "unset, use the
|
||||||
|
// applicable default"; a set value means the user (or a higher-level
|
||||||
|
// default) explicitly chose to include/exclude the component.
|
||||||
|
type TriState = *bool
|
||||||
|
|
||||||
|
func on() TriState { v := true; return &v }
|
||||||
|
func off() TriState { v := false; return &v }
|
||||||
|
|
||||||
|
// Options holds every flag that feeds package selection and download.
|
||||||
|
type Options struct {
|
||||||
|
Package []string
|
||||||
|
Ignore []string
|
||||||
|
Architecture []string // subset of x86,x64,arm,arm64,host
|
||||||
|
HostArch string // x86, x64 or arm64
|
||||||
|
OnlyHost bool
|
||||||
|
|
||||||
|
MSVCVersion string // "", "preview", "16.0".."18.0", "15.4".."15.9"
|
||||||
|
SDKVersion string
|
||||||
|
|
||||||
|
WithDefault TriState
|
||||||
|
WithWorkload TriState
|
||||||
|
WithMSVC TriState
|
||||||
|
WithASAN TriState
|
||||||
|
WithSDK TriState
|
||||||
|
WithATL TriState
|
||||||
|
WithDIA TriState
|
||||||
|
WithMSBuild TriState
|
||||||
|
WithDevCmd TriState
|
||||||
|
|
||||||
|
IncludeOptional bool
|
||||||
|
SkipRecommended bool
|
||||||
|
Language string
|
||||||
|
|
||||||
|
WithWDKInstallers string
|
||||||
|
}
|
||||||
|
|
||||||
|
func addIfWanted(opts *Options, flag TriState, pkg string) {
|
||||||
|
if flag == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if *flag {
|
||||||
|
opts.Package = append(opts.Package, pkg)
|
||||||
|
} else {
|
||||||
|
opts.Ignore = append(opts.Ignore, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type msvcVersionEntry struct {
|
||||||
|
gen string // "15" or "16" - which package-name scheme applies
|
||||||
|
sdk string
|
||||||
|
toolVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
// msvcVersionTable maps a --msvc-version value to the SDK it pulls in by
|
||||||
|
// default and the toolset version fragment used in package ids.
|
||||||
|
var msvcVersionTable = map[string]msvcVersionEntry{
|
||||||
|
"preview": {"16", "", "Preview"},
|
||||||
|
|
||||||
|
"16.0": {"16", "10.0.17763", "14.20"},
|
||||||
|
"16.1": {"16", "10.0.18362", "14.21"},
|
||||||
|
"16.2": {"16", "10.0.18362", "14.22"},
|
||||||
|
"16.3": {"16", "10.0.18362", "14.23"},
|
||||||
|
"16.4": {"16", "10.0.18362", "14.24"},
|
||||||
|
"16.5": {"16", "10.0.18362", "14.25"},
|
||||||
|
"16.6": {"16", "10.0.18362", "14.26"},
|
||||||
|
"16.7": {"16", "10.0.18362", "14.27"},
|
||||||
|
"16.8": {"16", "10.0.18362", "14.28"},
|
||||||
|
"16.9": {"16", "10.0.19041", "14.28.16.9"},
|
||||||
|
"16.10": {"16", "10.0.19041", "14.29.16.10"},
|
||||||
|
"16.11": {"16", "10.0.19041", "14.29.16.11"},
|
||||||
|
"17.0": {"16", "10.0.19041", "14.30.17.0"},
|
||||||
|
"17.1": {"16", "10.0.19041", "14.31.17.1"},
|
||||||
|
"17.2": {"16", "10.0.19041", "14.32.17.2"},
|
||||||
|
"17.3": {"16", "10.0.19041", "14.33.17.3"},
|
||||||
|
"17.4": {"16", "10.0.22621", "14.34.17.4"},
|
||||||
|
"17.5": {"16", "10.0.22621", "14.35.17.5"},
|
||||||
|
"17.6": {"16", "10.0.22621", "14.36.17.6"},
|
||||||
|
"17.7": {"16", "10.0.22621", "14.37.17.7"},
|
||||||
|
"17.8": {"16", "10.0.22621", "14.38.17.8"},
|
||||||
|
"17.9": {"16", "10.0.22621", "14.39.17.9"},
|
||||||
|
"17.10": {"16", "10.0.22621", "14.40.17.10"},
|
||||||
|
"17.11": {"16", "10.0.22621", "14.41.17.11"},
|
||||||
|
"17.12": {"16", "10.0.22621", "14.42.17.12"},
|
||||||
|
"17.13": {"16", "10.0.22621", "14.43.17.13"},
|
||||||
|
"17.14": {"16", "10.0.26100", "14.44.17.14"},
|
||||||
|
"18.0": {"16", "10.0.26100", "14.50.18.0"},
|
||||||
|
|
||||||
|
"15.4": {"15", "10.0.16299", "14.11"},
|
||||||
|
"15.5": {"15", "10.0.16299", "14.12"},
|
||||||
|
"15.6": {"15", "10.0.16299", "14.13"},
|
||||||
|
"15.7": {"15", "10.0.17134", "14.14"},
|
||||||
|
"15.8": {"15", "10.0.17134", "14.15"},
|
||||||
|
"15.9": {"15", "10.0.17763", "14.16"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectToolsetV16(opts *Options, idx Index, userVersion, sdk, toolVersion string, defaultPkgs, defaultIgnores []string) {
|
||||||
|
ext := ""
|
||||||
|
if toolVersion == "Preview" {
|
||||||
|
ext = ".Tools"
|
||||||
|
}
|
||||||
|
base := "Microsoft.VisualStudio.Component.VC." + toolVersion + ext
|
||||||
|
if idx.Find(base+".x86.x64", nil) != nil {
|
||||||
|
if contains(opts.Architecture, "x86") || contains(opts.Architecture, "x64") {
|
||||||
|
addIfWanted(opts, opts.WithMSVC, base+".x86.x64")
|
||||||
|
addIfWanted(opts, opts.WithASAN, "Microsoft.VC."+toolVersion+".ASAN.X86")
|
||||||
|
addIfWanted(opts, opts.WithATL, "Microsoft.VisualStudio.Component.VC."+toolVersion+".ATL")
|
||||||
|
}
|
||||||
|
if contains(opts.Architecture, "arm") {
|
||||||
|
addIfWanted(opts, opts.WithMSVC, "Microsoft.VisualStudio.Component.VC."+toolVersion+".ARM")
|
||||||
|
addIfWanted(opts, opts.WithATL, "Microsoft.VisualStudio.Component.VC."+toolVersion+".ATL.ARM")
|
||||||
|
}
|
||||||
|
if contains(opts.Architecture, "arm64") {
|
||||||
|
addIfWanted(opts, opts.WithMSVC, "Microsoft.VisualStudio.Component.VC."+toolVersion+".ARM64")
|
||||||
|
addIfWanted(opts, opts.WithATL, "Microsoft.VisualStudio.Component.VC."+toolVersion+".ATL.ARM64")
|
||||||
|
}
|
||||||
|
if opts.SDKVersion == "" {
|
||||||
|
opts.SDKVersion = sdk
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Didn't find exact version packages for %s, assuming this is provided by the default/latest version\n", userVersion)
|
||||||
|
opts.Package = append(opts.Package, defaultPkgs...)
|
||||||
|
opts.Ignore = append(opts.Ignore, defaultIgnores...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectToolsetV15(opts *Options, idx Index, userVersion, sdk, toolVersion string, defaultPkgs, defaultIgnores []string) {
|
||||||
|
id := "Microsoft.VisualStudio.Component.VC.Tools." + toolVersion
|
||||||
|
if idx.Find(id, nil) != nil {
|
||||||
|
addIfWanted(opts, opts.WithMSVC, id)
|
||||||
|
if opts.SDKVersion == "" {
|
||||||
|
opts.SDKVersion = sdk
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Didn't find exact version packages for %s, assuming this is provided by the default/latest version\n", userVersion)
|
||||||
|
opts.Package = append(opts.Package, defaultPkgs...)
|
||||||
|
opts.Ignore = append(opts.Ignore, defaultIgnores...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveSelection turns Options' high-level flags (--with-*,
|
||||||
|
// --msvc-version, --sdk-version, explicit packages) into the final
|
||||||
|
// opts.Package/opts.Ignore lists ready for ExpandSelection.
|
||||||
|
func ResolveSelection(opts *Options, idx Index) error {
|
||||||
|
if len(opts.Architecture) == 0 {
|
||||||
|
opts.Architecture = []string{"host", "x86", "x64", "arm", "arm64"}
|
||||||
|
}
|
||||||
|
if opts.HostArch != "" && contains(opts.Architecture, "host") {
|
||||||
|
opts.Architecture = append(opts.Architecture, opts.HostArch)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.MSVCVersion != "" {
|
||||||
|
if opts.WithMSVC == nil {
|
||||||
|
opts.WithMSVC = on()
|
||||||
|
}
|
||||||
|
if opts.WithASAN == nil {
|
||||||
|
opts.WithASAN = on()
|
||||||
|
}
|
||||||
|
if opts.WithATL == nil {
|
||||||
|
opts.WithATL = on()
|
||||||
|
}
|
||||||
|
if opts.WithSDK == nil {
|
||||||
|
opts.WithSDK = on()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.SDKVersion != "" && opts.WithSDK == nil {
|
||||||
|
opts.WithSDK = on()
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.WithDefault == nil && opts.MSVCVersion == "" && len(opts.Package) == 0 {
|
||||||
|
opts.WithDefault = on()
|
||||||
|
}
|
||||||
|
if opts.WithDefault != nil {
|
||||||
|
for _, pair := range []struct {
|
||||||
|
flag *TriState
|
||||||
|
}{
|
||||||
|
{&opts.WithWorkload}, {&opts.WithMSVC}, {&opts.WithASAN}, {&opts.WithSDK},
|
||||||
|
{&opts.WithATL}, {&opts.WithDIA}, {&opts.WithMSBuild}, {&opts.WithDevCmd},
|
||||||
|
} {
|
||||||
|
if *pair.flag == nil {
|
||||||
|
v := *opts.WithDefault
|
||||||
|
*pair.flag = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultPkgs, defaultIgnores := opts.Package, opts.Ignore
|
||||||
|
opts.Package, opts.Ignore = nil, nil
|
||||||
|
|
||||||
|
addIfWanted(opts, opts.WithWorkload, "Microsoft.VisualStudio.Workload.VCTools")
|
||||||
|
if contains(opts.Architecture, "x86") || contains(opts.Architecture, "x64") {
|
||||||
|
addIfWanted(opts, opts.WithMSVC, "Microsoft.VisualStudio.Component.VC.Tools.x86.x64")
|
||||||
|
addIfWanted(opts, opts.WithASAN, "Microsoft.VisualCpp.ASAN.X86")
|
||||||
|
addIfWanted(opts, opts.WithATL, "Microsoft.VisualStudio.Component.VC.ATL")
|
||||||
|
}
|
||||||
|
if contains(opts.Architecture, "arm") {
|
||||||
|
addIfWanted(opts, opts.WithMSVC, "Microsoft.VisualStudio.Component.VC.Tools.ARM")
|
||||||
|
addIfWanted(opts, opts.WithATL, "Microsoft.VisualStudio.Component.VC.ATL.ARM")
|
||||||
|
}
|
||||||
|
if contains(opts.Architecture, "arm64") {
|
||||||
|
addIfWanted(opts, opts.WithMSVC, "Microsoft.VisualStudio.Component.VC.Tools.ARM64")
|
||||||
|
addIfWanted(opts, opts.WithATL, "Microsoft.VisualStudio.Component.VC.ATL.ARM64")
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultPkgs, opts.Package = opts.Package, defaultPkgs
|
||||||
|
defaultIgnores, opts.Ignore = opts.Ignore, defaultIgnores
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case opts.MSVCVersion == "":
|
||||||
|
opts.Package = append(opts.Package, defaultPkgs...)
|
||||||
|
opts.Ignore = append(opts.Ignore, defaultIgnores...)
|
||||||
|
default:
|
||||||
|
entry, ok := msvcVersionTable[opts.MSVCVersion]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unsupported MSVC toolchain version %s", opts.MSVCVersion)
|
||||||
|
}
|
||||||
|
if entry.gen == "15" {
|
||||||
|
selectToolsetV15(opts, idx, opts.MSVCVersion, entry.sdk, entry.toolVersion, defaultPkgs, defaultIgnores)
|
||||||
|
} else {
|
||||||
|
selectToolsetV16(opts, idx, opts.MSVCVersion, entry.sdk, entry.toolVersion, defaultPkgs, defaultIgnores)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := selectSDK(opts, idx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
addIfWanted(opts, opts.WithDIA, "Microsoft.VisualCpp.DIA.SDK")
|
||||||
|
addIfWanted(opts, opts.WithMSBuild, "Microsoft.Build")
|
||||||
|
addIfWanted(opts, opts.WithMSBuild, "Microsoft.Build.Dependencies")
|
||||||
|
addIfWanted(opts, opts.WithDevCmd, "Microsoft.VisualStudio.VC.vcvars")
|
||||||
|
addIfWanted(opts, opts.WithDevCmd, "Microsoft.VisualStudio.PackageGroup.VsDevCmd")
|
||||||
|
|
||||||
|
if opts.WithWDKInstallers != "" {
|
||||||
|
opts.Package = append(opts.Package, "Component.Microsoft.Windows.DriverKit.BuildTools")
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeIgnoreCase(opts)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectSDK(opts *Options, idx Index) error {
|
||||||
|
switch {
|
||||||
|
case opts.WithSDK == nil:
|
||||||
|
return nil
|
||||||
|
case !*opts.WithSDK:
|
||||||
|
for key := range idx {
|
||||||
|
if strings.HasPrefix(key, "win10sdk") || strings.HasPrefix(key, "win11sdk") {
|
||||||
|
opts.Ignore = append(opts.Ignore, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case opts.SDKVersion == "":
|
||||||
|
saved := *opts
|
||||||
|
opts.Package = []string{"Microsoft.VisualStudio.Workload.VCTools"}
|
||||||
|
opts.IncludeOptional = false
|
||||||
|
opts.SkipRecommended = false
|
||||||
|
recommended, err := ExpandSelection(idx, opts)
|
||||||
|
*opts = saved
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, p := range recommended {
|
||||||
|
key := strings.ToLower(p.ID)
|
||||||
|
if strings.HasPrefix(key, "win10sdk") || strings.HasPrefix(key, "win11sdk") {
|
||||||
|
opts.Package = append(opts.Package, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
found := false
|
||||||
|
var versions []string
|
||||||
|
for key := range idx {
|
||||||
|
if !strings.HasPrefix(key, "win10sdk") && !strings.HasPrefix(key, "win11sdk") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
version := key[9:]
|
||||||
|
if reSDKVersion.MatchString(version) {
|
||||||
|
versions = append(versions, version)
|
||||||
|
}
|
||||||
|
if key == key[:8]+"_"+opts.SDKVersion {
|
||||||
|
found = true
|
||||||
|
opts.Package = append(opts.Package, key)
|
||||||
|
} else {
|
||||||
|
opts.Ignore = append(opts.Ignore, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("WinSDK version %s not found (available: %s)", opts.SDKVersion, strings.Join(versions, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeIgnoreCase(opts *Options) {
|
||||||
|
for i, s := range opts.Ignore {
|
||||||
|
opts.Ignore[i] = strings.ToLower(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(list []string, v string) bool {
|
||||||
|
for _, s := range list {
|
||||||
|
if s == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectDependencyClosure recursively resolves target's dependency tree,
|
||||||
|
// honoring arch matching, --ignore, and Optional/Recommended filtering.
|
||||||
|
func collectDependencyClosure(idx Index, included map[string]bool, target string, constraints map[string]string, opts *Options) []*Package {
|
||||||
|
if contains(opts.Ignore, strings.ToLower(target)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p := idx.Find(target, constraints)
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if opts.OnlyHost && !HostArchCompatible(p, opts.HostArch) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !TargetArchCompatible(p, opts.Architecture) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
key := p.Key()
|
||||||
|
if included[key] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
included[key] = true
|
||||||
|
|
||||||
|
ret := []*Package{p}
|
||||||
|
for target, dep := range p.Dependencies() {
|
||||||
|
id := target
|
||||||
|
if dep.TargetID != "" {
|
||||||
|
id = dep.TargetID
|
||||||
|
}
|
||||||
|
if dep.Type == "Optional" && !opts.IncludeOptional {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if dep.Type == "Recommended" && opts.SkipRecommended {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c := map[string]string{}
|
||||||
|
if dep.Version != "" {
|
||||||
|
c["version"] = dep.Version
|
||||||
|
}
|
||||||
|
ret = append(ret, collectDependencyClosure(idx, included, id, c, opts)...)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandSelection resolves opts.Package (and everything they transitively
|
||||||
|
// depend on) into a flat, deduped package list.
|
||||||
|
func ExpandSelection(idx Index, opts *Options) ([]*Package, error) {
|
||||||
|
included := map[string]bool{}
|
||||||
|
var ret []*Package
|
||||||
|
for _, id := range opts.Package {
|
||||||
|
ret = append(ret, collectDependencyClosure(idx, included, id, nil, opts)...)
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
package download
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fixtureManifest is a trimmed-down stand-in for a real installer manifest,
|
||||||
|
// covering: a workload -> component -> {required, Optional, Recommended}
|
||||||
|
// dependency chain, host-arch variants of the same package id, and
|
||||||
|
// language variants - enough to exercise BuildIndex/collectDependencyClosure
|
||||||
|
// offline, without ever hitting the network.
|
||||||
|
const fixtureManifestJSON = `{
|
||||||
|
"info": {"productDisplayVersion": "test"},
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"id": "Microsoft.VisualStudio.Workload.VCTools",
|
||||||
|
"type": "Workload",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64": "1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
|
||||||
|
"type": "Component",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.VC.Tools.Core": "1.0",
|
||||||
|
"Microsoft.VC.Tools.Optional": {"version": "1.0", "type": "Optional"},
|
||||||
|
"Microsoft.VC.Tools.Recommended": {"version": "1.0", "type": "Recommended"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Microsoft.VC.Tools.Core",
|
||||||
|
"type": "Msi",
|
||||||
|
"machineArch": "x86",
|
||||||
|
"payloads": [{"fileName": "core-x86.msi", "url": "https://example.invalid/core-x86.msi", "sha256": "aa", "size": 100}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Microsoft.VC.Tools.Core",
|
||||||
|
"type": "Msi",
|
||||||
|
"machineArch": "arm64",
|
||||||
|
"payloads": [{"fileName": "core-arm64.msi", "url": "https://example.invalid/core-arm64.msi", "sha256": "bb", "size": 200}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Microsoft.VC.Tools.Optional",
|
||||||
|
"type": "Msi",
|
||||||
|
"payloads": [{"fileName": "optional.msi", "url": "https://example.invalid/optional.msi", "size": 10}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Microsoft.VC.Tools.Recommended",
|
||||||
|
"type": "Msi",
|
||||||
|
"payloads": [{"fileName": "recommended.msi", "url": "https://example.invalid/recommended.msi", "size": 20}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Microsoft.VisualStudio.Resources",
|
||||||
|
"type": "Msi",
|
||||||
|
"language": "en-US",
|
||||||
|
"payloads": [{"fileName": "res-en.msi", "url": "https://example.invalid/res-en.msi", "size": 1}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Microsoft.VisualStudio.Resources",
|
||||||
|
"type": "Msi",
|
||||||
|
"language": "de-DE",
|
||||||
|
"payloads": [{"fileName": "res-de.msi", "url": "https://example.invalid/res-de.msi", "size": 1}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
func fixtureIndex(t *testing.T, hostArch string) Index {
|
||||||
|
t.Helper()
|
||||||
|
var m Manifest
|
||||||
|
if err := json.Unmarshal([]byte(fixtureManifestJSON), &m); err != nil {
|
||||||
|
t.Fatalf("parsing fixture manifest: %v", err)
|
||||||
|
}
|
||||||
|
return BuildIndex(&m, hostArch, "en")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildIndexPrioritizesHostArch(t *testing.T) {
|
||||||
|
idx := fixtureIndex(t, "x86")
|
||||||
|
p := idx.Find("Microsoft.VC.Tools.Core", nil)
|
||||||
|
if p == nil {
|
||||||
|
t.Fatal("expected to find Microsoft.VC.Tools.Core")
|
||||||
|
}
|
||||||
|
if p.MachineArch != "x86" {
|
||||||
|
t.Errorf("expected x86 variant to sort first for host x86, got machineArch=%q", p.MachineArch)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx64 := fixtureIndex(t, "arm64")
|
||||||
|
p64 := idx64.Find("Microsoft.VC.Tools.Core", nil)
|
||||||
|
if p64.MachineArch != "arm64" {
|
||||||
|
t.Errorf("expected arm64 variant to sort first for host arm64, got machineArch=%q", p64.MachineArch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildIndexPrioritizesLanguage(t *testing.T) {
|
||||||
|
idx := fixtureIndex(t, "x86")
|
||||||
|
p := idx.Find("Microsoft.VisualStudio.Resources", nil)
|
||||||
|
if p == nil {
|
||||||
|
t.Fatal("expected to find Microsoft.VisualStudio.Resources")
|
||||||
|
}
|
||||||
|
if p.Language != "en-US" {
|
||||||
|
t.Errorf("expected en-US variant to sort first for language=en, got %q", p.Language)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregateDependsDefaultExcludesOptionalOnly(t *testing.T) {
|
||||||
|
// Optional deps are skipped unless --include-optional is passed, but
|
||||||
|
// Recommended deps are pulled in unless --skip-recommended is passed -
|
||||||
|
// it's "recommended", not "extra".
|
||||||
|
idx := fixtureIndex(t, "x86")
|
||||||
|
opts := &Options{
|
||||||
|
Package: []string{"Microsoft.VisualStudio.Workload.VCTools"},
|
||||||
|
HostArch: "x86",
|
||||||
|
OnlyHost: true,
|
||||||
|
}
|
||||||
|
selected, err := ExpandSelection(idx, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ids := idsOf(selected)
|
||||||
|
|
||||||
|
mustContain(t, ids, "Microsoft.VisualStudio.Workload.VCTools")
|
||||||
|
mustContain(t, ids, "Microsoft.VisualStudio.Component.VC.Tools.x86.x64")
|
||||||
|
mustContain(t, ids, "Microsoft.VC.Tools.Core")
|
||||||
|
mustContain(t, ids, "Microsoft.VC.Tools.Recommended")
|
||||||
|
mustNotContain(t, ids, "Microsoft.VC.Tools.Optional")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregateDependsIncludeOptional(t *testing.T) {
|
||||||
|
idx := fixtureIndex(t, "x86")
|
||||||
|
opts := &Options{
|
||||||
|
Package: []string{"Microsoft.VisualStudio.Workload.VCTools"},
|
||||||
|
HostArch: "x86",
|
||||||
|
OnlyHost: true,
|
||||||
|
IncludeOptional: true,
|
||||||
|
}
|
||||||
|
selected, err := ExpandSelection(idx, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ids := idsOf(selected)
|
||||||
|
mustContain(t, ids, "Microsoft.VC.Tools.Optional")
|
||||||
|
// Recommended is included by default (only --skip-recommended excludes it).
|
||||||
|
mustContain(t, ids, "Microsoft.VC.Tools.Recommended")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregateDependsSkipRecommended(t *testing.T) {
|
||||||
|
idx := fixtureIndex(t, "x86")
|
||||||
|
opts := &Options{
|
||||||
|
Package: []string{"Microsoft.VisualStudio.Workload.VCTools"},
|
||||||
|
HostArch: "x86",
|
||||||
|
OnlyHost: true,
|
||||||
|
SkipRecommended: true,
|
||||||
|
}
|
||||||
|
selected, err := ExpandSelection(idx, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ids := idsOf(selected)
|
||||||
|
mustNotContain(t, ids, "Microsoft.VC.Tools.Recommended")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregateDependsRespectsIgnore(t *testing.T) {
|
||||||
|
idx := fixtureIndex(t, "x86")
|
||||||
|
opts := &Options{
|
||||||
|
Package: []string{"Microsoft.VisualStudio.Workload.VCTools"},
|
||||||
|
Ignore: []string{"microsoft.vc.tools.core"},
|
||||||
|
HostArch: "x86",
|
||||||
|
OnlyHost: true,
|
||||||
|
}
|
||||||
|
selected, err := ExpandSelection(idx, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ids := idsOf(selected)
|
||||||
|
mustNotContain(t, ids, "Microsoft.VC.Tools.Core")
|
||||||
|
mustContain(t, ids, "Microsoft.VisualStudio.Component.VC.Tools.x86.x64")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregateDependsHostArchMismatchExcludes(t *testing.T) {
|
||||||
|
idx := fixtureIndex(t, "x86")
|
||||||
|
opts := &Options{
|
||||||
|
Package: []string{"Microsoft.VC.Tools.Core"},
|
||||||
|
HostArch: "arm", // no arm variant exists, only x86/arm64
|
||||||
|
OnlyHost: true,
|
||||||
|
}
|
||||||
|
selected, err := ExpandSelection(idx, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(selected) != 0 {
|
||||||
|
t.Errorf("expected no packages selected for mismatched host arch, got %v", idsOf(selected))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func idsOf(pkgs []*Package) []string {
|
||||||
|
var ids []string
|
||||||
|
for _, p := range pkgs {
|
||||||
|
ids = append(ids, p.ID)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustContain(t *testing.T, ids []string, want string) {
|
||||||
|
t.Helper()
|
||||||
|
for _, id := range ids {
|
||||||
|
if id == want {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Errorf("expected %v to contain %q", ids, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustNotContain(t *testing.T, ids []string, unwanted string) {
|
||||||
|
t.Helper()
|
||||||
|
for _, id := range ids {
|
||||||
|
if id == unwanted {
|
||||||
|
t.Errorf("expected %v to NOT contain %q", ids, unwanted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package install
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FixIncludeOptions controls how FixInclude rewrites include directives.
|
||||||
|
type FixIncludeOptions struct {
|
||||||
|
// MapWinSDK restores the canonical "GL/" casing after lowercasing,
|
||||||
|
// since that's the cross-platform spelling for that header directory.
|
||||||
|
MapWinSDK bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// reIncludeLine matches `#include <foo/bar.h>` or `#include "foo.h"`, but
|
||||||
|
// not `#include IDENTIFIER` (macro-expanded includes).
|
||||||
|
var reIncludeLine = regexp.MustCompile(`^\s*#\s*include\s+["<][\w.\\/]+[">]`)
|
||||||
|
|
||||||
|
// FixInclude rewrites #include directives under root to reference the
|
||||||
|
// lowercase header names produced by Lowercase, since MSVC/WinSDK headers
|
||||||
|
// reference each other with casing that's internally inconsistent (but
|
||||||
|
// self-consistent once lowercased). Every text file under root is
|
||||||
|
// rewritten with normalized (LF) line endings as a side effect.
|
||||||
|
func FixInclude(root string, opts FixIncludeOptions) error {
|
||||||
|
var doDir func(dir string) error
|
||||||
|
doDir = func(dir string) error {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
path := filepath.Join(dir, e.Name())
|
||||||
|
if e.Type()&os.ModeSymlink != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if e.IsDir() {
|
||||||
|
if err := doDir(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := fixFile(path, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return doDir(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixFile(path string, opts FixIncludeOptions) error {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
if reIncludeLine.MatchString(line) {
|
||||||
|
code, comment := line, ""
|
||||||
|
if idx := strings.Index(line, "//"); idx >= 0 {
|
||||||
|
code, comment = line[:idx], line[idx:]
|
||||||
|
}
|
||||||
|
code = lowercaseAndSlash(code)
|
||||||
|
if opts.MapWinSDK {
|
||||||
|
code = strings.Replace(code, "gl/", "GL/", 1)
|
||||||
|
}
|
||||||
|
line = code + comment
|
||||||
|
}
|
||||||
|
lines[i] = strings.TrimRight(line, "\r\n")
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lowercaseAndSlash lowercases ASCII letters and turns backslashes into
|
||||||
|
// forward slashes, leaving everything else (including non-ASCII bytes)
|
||||||
|
// untouched.
|
||||||
|
func lowercaseAndSlash(s string) string {
|
||||||
|
b := []byte(s)
|
||||||
|
for i, c := range b {
|
||||||
|
switch {
|
||||||
|
case c >= 'A' && c <= 'Z':
|
||||||
|
b[i] = c - 'A' + 'a'
|
||||||
|
case c == '\\':
|
||||||
|
b[i] = '/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
@@ -0,0 +1,442 @@
|
|||||||
|
// Package install wires up a downloaded MSVC/WinSDK tree so the msvc-go-wine
|
||||||
|
// wrapper commands can find it: locating the installed toolchain/SDK
|
||||||
|
// versions, fixing up header/library name casing, laying out the
|
||||||
|
// per-architecture tool symlinks, and building the toolrelay helper.
|
||||||
|
package install
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Cheviiot/msvc-go-wine/assets"
|
||||||
|
"github.com/Cheviiot/msvc-go-wine/internal/wineenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var archs = []string{"x86", "x64", "arm", "arm64"}
|
||||||
|
|
||||||
|
// Install wires up dest (a directory previously populated by
|
||||||
|
// `msvc-go-wine download --dest dest`) with the tool wrapper symlinks and
|
||||||
|
// env.json config the wrapper runtime expects. selfBinary is the path to
|
||||||
|
// the currently running msvc-go-wine executable, copied into dest/bin so
|
||||||
|
// the arch-specific tool symlinks have something to point at.
|
||||||
|
func Install(dest, selfBinary string) error {
|
||||||
|
dest, err := filepath.Abs(dest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fi, err := os.Stat(dest); err != nil || !fi.IsDir() {
|
||||||
|
return fmt.Errorf("destination %q is not a directory", dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Targets are relative so the whole installed tree stays relocatable -
|
||||||
|
// moving or renaming dest doesn't break these symlinks the way an
|
||||||
|
// absolute target baked in at install time would.
|
||||||
|
if err := lnS("Windows Kits", filepath.Join(dest, "kits")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := lnS("VC", filepath.Join(dest, "vc")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := lnS("Tools", filepath.Join(dest, "vc", "tools")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := lnS("MSVC", filepath.Join(dest, "vc", "tools", "msvc")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msvcRoot := filepath.Join(dest, "vc", "tools", "msvc")
|
||||||
|
msvcVer, err := findMSVCVersion(msvcRoot)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("Using MSVC version", msvcVer)
|
||||||
|
msvcDir := filepath.Join(msvcRoot, msvcVer)
|
||||||
|
|
||||||
|
if err := fixLibCasing(filepath.Join(msvcDir, "lib")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
includeDir := filepath.Join(msvcDir, "include")
|
||||||
|
if err := Lowercase(includeDir, LowercaseOptions{Symlink: true}); err != nil {
|
||||||
|
return fmt.Errorf("lowercasing %s: %w", includeDir, err)
|
||||||
|
}
|
||||||
|
if err := FixInclude(includeDir, FixIncludeOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("fixing includes in %s: %w", includeDir, err)
|
||||||
|
}
|
||||||
|
atlIncludeDir := filepath.Join(msvcDir, "atlmfc", "include")
|
||||||
|
if isDir(atlIncludeDir) {
|
||||||
|
if err := FixInclude(atlIncludeDir, FixIncludeOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("fixing includes in %s: %w", atlIncludeDir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binDir := filepath.Join(msvcDir, "bin")
|
||||||
|
if err := removeVctip(binDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := renameHostDirs(binDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kits10 := filepath.Join(dest, "kits", "10")
|
||||||
|
if !isDir(kits10) {
|
||||||
|
return fmt.Errorf("%s not found - expected a Windows SDK already unpacked by `msvc-go-wine download`", kits10)
|
||||||
|
}
|
||||||
|
if err := lnS("Lib", filepath.Join(kits10, "lib")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := lnS("Include", filepath.Join(kits10, "include")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sdkVer, err := findSDKVersion(filepath.Join(kits10, "include"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("Using SDK version", sdkVer)
|
||||||
|
|
||||||
|
for _, sub := range []string{"um", "shared", "winrt", "km"} {
|
||||||
|
dir := filepath.Join(kits10, "include", sdkVer, sub)
|
||||||
|
if !isDir(dir) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := Lowercase(dir, LowercaseOptions{Symlink: true, MapWinSDK: true}); err != nil {
|
||||||
|
return fmt.Errorf("lowercasing %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
if err := FixInclude(dir, FixIncludeOptions{MapWinSDK: true}); err != nil {
|
||||||
|
return fmt.Errorf("fixing includes in %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wdfDir := filepath.Join(kits10, "include", "wdf")
|
||||||
|
if isDir(wdfDir) {
|
||||||
|
if err := Lowercase(wdfDir, LowercaseOptions{Symlink: true, MapWinSDK: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := FixInclude(wdfDir, FixIncludeOptions{MapWinSDK: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arch := range archs {
|
||||||
|
for _, sub := range []string{"um", "km"} {
|
||||||
|
dir := filepath.Join(kits10, "lib", sdkVer, sub, arch)
|
||||||
|
if !isDir(dir) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := Lowercase(dir, LowercaseOptions{Symlink: true}); err != nil {
|
||||||
|
return fmt.Errorf("lowercasing %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
host, dotnetHost := hostArch()
|
||||||
|
|
||||||
|
modulesRel := filepath.Join("VC", "Tools", "MSVC", msvcVer, "modules")
|
||||||
|
if isDir(filepath.Join(dest, modulesRel)) {
|
||||||
|
if err := lnS(modulesRel, filepath.Join(dest, "modules")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destBin := filepath.Join(dest, "bin")
|
||||||
|
if err := os.MkdirAll(destBin, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sharedBinary := filepath.Join(destBin, "msvc-go-wine")
|
||||||
|
if err := copyFile(selfBinary, sharedBinary, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("installing shared binary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
installed := 0
|
||||||
|
for _, arch := range archs {
|
||||||
|
clExe := filepath.Join(msvcDir, "bin", "Host"+host, arch, "cl.exe")
|
||||||
|
if !isFile(clExe) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := setupWrapperDir(destBin, selfBinary, arch, host, dotnetHost, msvcVer, sdkVer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
installed++
|
||||||
|
fmt.Println("Installed tool wrappers for", arch)
|
||||||
|
}
|
||||||
|
if installed == 0 {
|
||||||
|
return fmt.Errorf("no target architecture found under %s/bin/Host%s/*", msvcDir, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bootstrapWine() {
|
||||||
|
// Best-effort: if this fails, the wrapper runtime just falls back
|
||||||
|
// to invoking tools directly through wine, without toolrelay.exe's
|
||||||
|
// mt.exe/CMake exit-code fixup.
|
||||||
|
if err := buildToolRelay(dest, destBin, host); err != nil {
|
||||||
|
fmt.Println("Building toolrelay failed (continuing without it):", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Build toolrelay done.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findMSVCVersion(msvcRoot string) (string, error) {
|
||||||
|
entries, err := os.ReadDir(msvcRoot)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("reading %s: %w", msvcRoot, err)
|
||||||
|
}
|
||||||
|
var names []string
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
names = append(names, e.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// `ls -r`: reverse of the default (lexical) sort order.
|
||||||
|
sort.Sort(sort.Reverse(sort.StringSlice(names)))
|
||||||
|
for _, name := range names {
|
||||||
|
dir := filepath.Join(msvcRoot, name)
|
||||||
|
if isDir(filepath.Join(dir, "bin")) && isDir(filepath.Join(dir, "include")) && isDir(filepath.Join(dir, "lib")) {
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no suitable MSVC version found under %s", msvcRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findSDKVersion(includeDir string) (string, error) {
|
||||||
|
entries, err := os.ReadDir(includeDir)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("reading %s: %w", includeDir, err)
|
||||||
|
}
|
||||||
|
var versions []string
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() && strings.HasPrefix(e.Name(), "10.") {
|
||||||
|
versions = append(versions, e.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(versions) == 0 {
|
||||||
|
return "", fmt.Errorf("no Windows SDK version (10.*) found under %s", includeDir)
|
||||||
|
}
|
||||||
|
sort.Strings(versions)
|
||||||
|
return versions[len(versions)-1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixLibCasing adds uppercase symlinks (LIBCMT.lib -> libcmt.lib, etc) so
|
||||||
|
// lld-link can resolve the /DEFAULTLIB directives cl.exe emits, which name
|
||||||
|
// these libs in upper case, on a case-sensitive filesystem.
|
||||||
|
func fixLibCasing(libDir string) error {
|
||||||
|
names := []string{"libcmt", "libcmtd", "msvcrt", "msvcrtd", "oldnames"}
|
||||||
|
for _, arch := range archs {
|
||||||
|
dir := filepath.Join(libDir, arch)
|
||||||
|
if !isDir(dir) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, n := range names {
|
||||||
|
lower := filepath.Join(dir, n+".lib")
|
||||||
|
if !isFile(lower) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
upper := filepath.Join(dir, strings.ToUpper(n)+".lib")
|
||||||
|
if err := lnS(n+".lib", upper); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeVctip deletes any vctip.exe found under dir: it phones home to
|
||||||
|
// Microsoft and is known to cause problems under Wine.
|
||||||
|
func removeVctip(dir string) error {
|
||||||
|
return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !d.IsDir() && strings.EqualFold(d.Name(), "vctip.exe") {
|
||||||
|
return os.Remove(path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// renameHostDirs normalizes the several casings different MSVC releases
|
||||||
|
// have used for their host-arch bin directories.
|
||||||
|
func renameHostDirs(binDir string) error {
|
||||||
|
renames := []struct{ from, to string }{
|
||||||
|
{"HostX64", "Hostx64"}, // 15.x - 16.4
|
||||||
|
{"HostARM64", "Hostarm64"}, // 17.2 - 17.3
|
||||||
|
{"HostArm64", "Hostarm64"}, // 17.4
|
||||||
|
}
|
||||||
|
for _, r := range renames {
|
||||||
|
from := filepath.Join(binDir, r.from)
|
||||||
|
to := filepath.Join(binDir, r.to)
|
||||||
|
if isDir(from) && !exists(to) {
|
||||||
|
if err := os.Rename(from, to); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
oldArm64 := filepath.Join(binDir, "Hostarm64", "ARM64")
|
||||||
|
newArm64 := filepath.Join(binDir, "Hostarm64", "arm64")
|
||||||
|
if isDir(oldArm64) && !exists(newArm64) {
|
||||||
|
if err := os.Rename(oldArm64, newArm64); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupWrapperDir creates <destBin>/<arch> with its own local copy of the
|
||||||
|
// msvc-go-wine binary (not a symlink to the shared one in destBin), and
|
||||||
|
// symlinks every tool name to that LOCAL copy.
|
||||||
|
//
|
||||||
|
// This matters: the wrapper runtime locates its own install root via
|
||||||
|
// os.Executable(), which fully resolves symlinks (like /proc/self/exe does)
|
||||||
|
// - it can't rely on os.Args[0], since not every shell passes a
|
||||||
|
// PATH-resolved absolute path as argv[0] (some just pass the bare command
|
||||||
|
// name, e.g. "cl", which would make a naive argv[0]-based lookup resolve
|
||||||
|
// against the caller's cwd instead of the install dir). A same-directory
|
||||||
|
// symlink (cl -> msvc-go-wine) resolves to a binary that's still in the
|
||||||
|
// right arch dir; a symlink to a binary one level up (cl -> ../msvc-go-wine)
|
||||||
|
// would not be.
|
||||||
|
func setupWrapperDir(destBin, selfBinary, arch, host, dotnetHost, msvcVer, sdkVer string) error {
|
||||||
|
archDir := filepath.Join(destBin, arch)
|
||||||
|
if err := os.MkdirAll(archDir, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
localBinary := filepath.Join(archDir, "msvc-go-wine")
|
||||||
|
if err := copyFile(selfBinary, localBinary, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("installing per-arch binary: %w", err)
|
||||||
|
}
|
||||||
|
for name := range toolNames {
|
||||||
|
if err := lnS("msvc-go-wine", filepath.Join(archDir, name)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := lnS("msvc-go-wine", filepath.Join(archDir, name+".exe")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg := &wineenv.Config{
|
||||||
|
Arch: arch,
|
||||||
|
Host: host,
|
||||||
|
DotnetHost: dotnetHost,
|
||||||
|
MSVCVer: msvcVer,
|
||||||
|
SDKVer: sdkVer,
|
||||||
|
}
|
||||||
|
return cfg.Save(archDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// toolNames is the set of tool wrapper symlinks created in every arch dir.
|
||||||
|
var toolNames = map[string]bool{
|
||||||
|
"cl": true, "link": true, "lib": true, "ml": true, "ml64": true,
|
||||||
|
"mc": true, "midl": true, "mt": true, "rc": true, "dumpbin": true,
|
||||||
|
"msbuild": true, "nmake": true, "armasm": true, "armasm64": true,
|
||||||
|
"cmd": true, "findstr": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostArch() (host, dotnetHost string) {
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "arm64":
|
||||||
|
return "arm64", "arm64"
|
||||||
|
default:
|
||||||
|
return "x64", "amd64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bootstrapWine runs `wineboot --init` to set up the wine prefix ahead of
|
||||||
|
// time, and reports whether wine is available at all (so callers can decide
|
||||||
|
// whether to attempt building toolrelay.exe).
|
||||||
|
func bootstrapWine() bool {
|
||||||
|
wine, err := wineenv.FindWine()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("wine not found, skipping wineboot bootstrap and toolrelay build (install wine before running the tools)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fmt.Println("Bootstrapping wine prefix...")
|
||||||
|
if err := runQuiet(wine, "wineboot", "--init"); err != nil {
|
||||||
|
fmt.Println("wineboot --init failed (continuing):", err)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildToolRelay compiles the vendored toolrelay.cpp helper using the
|
||||||
|
// freshly-installed host-arch cl wrapper. toolrelay.exe is what lets the
|
||||||
|
// wrapper runtime give mt.exe's CMake-compatibility exit code
|
||||||
|
// (0x41020001 -> 0xbb) a chance to survive Wine's own exit-code truncation -
|
||||||
|
// see internal/wrapper's runViaToolRelay. Best-effort: any failure here just
|
||||||
|
// means the wrapper runtime falls back to a plain wine invocation without
|
||||||
|
// that fixup.
|
||||||
|
func buildToolRelay(dest, destBin, host string) error {
|
||||||
|
clWrapper := filepath.Join(destBin, host, "cl")
|
||||||
|
if !isFile(clWrapper) {
|
||||||
|
return fmt.Errorf("no cl wrapper for host arch %s at %s", host, clWrapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcPath := filepath.Join(dest, "toolrelay.cpp")
|
||||||
|
if err := os.WriteFile(srcPath, assets.ToolRelaySource, 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(srcPath)
|
||||||
|
|
||||||
|
fmt.Println("Build toolrelay ...")
|
||||||
|
cmd := exec.Command(clWrapper, "/EHsc", "/O2", srcPath)
|
||||||
|
cmd.Dir = dest
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("compiling toolrelay.cpp: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exePath := filepath.Join(dest, "toolrelay.exe")
|
||||||
|
objPath := filepath.Join(dest, "toolrelay.obj")
|
||||||
|
defer os.Remove(objPath)
|
||||||
|
if !isFile(exePath) {
|
||||||
|
return fmt.Errorf("cl did not produce %s", exePath)
|
||||||
|
}
|
||||||
|
return os.Rename(exePath, filepath.Join(destBin, "toolrelay.exe"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func runQuiet(name string, args ...string) error {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
cmd.Env = append(os.Environ(), "WINEDEBUG=-all")
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func lnS(target, link string) error {
|
||||||
|
if _, err := os.Lstat(link); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.Symlink(target, link)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDir(path string) bool {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
return err == nil && fi.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFile(path string) bool {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
return err == nil && !fi.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func exists(path string) bool {
|
||||||
|
_, err := os.Lstat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(src, dst string, mode os.FileMode) error {
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
_ = os.Remove(dst)
|
||||||
|
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
_, err = io.Copy(out, in)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package install
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustMkdirAll(t *testing.T, path string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.MkdirAll(path, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustWriteFile(t *testing.T, path, content string) {
|
||||||
|
t.Helper()
|
||||||
|
mustMkdirAll(t, filepath.Dir(path))
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLowercaseRenameMode(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
// The top-level dir passed to Lowercase is itself lowercased too when
|
||||||
|
// not already lowercase (matching the original perl script's dodir,
|
||||||
|
// which lowercases relpath=="" using the dir's own basename).
|
||||||
|
tree := filepath.Join(root, "Include")
|
||||||
|
mustWriteFile(t, filepath.Join(tree, "Foo", "Bar.H"), "content")
|
||||||
|
|
||||||
|
if err := Lowercase(tree, LowercaseOptions{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lowerTree := filepath.Join(root, "include")
|
||||||
|
if !isFile(filepath.Join(lowerTree, "foo", "bar.h")) {
|
||||||
|
t.Fatalf("expected lowercased path to exist under %s", lowerTree)
|
||||||
|
}
|
||||||
|
if isDir(tree) {
|
||||||
|
t.Fatalf("original-cased top dir should have been renamed away")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLowercaseSymlinkMode(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
tree := filepath.Join(root, "include")
|
||||||
|
mustWriteFile(t, filepath.Join(tree, "Foo", "Bar.h"), "content")
|
||||||
|
|
||||||
|
if err := Lowercase(tree, LowercaseOptions{Symlink: true}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original casing must still be reachable (symlink mode never deletes).
|
||||||
|
if !isFile(filepath.Join(tree, "Foo", "Bar.h")) {
|
||||||
|
t.Fatalf("original-cased file should still exist in symlink mode")
|
||||||
|
}
|
||||||
|
// Lowercase alias must resolve to the same content.
|
||||||
|
data, err := os.ReadFile(filepath.Join(tree, "foo", "bar.h"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected lowercase alias to resolve: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != "content" {
|
||||||
|
t.Errorf("got %q", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLowercaseMergeOnCollision(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
tree := filepath.Join(root, "include")
|
||||||
|
// Both "GL" and "gl" exist as siblings; lowercasing "GL" must merge its
|
||||||
|
// contents into the already-lowercase "gl" rather than clobbering it.
|
||||||
|
mustWriteFile(t, filepath.Join(tree, "gl", "existing.h"), "existing")
|
||||||
|
mustWriteFile(t, filepath.Join(tree, "GL", "New.h"), "new")
|
||||||
|
|
||||||
|
if err := Lowercase(tree, LowercaseOptions{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isFile(filepath.Join(tree, "gl", "existing.h")) {
|
||||||
|
t.Errorf("pre-existing lowercase file lost during merge")
|
||||||
|
}
|
||||||
|
if !isFile(filepath.Join(tree, "gl", "new.h")) {
|
||||||
|
t.Errorf("merged file not found at lowercase destination")
|
||||||
|
}
|
||||||
|
if isDir(filepath.Join(tree, "GL")) {
|
||||||
|
t.Errorf("source dir should have been removed after merge (non-symlink mode)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLowercaseMapWinSDKPreservesGL(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
tree := filepath.Join(root, "um")
|
||||||
|
mustWriteFile(t, filepath.Join(tree, "GL", "gl.h"), "content")
|
||||||
|
|
||||||
|
if err := Lowercase(tree, LowercaseOptions{Symlink: true, MapWinSDK: true}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isDir(filepath.Join(tree, "GL")) {
|
||||||
|
t.Errorf("GL directory casing should be preserved under -map_winsdk")
|
||||||
|
}
|
||||||
|
if isDir(filepath.Join(tree, "gl")) {
|
||||||
|
t.Errorf("no lowercase alias should be created for GL under -map_winsdk (name maps back to itself)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFixIncludeLowercasesAndConvertsSlashes(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
header := filepath.Join(root, "foo.h")
|
||||||
|
mustWriteFile(t, header, "#include <Some\\Path.H>\r\n#include \"Other.h\" // comment\r\nplain line\r\n")
|
||||||
|
|
||||||
|
if err := FixInclude(root, FixIncludeOptions{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(header)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := "#include <some/path.h>\n#include \"other.h\" // comment\nplain line\n"
|
||||||
|
if string(data) != want {
|
||||||
|
t.Errorf("got %q want %q", data, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFixIncludeMapWinSDKPreservesGL(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
header := filepath.Join(root, "foo.h")
|
||||||
|
mustWriteFile(t, header, "#include <GL/gl.h>\n")
|
||||||
|
|
||||||
|
if err := FixInclude(root, FixIncludeOptions{MapWinSDK: true}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(header)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := "#include <GL/gl.h>\n"
|
||||||
|
if string(data) != want {
|
||||||
|
t.Errorf("got %q want %q", data, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFixIncludeSkipsSymlinks(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
real := filepath.Join(root, "Real.h")
|
||||||
|
mustWriteFile(t, real, "#include <Foo.h>\n")
|
||||||
|
link := filepath.Join(root, "alias.h")
|
||||||
|
if err := os.Symlink(real, link); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := FixInclude(root, FixIncludeOptions{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The symlink itself must be untouched (still a symlink to the same
|
||||||
|
// target); only Real.h's contents get rewritten.
|
||||||
|
target, err := os.Readlink(link)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("alias.h should still be a symlink: %v", err)
|
||||||
|
}
|
||||||
|
if target != real {
|
||||||
|
t.Errorf("symlink target changed: %q", target)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package install
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestBuildToolRelay exercises the file shuffling around the actual
|
||||||
|
// compiler invocation (write embedded source, run "cl", move the resulting
|
||||||
|
// .exe into bin/, clean up the .obj and temp source) using a fake `cl`
|
||||||
|
// shell script standing in for the real Wine-hosted compiler - this can
|
||||||
|
// run fully offline, without wine or a real MSVC install.
|
||||||
|
func TestBuildToolRelay(t *testing.T) {
|
||||||
|
dest := t.TempDir()
|
||||||
|
destBin := filepath.Join(dest, "bin")
|
||||||
|
hostDir := filepath.Join(destBin, "x64")
|
||||||
|
if err := os.MkdirAll(hostDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fake `cl`: just drop toolrelay.exe/.obj next to the source it was
|
||||||
|
// given, like the real cl.exe would when invoked without /Fe or /Fo.
|
||||||
|
fakeCl := filepath.Join(hostDir, "cl")
|
||||||
|
script := "#!/bin/sh\ntouch toolrelay.exe toolrelay.obj\n"
|
||||||
|
if err := os.WriteFile(fakeCl, []byte(script), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := buildToolRelay(dest, destBin, "x64"); err != nil {
|
||||||
|
t.Fatalf("buildToolRelay: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isFile(filepath.Join(destBin, "toolrelay.exe")) {
|
||||||
|
t.Errorf("expected %s/toolrelay.exe to exist", destBin)
|
||||||
|
}
|
||||||
|
if exists(filepath.Join(dest, "toolrelay.obj")) {
|
||||||
|
t.Errorf("toolrelay.obj should have been removed from %s", dest)
|
||||||
|
}
|
||||||
|
if exists(filepath.Join(dest, "toolrelay.cpp")) {
|
||||||
|
t.Errorf("temp toolrelay.cpp source should have been removed from %s", dest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildToolRelayNoClWrapper(t *testing.T) {
|
||||||
|
dest := t.TempDir()
|
||||||
|
destBin := filepath.Join(dest, "bin")
|
||||||
|
if err := os.MkdirAll(destBin, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := buildToolRelay(dest, destBin, "x64"); err == nil {
|
||||||
|
t.Fatal("expected an error when no host-arch cl wrapper exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Package wineenv computes the environment (INCLUDE/LIB/WINEPATH/...) needed
|
||||||
|
// to run a Wine-hosted MSVC tool.
|
||||||
|
package wineenv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigFileName is the per-architecture config dropped next to the tool
|
||||||
|
// symlinks by `msvc-go-wine install`.
|
||||||
|
const ConfigFileName = "env.json"
|
||||||
|
|
||||||
|
// Config is the per-architecture info generated at install time.
|
||||||
|
type Config struct {
|
||||||
|
Arch string `json:"arch"` // x86, x64, arm, arm64 (target arch)
|
||||||
|
Host string `json:"host"` // x64 or arm64 (Host<Host> bin dir suffix)
|
||||||
|
DotnetHost string `json:"dotnet_host"` // amd64 or arm64 (.NET tool host dir suffix)
|
||||||
|
MSVCVer string `json:"msvc_ver"`
|
||||||
|
SDKVer string `json:"sdk_ver"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reads env.json from dir (the directory the running binary was
|
||||||
|
// invoked from, i.e. <dest>/bin/<arch>).
|
||||||
|
func Load(dir string) (*Config, error) {
|
||||||
|
data, err := os.ReadFile(filepath.Join(dir, ConfigFileName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading %s: %w", ConfigFileName, err)
|
||||||
|
}
|
||||||
|
var c Config
|
||||||
|
if err := json.Unmarshal(data, &c); err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing %s: %w", ConfigFileName, err)
|
||||||
|
}
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save writes env.json into dir.
|
||||||
|
func (c *Config) Save(dir string) error {
|
||||||
|
data, err := json.MarshalIndent(c, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(filepath.Join(dir, ConfigFileName), data, 0o644)
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package wineenv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Paths holds every path/env-var derived from a Config plus the on-disk
|
||||||
|
// installation root - INCLUDE, LIB, WINEPATH and friends, in the Windows
|
||||||
|
// notation Wine expects.
|
||||||
|
type Paths struct {
|
||||||
|
BaseUnix string // <dest>, absolute unix path
|
||||||
|
MSVCDirUnix string // <dest>/vc/tools/msvc/<ver>
|
||||||
|
BinDir string // <dest>/vc/tools/msvc/<ver>/bin/Host<host>/<arch> - cl/link/lib/ml/nmake/armasm 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
|
||||||
|
|
||||||
|
Include string
|
||||||
|
Lib string
|
||||||
|
LibPath string
|
||||||
|
WinePath string
|
||||||
|
WineDLLOverrides string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindBaseUnix locates the installation root starting from the directory the
|
||||||
|
// tool wrapper was invoked from (scriptDir), searching one or two levels up
|
||||||
|
// for a "vc" entry - matching the original wrappers' `dirname "$0"`/".." walk,
|
||||||
|
// which tolerates wrappers living either directly in <dest>/bin/<arch> or
|
||||||
|
// nested one level deeper.
|
||||||
|
func FindBaseUnix(scriptDir string) (string, error) {
|
||||||
|
base, err := filepath.Abs(filepath.Join(scriptDir, ".."))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(base, "vc")); err != nil {
|
||||||
|
base = filepath.Join(base, "..")
|
||||||
|
}
|
||||||
|
return filepath.Abs(base)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPaths computes all derived paths/env-vars for cfg installed at baseUnix.
|
||||||
|
func NewPaths(cfg *Config, baseUnix string) *Paths {
|
||||||
|
winBase := "z:" + toWindowsSlashes(baseUnix)
|
||||||
|
msvcBase := winBase + `\vc`
|
||||||
|
sdkBase := winBase + `\kits\10`
|
||||||
|
msvcDirWin := msvcBase + `\tools\msvc\` + cfg.MSVCVer
|
||||||
|
sdkIncludeWin := sdkBase + `\include\` + cfg.SDKVer
|
||||||
|
sdkLibWin := sdkBase + `\lib\` + cfg.SDKVer
|
||||||
|
|
||||||
|
msvcDirUnix := filepath.Join(baseUnix, "vc", "tools", "msvc", cfg.MSVCVer)
|
||||||
|
binDir := filepath.Join(msvcDirUnix, "bin", "Host"+cfg.Host, cfg.Arch)
|
||||||
|
sdkBinDir := filepath.Join(baseUnix, "kits", "10", "bin", cfg.SDKVer, cfg.Host)
|
||||||
|
msbuildBinDir := filepath.Join(baseUnix, "MSBuild", "Current", "Bin", cfg.DotnetHost)
|
||||||
|
|
||||||
|
include := strings.Join([]string{
|
||||||
|
msvcDirWin + `\atlmfc\include`,
|
||||||
|
msvcDirWin + `\include`,
|
||||||
|
sdkIncludeWin + `\shared`,
|
||||||
|
sdkIncludeWin + `\ucrt`,
|
||||||
|
sdkIncludeWin + `\um`,
|
||||||
|
sdkIncludeWin + `\winrt`,
|
||||||
|
sdkIncludeWin + `\km`,
|
||||||
|
}, ";")
|
||||||
|
|
||||||
|
lib := strings.Join([]string{
|
||||||
|
msvcDirWin + `\atlmfc\lib\` + cfg.Arch,
|
||||||
|
msvcDirWin + `\lib\` + cfg.Arch,
|
||||||
|
sdkLibWin + `\ucrt\` + cfg.Arch,
|
||||||
|
sdkLibWin + `\um\` + cfg.Arch,
|
||||||
|
sdkLibWin + `\km\` + cfg.Arch,
|
||||||
|
}, ";")
|
||||||
|
|
||||||
|
// WINEPATH: unix dirs with slashes swapped for backslashes (no drive
|
||||||
|
// letter - Wine accepts this for locating DLLs), plus the MSVC host bin
|
||||||
|
// dir in full windows notation, deliberately always the *host* (not
|
||||||
|
// target) x64/arm64 tool dir for the third entry - that's where DLLs
|
||||||
|
// like mspdbcore.dll actually live.
|
||||||
|
winePath := strings.Join([]string{
|
||||||
|
toWindowsSlashes(binDir),
|
||||||
|
toWindowsSlashes(sdkBinDir),
|
||||||
|
msvcDirWin + `\bin\Host` + cfg.Host + `\` + cfg.Host,
|
||||||
|
}, ";")
|
||||||
|
|
||||||
|
return &Paths{
|
||||||
|
BaseUnix: baseUnix,
|
||||||
|
MSVCDirUnix: msvcDirUnix,
|
||||||
|
BinDir: binDir,
|
||||||
|
SDKBinDir: sdkBinDir,
|
||||||
|
MSBuildBinDir: msbuildBinDir,
|
||||||
|
|
||||||
|
Include: include,
|
||||||
|
Lib: lib,
|
||||||
|
LibPath: lib,
|
||||||
|
WinePath: winePath,
|
||||||
|
WineDLLOverrides: "vcruntime140=n;vcruntime140_1=n",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toWindowsSlashes(p string) string {
|
||||||
|
return strings.ReplaceAll(p, "/", `\`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToWinPath prefixes a unix absolute path with the "z:" drive Wine maps the
|
||||||
|
// unix root to, converting slashes to backslashes.
|
||||||
|
func ToWinPath(unixPath string) string {
|
||||||
|
return "z:" + toWindowsSlashes(unixPath)
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package wineenv
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Values lifted from the original wrappers/cl template
|
||||||
|
// (MSVCVER=14.13.26128, SDKVER=10.0.16299.0, ARCH=x86) to cross-check the Go
|
||||||
|
// port produces byte-identical INCLUDE/LIB strings.
|
||||||
|
func TestNewPathsMatchesOriginalTemplate(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
Arch: "x86",
|
||||||
|
Host: "x64",
|
||||||
|
DotnetHost: "amd64",
|
||||||
|
MSVCVer: "14.13.26128",
|
||||||
|
SDKVer: "10.0.16299.0",
|
||||||
|
}
|
||||||
|
p := NewPaths(cfg, "/opt/msvc")
|
||||||
|
|
||||||
|
wantInclude := `z:\opt\msvc\vc\tools\msvc\14.13.26128\atlmfc\include;` +
|
||||||
|
`z:\opt\msvc\vc\tools\msvc\14.13.26128\include;` +
|
||||||
|
`z:\opt\msvc\kits\10\include\10.0.16299.0\shared;` +
|
||||||
|
`z:\opt\msvc\kits\10\include\10.0.16299.0\ucrt;` +
|
||||||
|
`z:\opt\msvc\kits\10\include\10.0.16299.0\um;` +
|
||||||
|
`z:\opt\msvc\kits\10\include\10.0.16299.0\winrt;` +
|
||||||
|
`z:\opt\msvc\kits\10\include\10.0.16299.0\km`
|
||||||
|
if p.Include != wantInclude {
|
||||||
|
t.Errorf("INCLUDE mismatch:\n got: %s\nwant: %s", p.Include, wantInclude)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantLib := `z:\opt\msvc\vc\tools\msvc\14.13.26128\atlmfc\lib\x86;` +
|
||||||
|
`z:\opt\msvc\vc\tools\msvc\14.13.26128\lib\x86;` +
|
||||||
|
`z:\opt\msvc\kits\10\lib\10.0.16299.0\ucrt\x86;` +
|
||||||
|
`z:\opt\msvc\kits\10\lib\10.0.16299.0\um\x86;` +
|
||||||
|
`z:\opt\msvc\kits\10\lib\10.0.16299.0\km\x86`
|
||||||
|
if p.Lib != wantLib {
|
||||||
|
t.Errorf("LIB mismatch:\n got: %s\nwant: %s", p.Lib, wantLib)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.WineDLLOverrides != "vcruntime140=n;vcruntime140_1=n" {
|
||||||
|
t.Errorf("WINEDLLOVERRIDES mismatch: %s", p.WineDLLOverrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantBinDir := "/opt/msvc/vc/tools/msvc/14.13.26128/bin/Hostx64/x86"
|
||||||
|
if p.BinDir != wantBinDir {
|
||||||
|
t.Errorf("BinDir mismatch:\n got: %s\nwant: %s", p.BinDir, wantBinDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantSDKBinDir := "/opt/msvc/kits/10/bin/10.0.16299.0/x64"
|
||||||
|
if p.SDKBinDir != wantSDKBinDir {
|
||||||
|
t.Errorf("SDKBinDir mismatch:\n got: %s\nwant: %s", p.SDKBinDir, wantSDKBinDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantMSBuildBinDir := "/opt/msvc/MSBuild/Current/Bin/amd64"
|
||||||
|
if p.MSBuildBinDir != wantMSBuildBinDir {
|
||||||
|
t.Errorf("MSBuildBinDir mismatch:\n got: %s\nwant: %s", p.MSBuildBinDir, wantMSBuildBinDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package wineenv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FindWine locates the wine binary to use, preferring wine64 like the
|
||||||
|
// original wrappers (`command -v wine64 || command -v wine`).
|
||||||
|
func FindWine() (string, error) {
|
||||||
|
if p, err := exec.LookPath("wine64"); err == nil {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
if p, err := exec.LookPath("wine"); err == nil {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("neither wine64 nor wine found in PATH")
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package wrapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// clPostProcess mirrors cl's post-run fixup for `/P /Fi<file>` (preprocess-
|
||||||
|
// to-file): the generated file still has CRLF endings and z:-prefixed
|
||||||
|
// #line directives, so run it through the same line-directive rewrite used
|
||||||
|
// for stdout.
|
||||||
|
func clPostProcess(origArgs []string) {
|
||||||
|
var hasP bool
|
||||||
|
var fiFile string
|
||||||
|
for _, a := range origArgs {
|
||||||
|
switch {
|
||||||
|
case a == "-P" || a == "/P":
|
||||||
|
hasP = true
|
||||||
|
case strings.HasPrefix(a, "-Fi") || strings.HasPrefix(a, "/Fi"):
|
||||||
|
fiFile = a[3:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasP || fiFile == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(fiFile)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
line = stripCR(line)
|
||||||
|
if reLineDirective.MatchString(line) {
|
||||||
|
line = strings.ReplaceAll(stripFirstZDrive(line), `\\`, `/`)
|
||||||
|
}
|
||||||
|
lines[i] = line
|
||||||
|
}
|
||||||
|
_ = os.WriteFile(fiFile, []byte(strings.Join(lines, "\n")), 0o644)
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package wrapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// lineFilter rewrites a single line of tool output. nil means "no extra
|
||||||
|
// rewriting" (CR-stripping still always happens, see stripCR).
|
||||||
|
type lineFilter func(line string) string
|
||||||
|
|
||||||
|
// reZDrive matches the wine "z:" drive prefix followed by a path separator,
|
||||||
|
// case-insensitively, anywhere in the line - mirroring sed's `s/z:([\\/])/\1/i`
|
||||||
|
// (no /g flag: only the first occurrence is touched).
|
||||||
|
var reZDrive = regexp.MustCompile(`(?i)z:([\\/])`)
|
||||||
|
|
||||||
|
// stripFirstZDrive removes the first "z:" preceding a path separator,
|
||||||
|
// keeping the separator itself, matching the original's un-global sed rule.
|
||||||
|
func stripFirstZDrive(line string) string {
|
||||||
|
loc := reZDrive.FindStringSubmatchIndex(line)
|
||||||
|
if loc == nil {
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
sep := line[loc[2]:loc[3]]
|
||||||
|
return line[:loc[0]] + sep + line[loc[1]:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
reNoteIncluding = regexp.MustCompile(`^Note: including file: `)
|
||||||
|
reLineDirective = regexp.MustCompile(`^[ \t]*#[ \t]*line[ \t]`)
|
||||||
|
reNoteErrorWarning = regexp.MustCompile(`(?i)^z:.*\([0-9]+\): (note|error c[0-9]{4}|warning c[0-9]{4}): `)
|
||||||
|
reDumpbinPath = regexp.MustCompile(`^(Dump of file | PDB file found at )`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// clStdoutFilter rewrites cl's "Note: including file:", "#line", and
|
||||||
|
// note/warning/error diagnostic lines from wine's z:\... notation back to
|
||||||
|
// plain unix paths.
|
||||||
|
func clStdoutFilter(line string) string {
|
||||||
|
switch {
|
||||||
|
case reNoteIncluding.MatchString(line):
|
||||||
|
return strings.ReplaceAll(stripFirstZDrive(line), `\`, `/`)
|
||||||
|
case reLineDirective.MatchString(line):
|
||||||
|
return strings.ReplaceAll(stripFirstZDrive(line), `\\`, `/`)
|
||||||
|
case reNoteErrorWarning.MatchString(line):
|
||||||
|
return strings.ReplaceAll(stripFirstZDrive(line), `\`, `/`)
|
||||||
|
default:
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clStderrFilter only rewrites "Note: including file:" lines - cl's stderr
|
||||||
|
// diagnostics don't carry the z:\... prefix the stdout ones do.
|
||||||
|
func clStderrFilter(line string) string {
|
||||||
|
if reNoteIncluding.MatchString(line) {
|
||||||
|
return strings.ReplaceAll(stripFirstZDrive(line), `\`, `/`)
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
// dumpbinStdoutFilter mirrors dumpbin's unixify_path variant.
|
||||||
|
func dumpbinStdoutFilter(line string) string {
|
||||||
|
if reDumpbinPath.MatchString(line) {
|
||||||
|
return strings.ReplaceAll(stripFirstZDrive(line), `\`, `/`)
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
// stripCR mirrors the base `s/\r//` rule every wrapper prepends: removes the
|
||||||
|
// first carriage return in the line (CRLF line endings only ever carry one).
|
||||||
|
func stripCR(line string) string {
|
||||||
|
return strings.Replace(line, "\r", "", 1)
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package wrapper
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestClStdoutFilterNoteIncluding(t *testing.T) {
|
||||||
|
in := `Note: including file: z:\home\user\project\test.h`
|
||||||
|
want := `Note: including file: /home/user/project/test.h`
|
||||||
|
if got := clStdoutFilter(in); got != want {
|
||||||
|
t.Errorf("got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClStdoutFilterLineDirective(t *testing.T) {
|
||||||
|
in := `#line 5 "z:\\home\\user\\project\\test.c"`
|
||||||
|
want := `#line 5 "/home/user/project/test.c"`
|
||||||
|
if got := clStdoutFilter(in); got != want {
|
||||||
|
t.Errorf("got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClStdoutFilterErrorLine(t *testing.T) {
|
||||||
|
in := `z:\home\user\project\test.c(5): warning C4996: 'foo' was declared deprecated`
|
||||||
|
want := `/home/user/project/test.c(5): warning C4996: 'foo' was declared deprecated`
|
||||||
|
if got := clStdoutFilter(in); got != want {
|
||||||
|
t.Errorf("got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClStdoutFilterNoteLine(t *testing.T) {
|
||||||
|
in := `z:\home\user\project\test.h(3): note: see declaration of 'foo'`
|
||||||
|
want := `/home/user/project/test.h(3): note: see declaration of 'foo'`
|
||||||
|
if got := clStdoutFilter(in); got != want {
|
||||||
|
t.Errorf("got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClStdoutFilterPassthrough(t *testing.T) {
|
||||||
|
in := "test.c"
|
||||||
|
if got := clStdoutFilter(in); got != in {
|
||||||
|
t.Errorf("got %q want %q", got, in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClStderrFilter(t *testing.T) {
|
||||||
|
in := `Note: including file: z:\a\b.h`
|
||||||
|
want := `Note: including file: /a/b.h`
|
||||||
|
if got := clStderrFilter(in); got != want {
|
||||||
|
t.Errorf("got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
// stderr does NOT get the error/warning/line-directive rewrite.
|
||||||
|
in2 := `z:\a\b.c(1): error C2065: undeclared identifier`
|
||||||
|
if got := clStderrFilter(in2); got != in2 {
|
||||||
|
t.Errorf("stderr should pass through error lines unchanged, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDumpbinStdoutFilter(t *testing.T) {
|
||||||
|
in := ` PDB file found at z:\a\b\file.pdb`
|
||||||
|
want := ` PDB file found at /a/b/file.pdb`
|
||||||
|
if got := dumpbinStdoutFilter(in); got != want {
|
||||||
|
t.Errorf("got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
in2 := `Dump of file z:\a\b\file.exe`
|
||||||
|
want2 := `Dump of file /a/b/file.exe`
|
||||||
|
if got := dumpbinStdoutFilter(in2); got != want2 {
|
||||||
|
t.Errorf("got %q want %q", got, want2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStripCR(t *testing.T) {
|
||||||
|
if got := stripCR("hello\r"); got != "hello" {
|
||||||
|
t.Errorf("got %q", got)
|
||||||
|
}
|
||||||
|
if got := stripCR("no cr here"); got != "no cr here" {
|
||||||
|
t.Errorf("got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package wrapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// runNative handles the two tool names that need no Wine/MSVC install at
|
||||||
|
// all: `cmd` (strips a leading "//c" and execs the rest) and `findstr`
|
||||||
|
// (delegates to grep).
|
||||||
|
func runNative(tool string, args []string) int {
|
||||||
|
switch tool {
|
||||||
|
case "cmd":
|
||||||
|
if len(args) > 0 && args[0] == "//c" {
|
||||||
|
args = args[1:]
|
||||||
|
}
|
||||||
|
return execInherit(args)
|
||||||
|
case "findstr":
|
||||||
|
return execInherit(append([]string{"grep"}, args...))
|
||||||
|
}
|
||||||
|
return 127
|
||||||
|
}
|
||||||
|
|
||||||
|
func execInherit(args []string) int {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
return exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
return 127
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package wrapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Argument path rewriting: MSVC/Wine sometimes fails to resolve relative-
|
||||||
|
// looking includes passed as plain unix absolute paths (see
|
||||||
|
// https://bugs.winehq.org/show_bug.cgi?id=55200); the fix is to rewrite
|
||||||
|
// `-I/abs/path` into `-Iz:/abs/path` and similar, trying each option-prefix
|
||||||
|
// shape in priority order until one matches.
|
||||||
|
var (
|
||||||
|
reOpt1 = regexp.MustCompile(`^[-/][A-Za-z](/.*)$`) // -I/path, /I/path
|
||||||
|
reOpt2 = regexp.MustCompile(`^[-/][A-Za-z][A-Za-z](/.*)$`) // -Fo/path
|
||||||
|
reOpt3 = regexp.MustCompile(`^[-/][A-Za-z][A-Za-z][A-Za-z]*:(/.*)$`) // -MANIFESTINPUT:/path
|
||||||
|
reBare = regexp.MustCompile(`^(/.*)$`) // /abs/path alone
|
||||||
|
)
|
||||||
|
|
||||||
|
// RewriteArgs rewrites absolute unix paths embedded in tool arguments into
|
||||||
|
// "z:/abs/path" form, only when the path's parent directory actually exists
|
||||||
|
// on disk and isn't "/" - matching the original bash's `[ -d "$(dirname ..)" ]`
|
||||||
|
// guard, which keeps short flags like "/P" or "/D" untouched.
|
||||||
|
func RewriteArgs(args []string) []string {
|
||||||
|
out := make([]string, len(args))
|
||||||
|
for i, a := range args {
|
||||||
|
out[i] = rewriteArg(a)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func rewriteArg(a string) string {
|
||||||
|
var path string
|
||||||
|
switch {
|
||||||
|
case reOpt1.MatchString(a):
|
||||||
|
path = reOpt1.FindStringSubmatch(a)[1]
|
||||||
|
case reOpt2.MatchString(a):
|
||||||
|
path = reOpt2.FindStringSubmatch(a)[1]
|
||||||
|
case reOpt3.MatchString(a):
|
||||||
|
path = reOpt3.FindStringSubmatch(a)[1]
|
||||||
|
case reBare.MatchString(a):
|
||||||
|
path = reBare.FindStringSubmatch(a)[1]
|
||||||
|
default:
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if dir == "/" {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
fi, err := os.Stat(dir)
|
||||||
|
if err != nil || !fi.IsDir() {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := a[:len(a)-len(path)]
|
||||||
|
return prefix + "z:" + path
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package wrapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRewriteArg(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
sub := filepath.Join(dir, "inc")
|
||||||
|
if err := os.Mkdir(sub, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
file := filepath.Join(sub, "foo.h")
|
||||||
|
if err := os.WriteFile(file, nil, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"single-letter option + abs dir", "-I" + sub, "-Iz:" + sub},
|
||||||
|
{"two-letter option + abs file", "-Fo" + file, "-Foz:" + file},
|
||||||
|
{"long colon option + abs file", "-MANIFESTINPUT:" + file, "-MANIFESTINPUT:z:" + file},
|
||||||
|
{"bare absolute path", file, "z:" + file},
|
||||||
|
{"plain flag untouched", "-nologo", "-nologo"},
|
||||||
|
{"nonexistent dir untouched", "-I/does/not/exist/at/all", "-I/does/not/exist/at/all"},
|
||||||
|
{"root-level bare path untouched", "/nologo", "/nologo"},
|
||||||
|
{"relative path untouched", "test.c", "test.c"},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := rewriteArg(tc.in); got != tc.want {
|
||||||
|
t.Errorf("rewriteArg(%q) = %q, want %q", tc.in, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRewriteArgsPreservesOrderAndLength(t *testing.T) {
|
||||||
|
in := []string{"/nologo", "-c", "test.c"}
|
||||||
|
out := RewriteArgs(in)
|
||||||
|
if len(out) != len(in) {
|
||||||
|
t.Fatalf("length changed: %v -> %v", in, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
package wrapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/Cheviiot/msvc-go-wine/internal/wineenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// toolRelayName is where `msvc-go-wine install` places the compiled
|
||||||
|
// toolrelay.exe helper, shared across all arch bin dirs.
|
||||||
|
const toolRelayName = "toolrelay.exe"
|
||||||
|
|
||||||
|
// Run executes the named multi-call tool with args, exactly as the original
|
||||||
|
// bash wrappers would, and returns the process exit code.
|
||||||
|
func Run(tool string, args []string) int {
|
||||||
|
if nativeTools[tool] {
|
||||||
|
return runNative(tool, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, ok := Tools[tool]
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintf(os.Stderr, "msvc-go-wine: unknown tool %q\n", tool)
|
||||||
|
return 127
|
||||||
|
}
|
||||||
|
|
||||||
|
// os.Executable() (backed by /proc/self/exe on Linux) fully resolves
|
||||||
|
// symlinks, unlike os.Args[0]: not every shell passes a PATH-resolved
|
||||||
|
// absolute path as argv[0] (some just pass the bare command name), which
|
||||||
|
// would make an argv[0]-based lookup resolve against the caller's cwd
|
||||||
|
// instead of the actual install dir. `install` sets each arch dir up
|
||||||
|
// with its own local copy of the binary precisely so this resolves to
|
||||||
|
// <dest>/bin/<arch>, not <dest>/bin.
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
scriptDir := filepath.Dir(exePath)
|
||||||
|
|
||||||
|
cfg, err := wineenv.Load(scriptDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine: loading install config:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
baseUnix, err := wineenv.FindBaseUnix(scriptDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine: locating installation root:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
paths := wineenv.NewPaths(cfg, baseUnix)
|
||||||
|
|
||||||
|
toolExePath := filepath.Join(s.exeDir(paths), s.exeName)
|
||||||
|
|
||||||
|
wineBin, err := wineenv.FindWine()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
rewritten := RewriteArgs(args)
|
||||||
|
|
||||||
|
var exitCode int
|
||||||
|
switch {
|
||||||
|
case s.rawStdout:
|
||||||
|
// MSBuild: skip all filtering/toolrelay, inherit stdio directly.
|
||||||
|
cmd := exec.Command(wineBin, append([]string{toolExePath}, rewritten...)...)
|
||||||
|
cmd.Env = buildEnv(paths)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
exitCode = runAndWait(cmd)
|
||||||
|
default:
|
||||||
|
relay := filepath.Join(paths.BaseUnix, "bin", toolRelayName)
|
||||||
|
if fi, err := os.Stat(relay); err == nil && !fi.IsDir() {
|
||||||
|
exitCode = runViaToolRelay(wineBin, relay, toolExePath, rewritten, paths, s.stdoutFilter, s.stderrFilter)
|
||||||
|
} else {
|
||||||
|
cmd := exec.Command(wineBin, append([]string{toolExePath}, rewritten...)...)
|
||||||
|
cmd.Env = buildEnv(paths)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
exitCode = runFiltered(cmd, s.stdoutFilter, s.stderrFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.postProcess != nil {
|
||||||
|
s.postProcess(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
return exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// runViaToolRelay runs exePath through the compiled toolrelay.exe helper:
|
||||||
|
// toolrelay.exe spawns the real tool natively under Windows, redirecting
|
||||||
|
// its stdio to two named FIFOs we create and read from here. This is what
|
||||||
|
// lets `mt.exe`'s CMake-compatibility exit code translation (0x41020001 ->
|
||||||
|
// 0xbb) survive Wine's own exit-code truncation, since toolrelay.exe
|
||||||
|
// observes the real 32-bit exit code via Win32 before translating and
|
||||||
|
// re-exiting with a value that fits in a byte.
|
||||||
|
func runViaToolRelay(wineBin, relayExe, exePath string, args []string, paths *wineenv.Paths, stdoutF, stderrF lineFilter) int {
|
||||||
|
stdoutFifo := filepath.Join(os.TempDir(), fmt.Sprintf("msvc-go-wine.stdout.%d", os.Getpid()))
|
||||||
|
stderrFifo := filepath.Join(os.TempDir(), fmt.Sprintf("msvc-go-wine.stderr.%d", os.Getpid()))
|
||||||
|
os.Remove(stdoutFifo)
|
||||||
|
os.Remove(stderrFifo)
|
||||||
|
if err := syscall.Mkfifo(stdoutFifo, 0o600); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer os.Remove(stdoutFifo)
|
||||||
|
if err := syscall.Mkfifo(stderrFifo, 0o600); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer os.Remove(stderrFifo)
|
||||||
|
|
||||||
|
cmdArgs := append([]string{relayExe, exePath}, args...)
|
||||||
|
cmd := exec.Command(wineBin, cmdArgs...)
|
||||||
|
cmd.Env = append(buildEnv(paths), "MSVCGOWINE_STDOUT="+stdoutFifo, "MSVCGOWINE_STDERR="+stderrFifo)
|
||||||
|
if devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0); err == nil {
|
||||||
|
defer devNull.Close()
|
||||||
|
cmd.Stdout = devNull
|
||||||
|
cmd.Stderr = devNull
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
f, err := os.Open(stdoutFifo) // blocks until toolrelay.exe opens its end
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
pumpLines(f, os.Stdout, stdoutF)
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
f, err := os.Open(stderrFifo)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
pumpLines(f, os.Stderr, stderrF)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := cmd.Wait()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
return exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildEnv(p *wineenv.Paths) []string {
|
||||||
|
overrides := map[string]string{
|
||||||
|
"INCLUDE": p.Include,
|
||||||
|
"LIB": p.Lib,
|
||||||
|
"LIBPATH": p.LibPath,
|
||||||
|
"WINEPATH": p.WinePath,
|
||||||
|
"WINEDLLOVERRIDES": p.WineDLLOverrides,
|
||||||
|
}
|
||||||
|
base := os.Environ()
|
||||||
|
if _, set := os.LookupEnv("WINEDEBUG"); !set {
|
||||||
|
overrides["WINEDEBUG"] = "-all"
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]string, 0, len(base)+len(overrides))
|
||||||
|
for _, kv := range base {
|
||||||
|
key := kv[:strings.IndexByte(kv, '=')]
|
||||||
|
if _, skip := overrides[key]; skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, kv)
|
||||||
|
}
|
||||||
|
for k, v := range overrides {
|
||||||
|
out = append(out, k+"="+v)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// runFiltered streams stdout/stderr line by line through the tool's
|
||||||
|
// filters (CR-stripping always applied first), then waits for completion.
|
||||||
|
func runFiltered(cmd *exec.Cmd, stdoutF, stderrF lineFilter) int {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
go func() { defer wg.Done(); pumpLines(stdout, os.Stdout, stdoutF) }()
|
||||||
|
go func() { defer wg.Done(); pumpLines(stderr, os.Stderr, stderrF) }()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
return exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// pumpLines reads r line by line, CR-stripping and applying filter (if
|
||||||
|
// non-nil) before writing each line to w.
|
||||||
|
func pumpLines(r io.Reader, w *os.File, filter lineFilter) {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := stripCR(scanner.Text())
|
||||||
|
if filter != nil {
|
||||||
|
line = filter(line)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAndWait(cmd *exec.Cmd) int {
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
return exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// Package wrapper implements the per-tool command dispatch (cl, link, lib,
|
||||||
|
// ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd,
|
||||||
|
// findstr): loading each tool's environment, invoking it under Wine, and
|
||||||
|
// filtering its output.
|
||||||
|
package wrapper
|
||||||
|
|
||||||
|
import "github.com/Cheviiot/msvc-go-wine/internal/wineenv"
|
||||||
|
|
||||||
|
// dirKind selects which install directory a tool's real .exe lives in.
|
||||||
|
type dirKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
dirBin dirKind = iota // <bindir> - vc/tools/msvc/<ver>/bin/Host<host>/<arch>
|
||||||
|
dirSDK // <sdkbindir> - kits/10/bin/<sdkver>/<host>
|
||||||
|
dirMSBuild
|
||||||
|
)
|
||||||
|
|
||||||
|
type spec struct {
|
||||||
|
exeName string
|
||||||
|
dir dirKind
|
||||||
|
rawStdout bool // MSBuild: skip line filtering, inherit stdio directly
|
||||||
|
stdoutFilter lineFilter
|
||||||
|
stderrFilter lineFilter
|
||||||
|
postProcess func(origArgs []string) // cl: fix up /P /Fi<file> output
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools lists every multi-call name this binary answers to, besides its own
|
||||||
|
// management-CLI name.
|
||||||
|
var Tools = map[string]spec{
|
||||||
|
"cl": {exeName: "cl.exe", dir: dirBin, stdoutFilter: clStdoutFilter, stderrFilter: clStderrFilter, postProcess: clPostProcess},
|
||||||
|
"link": {exeName: "link.exe", dir: dirBin},
|
||||||
|
"lib": {exeName: "lib.exe", dir: dirBin},
|
||||||
|
"ml": {exeName: "ml.exe", dir: dirBin},
|
||||||
|
"ml64": {exeName: "ml64.exe", dir: dirBin},
|
||||||
|
"nmake": {exeName: "nmake.exe", dir: dirBin},
|
||||||
|
"armasm": {exeName: "armasm.exe", dir: dirBin},
|
||||||
|
"armasm64": {exeName: "armasm64.exe", dir: dirBin},
|
||||||
|
"dumpbin": {exeName: "dumpbin.exe", dir: dirBin, stdoutFilter: dumpbinStdoutFilter},
|
||||||
|
"mc": {exeName: "mc.exe", dir: dirSDK},
|
||||||
|
"midl": {exeName: "midl.exe", dir: dirSDK},
|
||||||
|
"mt": {exeName: "mt.exe", dir: dirSDK},
|
||||||
|
"rc": {exeName: "rc.exe", dir: dirSDK},
|
||||||
|
"msbuild": {exeName: "MSBuild.exe", dir: dirMSBuild, rawStdout: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
// nativeTools are handled entirely without Wine.
|
||||||
|
var nativeTools = map[string]bool{"cmd": true, "findstr": true}
|
||||||
|
|
||||||
|
func (s spec) exeDir(p *wineenv.Paths) string {
|
||||||
|
switch s.dir {
|
||||||
|
case dirSDK:
|
||||||
|
return p.SDKBinDir
|
||||||
|
case dirMSBuild:
|
||||||
|
return p.MSBuildBinDir
|
||||||
|
default:
|
||||||
|
return p.BinDir
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user