mirror of
https://github.com/Cheviiot/Vintner.git
synced 2026-08-03 15:57:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c049274626 | ||
|
|
fc20b2fb15 | ||
|
|
26f6df6a9a | ||
|
|
98ee39018a | ||
|
|
4b3aacfdf7 | ||
|
|
0b7b686e3a | ||
|
|
0a4f05c673 | ||
|
|
e0475101b2 | ||
|
|
d11b534fa1 | ||
|
|
a1743e4435 | ||
|
|
23ea620ce2 | ||
|
|
8551d7fe99 | ||
|
|
44fee57ddd | ||
|
|
47471e007c | ||
|
|
867c915596 |
@@ -0,0 +1,55 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Build, vet & test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.23"
|
||||
|
||||
- name: gofmt
|
||||
run: |
|
||||
out="$(gofmt -l .)"
|
||||
if [ -n "$out" ]; then
|
||||
echo "gofmt would reformat:"
|
||||
echo "$out"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: go vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: go build
|
||||
run: go build ./...
|
||||
|
||||
- name: go test
|
||||
run: go test ./...
|
||||
|
||||
- name: staticcheck
|
||||
uses: dominikh/staticcheck-action@9716614d4101e79b4340dd97b10e54d68234e431 # v1.4.1
|
||||
with:
|
||||
version: latest
|
||||
# Staticcheck itself needs a newer Go than the 1.23 this repo
|
||||
# targets (go.mod's floor) - let the action install its own,
|
||||
# separate from the "Set up Go" step above. min-go-version
|
||||
# defaults to reading go.mod, so diagnostics still target 1.23.
|
||||
install-go: true
|
||||
@@ -48,17 +48,17 @@ jobs:
|
||||
tag="${{ inputs.tag || github.ref_name }}"
|
||||
version="${tag#v}"
|
||||
go build -trimpath -ldflags="-s -w -X main.version=${version}" \
|
||||
-o "msvc-go-wine-linux-${{ matrix.goarch }}" ./cmd/msvc-go-wine
|
||||
-o "vintner-linux-${{ matrix.goarch }}" ./cmd/vintner
|
||||
|
||||
- name: Verify it runs
|
||||
if: matrix.goarch == 'amd64'
|
||||
run: ./msvc-go-wine-linux-amd64 version
|
||||
run: ./vintner-linux-amd64 version
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: msvc-go-wine-linux-${{ matrix.goarch }}
|
||||
path: msvc-go-wine-linux-${{ matrix.goarch }}
|
||||
name: vintner-linux-${{ matrix.goarch }}
|
||||
path: vintner-linux-${{ matrix.goarch }}
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
@@ -71,11 +71,11 @@ jobs:
|
||||
- name: Download all architectures
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: msvc-go-wine-linux-*
|
||||
pattern: vintner-linux-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Checksums
|
||||
run: sha256sum msvc-go-wine-linux-* > SHA256SUMS
|
||||
run: sha256sum vintner-linux-* > SHA256SUMS
|
||||
|
||||
- name: Create or update GitHub Release
|
||||
env:
|
||||
@@ -85,10 +85,10 @@ jobs:
|
||||
tag="${{ inputs.tag || github.ref_name }}"
|
||||
if ! gh release view "$tag" >/dev/null 2>&1; then
|
||||
gh release create "$tag" \
|
||||
--title "msvc-go-wine $tag" \
|
||||
--title "vintner $tag" \
|
||||
--notes "Linux amd64/arm64 binaries built by CI from this tag. See README.md for install instructions."
|
||||
fi
|
||||
gh release upload "$tag" \
|
||||
msvc-go-wine-linux-* \
|
||||
vintner-linux-* \
|
||||
SHA256SUMS \
|
||||
--clobber
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
/msvc-go-wine
|
||||
/vintner
|
||||
*.test
|
||||
.claude/
|
||||
|
||||
+2
-2
@@ -20,6 +20,6 @@ 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`
|
||||
This license covers vintner's own source code. It does not cover the
|
||||
Microsoft Visual C++ Build Tools / Windows SDK that `vintner download`
|
||||
fetches and unpacks, which remain governed by Microsoft's own license terms.
|
||||
|
||||
@@ -1,129 +1,219 @@
|
||||
# msvc-go-wine
|
||||
# vintner
|
||||
|
||||
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.
|
||||
[](https://github.com/Cheviiot/vintner/actions/workflows/ci.yml)
|
||||
[](https://github.com/Cheviiot/vintner/releases/latest)
|
||||
[](LICENSE.txt)
|
||||
|
||||
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`.
|
||||
vintner cross-compiles with the real MSVC toolchain on Linux, using Wine.
|
||||
One Go binary drops in as `cl`, `link`, `lib`, `rc`, `midl`, `mc`, `mt`,
|
||||
`dumpbin`, `msbuild`, `nmake`, `ml`, `ml64`, `armasm`, `armasm64`, plus
|
||||
`cmd`/`findstr` shims, so once installed you invoke the real Microsoft
|
||||
tools exactly like on Windows. It handles full MSBuild projects, and with
|
||||
`--with-wdk`, real KMDF/UMDF Windows drivers.
|
||||
|
||||
Inspired by [mstorsjo/msvc-wine](https://github.com/mstorsjo/msvc-wine)'s
|
||||
approach: download the real MSVC/WinSDK, wrap the compiler under Wine.
|
||||
|
||||
## Contents
|
||||
|
||||
- [How it works](#how-it-works)
|
||||
- [Installation](#installation)
|
||||
- [Quick start](#quick-start)
|
||||
- [Commands](#commands)
|
||||
- [Building drivers (WDK)](#building-drivers-wdk)
|
||||
- [Language](#language)
|
||||
- [Shell completion](#shell-completion)
|
||||
- [Using clang-cl/lld-link instead of Wine](#using-clang-cllld-link-instead-of-wine)
|
||||
- [toolrelay.exe](#toolrelayexe)
|
||||
- [Compatibility patches](#compatibility-patches)
|
||||
- [Building from source](#building-from-source)
|
||||
- [License](#license)
|
||||
|
||||
## 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):
|
||||
vintner is a multi-call binary, like busybox: it behaves differently
|
||||
depending on the name it's invoked as.
|
||||
|
||||
- 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
|
||||
- As `cl`, `link`, `lib`, and the rest: it loads a per-architecture
|
||||
`env.json`, sets `INCLUDE`/`LIB`/`WINEPATH`, and rewrites absolute Unix
|
||||
paths in the arguments to Wine's `z:\...` form (Wine and cl.exe
|
||||
otherwise mishandle relative includes — see
|
||||
[winehq bug 55200](https://bugs.winehq.org/show_bug.cgi?id=55200)). It
|
||||
then runs the real `.exe` under `wine`/`wine64`, and rewrites `z:\...`
|
||||
paths back to Unix paths in the output, 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.
|
||||
- As `vintner`: it exposes the `download`, `install`, `env`, `version`
|
||||
and `completion` subcommands below (short aliases: `dl`, `i`, `e`, `v`;
|
||||
`help`/`h` prints usage).
|
||||
|
||||
## Quick start
|
||||
## Installation
|
||||
|
||||
On ALT Linux, via [Nivora](https://github.com/Cheviiot/Nivora):
|
||||
|
||||
```bash
|
||||
# 1. Download and unpack MSVC + Windows SDK into ~/.msvc-go-wine (requires
|
||||
# accepting Microsoft's Visual Studio Build Tools license, and msitools
|
||||
# for unpacking .msi payloads). Pass --dest <dir> for a different location.
|
||||
msvc-go-wine download --accept-license
|
||||
|
||||
# 2. Wire up the tool wrappers
|
||||
msvc-go-wine install
|
||||
|
||||
# 3. Add the toolchain to PATH and build
|
||||
export PATH=~/.msvc-go-wine/bin/x64:$PATH
|
||||
cl /nologo /EHsc hello.cpp
|
||||
stplr install nivora/vintner
|
||||
```
|
||||
|
||||
Prebuilt binary, from the [latest release](https://github.com/Cheviiot/vintner/releases/latest):
|
||||
|
||||
```bash
|
||||
curl -fLo vintner "https://github.com/Cheviiot/vintner/releases/latest/download/vintner-linux-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"
|
||||
chmod +x vintner
|
||||
sudo install vintner /usr/local/bin/vintner
|
||||
```
|
||||
|
||||
From source: see [Building from source](#building-from-source).
|
||||
|
||||
Either way, `wine`/`wine64`, `msitools` (for `msiextract`) and `git` need
|
||||
to be on `PATH` at run time. Nivora installs pull these in automatically
|
||||
as package dependencies.
|
||||
|
||||
### 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).
|
||||
- `git` — applies the compatibility patches bundled with `download` (see
|
||||
[Compatibility patches](#compatibility-patches)).
|
||||
|
||||
On ALT Linux:
|
||||
|
||||
```bash
|
||||
pkcon install wine msitools
|
||||
pkcon install wine msitools git
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# 1. Download and unpack MSVC + Windows SDK into ~/.vintner (accepts
|
||||
# Microsoft's Visual Studio Build Tools license). Pass --dest <dir>
|
||||
# for a different location.
|
||||
vintner download --accept-license
|
||||
|
||||
# 2. Wire up the tool wrappers
|
||||
vintner install
|
||||
|
||||
# 3. Add the toolchain to PATH and build
|
||||
export PATH=~/.vintner/bin/x64:$PATH
|
||||
cl /nologo /EHsc hello.cpp
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
```
|
||||
msvc-go-wine download --accept-license [--dest <dir>] [options] fetch and unpack MSVC/WinSDK
|
||||
msvc-go-wine install [dir] wire up wrappers for a downloaded MSVC
|
||||
msvc-go-wine env --bin <dir>/bin/<arch> print INCLUDE/LIB for native clang-cl/lld-link use
|
||||
msvc-go-wine version print the version
|
||||
vintner download (dl) --accept-license [--dest <dir>] [options] fetch and unpack MSVC/WinSDK/WDK
|
||||
vintner install (i) [dir] wire up wrappers for a downloaded MSVC
|
||||
vintner env (e) --bin <dir>/bin/<arch> print INCLUDE/LIB for native clang-cl/lld-link use
|
||||
vintner version (v) print the version
|
||||
vintner help (h) print usage
|
||||
vintner completion bash|zsh print a shell completion script
|
||||
```
|
||||
|
||||
`--dest`/`[dir]` both default to `~/.msvc-go-wine` when omitted.
|
||||
`--dest`/`[dir]` both default to `~/.vintner` when omitted.
|
||||
|
||||
`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.
|
||||
`download`'s main options: `--msvc-version`, `--sdk-version`,
|
||||
`--architecture` (repeatable: `x86`/`x64`/`arm`/`arm64`/`host`),
|
||||
`--host-arch`, `--only-host`, `--with-wdk` (see below), `--ignore`
|
||||
(repeatable), `--only-download`, `--only-unpack`, `--keep-unpack`,
|
||||
`--skip-patch`, `--cache`, `--language`, `--include-optional`,
|
||||
`--skip-recommended`, `--major`, `--preview`, `--manifest`,
|
||||
`--list-workloads`, `--list-components`, `--print-deps-tree`. Run
|
||||
`vintner download -h` for the full list with descriptions.
|
||||
|
||||
### Using clang-cl/lld-link instead of Wine
|
||||
`--list-workloads`/`--list-components` print every workload/component id
|
||||
and its human-readable title from the fetched manifest, then exit
|
||||
without downloading anything. Useful for finding what to pass as a bare
|
||||
package id or through `--with-*`. `--print-deps-tree` prints the
|
||||
dependency tree of whatever would actually be selected — honoring every
|
||||
other flag — without downloading anything.
|
||||
|
||||
You don't need Wine at all if you drive the (nonredistributable) MSVC/WinSDK
|
||||
headers and libraries with Clang/LLD in MSVC-compatible mode:
|
||||
## Building drivers (WDK)
|
||||
|
||||
`--with-wdk` also fetches the Windows Driver Kit: headers, import libs,
|
||||
and the MSBuild `WindowsKernelModeDriver10.0`/`WindowsUserModeDriver10.0`
|
||||
PlatformToolsets.
|
||||
|
||||
```bash
|
||||
eval "$(msvc-go-wine env --bin ~/.msvc-go-wine/bin/x64)"
|
||||
vintner download --accept-license --with-wdk
|
||||
```
|
||||
|
||||
With it, `msbuild` builds real KMDF/UMDF drivers — compiling, linking,
|
||||
INF stamping, and the `Inf2Cat` signability check (`SignMode=off`) all
|
||||
work under Wine. Tested against a real sample driver from
|
||||
[microsoft/Windows-driver-samples](https://github.com/microsoft/Windows-driver-samples).
|
||||
Only x64 and arm64 targets have a WDK package upstream; there's no x86 or
|
||||
arm one.
|
||||
|
||||
## Language
|
||||
|
||||
CLI text (usage, progress lines, prompts) defaults to English. Set
|
||||
`VINTNER_LANG=ru` (or a `ru`-prefixed `LC_ALL`/`LC_MESSAGES`/`LANG`, e.g.
|
||||
`ru_RU.UTF-8`) for Russian:
|
||||
|
||||
```bash
|
||||
VINTNER_LANG=ru vintner help
|
||||
```
|
||||
|
||||
Error text from internal packages stays in English regardless.
|
||||
|
||||
## Shell completion
|
||||
|
||||
```bash
|
||||
source <(vintner completion bash) # or add to ~/.bashrc
|
||||
source <(vintner completion zsh) # or add to ~/.zshrc
|
||||
```
|
||||
|
||||
Completes subcommands, including the short aliases, `download`'s flags,
|
||||
and directory arguments for `install`/`env --bin`.
|
||||
|
||||
## Using clang-cl/lld-link instead of Wine
|
||||
|
||||
The MSVC/WinSDK headers and libraries work directly with Clang/LLD in
|
||||
MSVC-compatible mode. No Wine needed:
|
||||
|
||||
```bash
|
||||
eval "$(vintner env --bin ~/.vintner/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.
|
||||
`install` compiles `assets/vendor/toolrelay.cpp`, a small native Windows
|
||||
launcher, with the freshly-installed host-arch `cl.exe`. This is
|
||||
best-effort: if `wine` isn't available yet, or the compile fails, install
|
||||
still succeeds, and tool invocations just skip it. When present, every
|
||||
non-MSBuild tool call is routed through it via two named FIFOs.
|
||||
|
||||
That's what lets `mt.exe`'s CMake-compatibility exit code
|
||||
(`0x41020001` → `0xbb`) survive Wine's own exit-code truncation: a native
|
||||
Windows process can read the real 32-bit exit code via
|
||||
`GetExitCodeProcess()` before Wine collapses it to a single byte on the
|
||||
way back to Unix.
|
||||
|
||||
## 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.
|
||||
`download` applies a few small patches (`assets/patches`) to the
|
||||
downloaded MSVC/WinSDK tree, so `VsDevCmd.bat` and MSBuild's
|
||||
SDK-detection props work without a Windows Registry, which doesn't exist
|
||||
under Wine. They look up the SDK directly under the VS install root
|
||||
instead of querying the registry, skip telemetry, and don't fail devcmd
|
||||
setup when an optional component (ConnectionManagerExe, bundled
|
||||
CMake/Ninja) is missing.
|
||||
|
||||
## Known gaps
|
||||
## Building from source
|
||||
|
||||
- `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.
|
||||
```bash
|
||||
go build -o vintner ./cmd/vintner
|
||||
go vet ./...
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Go 1.23+ builds it. `wine`/`msitools` are only needed at run time, for
|
||||
`install`/tool invocation and `download` respectively.
|
||||
|
||||
## 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
|
||||
MIT (see [LICENSE.txt](LICENSE.txt)) for vintner's own source. The MSVC
|
||||
Build Tools, Windows SDK, and WDK that `download` fetches stay under
|
||||
Microsoft's own license (accepted via `--accept-license`), same as with
|
||||
any other way of obtaining them.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
msvc-go-wine: skip VsDevCmd's telemetry upload.
|
||||
vintner: 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
msvc-go-wine: always record the starting directory.
|
||||
vintner: 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
msvc-go-wine: find the Windows SDK/UCRT dirs on disk before trying the registry.
|
||||
vintner: 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
msvc-go-wine: don't hard-fail devcmd setup when ConnectionManagerExe is missing.
|
||||
vintner: 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
msvc-go-wine: don't hard-fail devcmd setup when the bundled CMake/Ninja is missing.
|
||||
vintner: 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.
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
msvc-go-wine: make MSBuild resolve the Windows SDK / UCRT / preferred tool
|
||||
vintner: 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.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
msvc-go-wine: let MSBuild find the Windows SDK without the registry.
|
||||
vintner: 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
|
||||
roots (populated by vintner.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
|
||||
|
||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
||||
// toolrelay.exe - a thin native launcher for a Wine-hosted MSVC tool
|
||||
// invocation, built and used by msvc-go-wine (see internal/install's
|
||||
// invocation, built and used by vintner (see internal/install's
|
||||
// buildToolRelay and internal/wrapper's runViaToolRelay).
|
||||
//
|
||||
// It exists to solve two problems that are only solvable from inside a
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// version is set at build time via -ldflags "-X main.version=X.Y.Z";
|
||||
// left as "dev" for plain `go build`/`go run`.
|
||||
var version = "dev"
|
||||
|
||||
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 " + version)
|
||||
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 --accept-license [--dest <dir>] [options]
|
||||
fetch and unpack MSVC/WinSDK
|
||||
msvc-go-wine install [dir] wire up wrappers for a downloaded MSVC
|
||||
msvc-go-wine env --bin <dir/bin/arch> print INCLUDE/LIB for native clang-cl/lld-link use
|
||||
msvc-go-wine version print the version
|
||||
|
||||
--dest/[dir] default to ~/.msvc-go-wine if omitted.
|
||||
|
||||
Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
|
||||
cl, link, lib, ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd, findstr
|
||||
`)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
// runCompletion prints a shell completion script for shell ("bash" or
|
||||
// "zsh") to stdout, meant to be sourced directly:
|
||||
//
|
||||
// source <(vintner completion bash) # or add to ~/.bashrc
|
||||
// source <(vintner completion zsh) # or add to ~/.zshrc
|
||||
//
|
||||
// The flag lists below are hand-maintained alongside download.go/env.go's
|
||||
// flag.FlagSet definitions rather than generated from them - there's no
|
||||
// reflection-friendly registry to walk, and the flag set rarely changes.
|
||||
func runCompletion(args []string) int {
|
||||
if len(args) != 1 {
|
||||
fmt.Println("usage: vintner completion bash|zsh")
|
||||
return 1
|
||||
}
|
||||
switch args[0] {
|
||||
case "bash":
|
||||
fmt.Print(bashCompletionScript)
|
||||
return 0
|
||||
case "zsh":
|
||||
fmt.Print(zshCompletionScript)
|
||||
return 0
|
||||
default:
|
||||
fmt.Printf("vintner completion: unsupported shell %q (want bash or zsh)\n", args[0])
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
const downloadFlags = "--dest --cache --major --preview --manifest --accept-license " +
|
||||
"--msvc-version --sdk-version --host-arch --only-host --language " +
|
||||
"--include-optional --skip-recommended --only-download --only-unpack " +
|
||||
"--keep-unpack --skip-patch --list-workloads --list-components " +
|
||||
"--print-deps-tree --with-wdk --architecture --ignore -h --help"
|
||||
|
||||
var bashCompletionScript = `# vintner bash completion - eval "$(vintner completion bash)"
|
||||
_vintner_complete() {
|
||||
local cur cmd
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
cmd="${COMP_WORDS[1]}"
|
||||
|
||||
if [ "$COMP_CWORD" -eq 1 ]; then
|
||||
COMPREPLY=($(compgen -W "download dl install i env e version v help h completion" -- "$cur"))
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$cmd" in
|
||||
download|dl)
|
||||
COMPREPLY=($(compgen -W "` + downloadFlags + `" -- "$cur"))
|
||||
;;
|
||||
install|i)
|
||||
COMPREPLY=($(compgen -d -- "$cur"))
|
||||
;;
|
||||
env|e)
|
||||
COMPREPLY=($(compgen -W "--bin -h --help" -- "$cur"))
|
||||
;;
|
||||
completion)
|
||||
COMPREPLY=($(compgen -W "bash zsh" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
complete -F _vintner_complete vintner
|
||||
`
|
||||
|
||||
var zshCompletionScript = `#compdef vintner
|
||||
# vintner zsh completion - source <(vintner completion zsh)
|
||||
|
||||
_vintner() {
|
||||
local -a subcommands
|
||||
subcommands=(
|
||||
'download:fetch and unpack MSVC/WinSDK/WDK'
|
||||
'dl:alias for download'
|
||||
'install:wire up wrappers for a downloaded MSVC'
|
||||
'i:alias for install'
|
||||
'env:print INCLUDE/LIB for native clang-cl/lld-link use'
|
||||
'e:alias for env'
|
||||
'version:print the version'
|
||||
'v:alias for version'
|
||||
'help:print usage'
|
||||
'h:alias for help'
|
||||
'completion:print a shell completion script'
|
||||
)
|
||||
|
||||
if (( CURRENT == 2 )); then
|
||||
_describe 'command' subcommands
|
||||
return
|
||||
fi
|
||||
|
||||
case "${words[2]}" in
|
||||
download|dl)
|
||||
local -a flags
|
||||
flags=(
|
||||
'--dest[directory to install into]:directory:_files -/'
|
||||
'--cache[persistent download cache directory]:directory:_files -/'
|
||||
'--major[major VS version]:version:'
|
||||
'--preview[use the preview/insiders channel]'
|
||||
'--manifest[use a predownloaded installer manifest file]:file:_files'
|
||||
'--accept-license[do not prompt for accepting the license]'
|
||||
'--msvc-version[install a specific MSVC toolchain version]:version:'
|
||||
'--sdk-version[install a specific Windows SDK version]:version:'
|
||||
'--host-arch[host architecture]:arch:(x86 x64 arm64)'
|
||||
'--only-host[only download packages matching the host architecture]'
|
||||
'--language[preferred package language]:language:'
|
||||
'--include-optional[include all optional dependencies]'
|
||||
'--skip-recommended[skip recommended dependencies]'
|
||||
'--only-download[stop after downloading package files]'
|
||||
'--only-unpack[unpack without pruning to just the CLI tools]'
|
||||
'--keep-unpack[keep the scratch unpack dir]'
|
||||
'--skip-patch[do not apply the Wine compatibility patches]'
|
||||
'--list-workloads[list available workloads and exit]'
|
||||
'--list-components[list available components and exit]'
|
||||
'--print-deps-tree[print the dependency tree and exit]'
|
||||
'--with-wdk[also fetch the Windows Driver Kit]'
|
||||
'--architecture[target architecture]:arch:(x86 x64 arm arm64 host)'
|
||||
'--ignore[package id to skip]:package id:'
|
||||
'-h[show help]'
|
||||
'--help[show help]'
|
||||
)
|
||||
_arguments $flags
|
||||
;;
|
||||
install|i)
|
||||
_files -/
|
||||
;;
|
||||
env|e)
|
||||
_arguments \
|
||||
'--bin[bin/<arch> directory produced by install]:directory:_files -/' \
|
||||
'-h[show help]' '--help[show help]'
|
||||
;;
|
||||
completion)
|
||||
_values 'shell' bash zsh
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_vintner "$@"
|
||||
`
|
||||
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCompletionScriptsAreSyntacticallyValid catches the easy way to break
|
||||
// these: a typo in the hand-maintained flag lists that produces invalid
|
||||
// shell syntax. It shells out to bash/zsh -n rather than parsing the script
|
||||
// itself, so it's testing exactly what a user's shell would see.
|
||||
func TestCompletionScriptsAreSyntacticallyValid(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
shell string
|
||||
script string
|
||||
}{
|
||||
{"bash", bashCompletionScript},
|
||||
{"zsh", zshCompletionScript},
|
||||
} {
|
||||
t.Run(tc.shell, func(t *testing.T) {
|
||||
if _, err := exec.LookPath(tc.shell); err != nil {
|
||||
t.Skipf("%s not installed", tc.shell)
|
||||
}
|
||||
cmd := exec.Command(tc.shell, "-n", "/dev/stdin")
|
||||
cmd.Stdin = strings.NewReader(tc.script)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("%s -n rejected the completion script: %v\n%s", tc.shell, err, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCompletionUnknownShell(t *testing.T) {
|
||||
if code := runCompletion([]string{"fish"}); code != 1 {
|
||||
t.Errorf("runCompletion([\"fish\"]) = %d, want 1", code)
|
||||
}
|
||||
if code := runCompletion(nil); code != 1 {
|
||||
t.Errorf("runCompletion(nil) = %d, want 1", code)
|
||||
}
|
||||
if code := runCompletion([]string{"bash", "extra"}); code != 1 {
|
||||
t.Errorf("runCompletion with extra arg = %d, want 1", code)
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,13 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/internal/download"
|
||||
"github.com/Cheviiot/vintner/internal/download"
|
||||
"github.com/Cheviiot/vintner/internal/i18n"
|
||||
)
|
||||
|
||||
func runDownload(args []string) int {
|
||||
fs := flag.NewFlagSet("download", flag.ContinueOnError)
|
||||
dest := fs.String("dest", "", "directory to install into (default: ~/.msvc-go-wine)")
|
||||
dest := fs.String("dest", "", "directory to install into (default: ~/.vintner)")
|
||||
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")
|
||||
@@ -30,6 +31,10 @@ func runDownload(args []string) int {
|
||||
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")
|
||||
listWorkloads := fs.Bool("list-workloads", false, "list available workloads from the manifest and exit, without downloading anything")
|
||||
listComponents := fs.Bool("list-components", false, "list available components from the manifest and exit, without downloading anything")
|
||||
printDepsTree := fs.Bool("print-deps-tree", false, "print the dependency tree of the selected packages and exit, without downloading anything")
|
||||
withWDK := fs.Bool("with-wdk", false, "also fetch and install the Windows Driver Kit (headers, libs and MSBuild driver PlatformToolsets, for building KMDF/UMDF drivers)")
|
||||
var archsFlag stringList
|
||||
fs.Var(&archsFlag, "architecture", "target architecture to include (x86, x64, arm, arm64, host); repeatable")
|
||||
var ignoreFlag stringList
|
||||
@@ -40,6 +45,17 @@ func runDownload(args []string) int {
|
||||
}
|
||||
packages := fs.Args()
|
||||
|
||||
for _, a := range archsFlag {
|
||||
if !validArchitectures[a] {
|
||||
fmt.Fprintf(os.Stderr, "vintner download: invalid --architecture %q (expected one of x86, x64, arm, arm64, host)\n", a)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
if *hostArch != "" && !validHostArchs[*hostArch] {
|
||||
fmt.Fprintf(os.Stderr, "vintner download: invalid --host-arch %q (expected one of x86, x64, arm64)\n", *hostArch)
|
||||
return 2
|
||||
}
|
||||
|
||||
opts := &download.Options{
|
||||
Package: packages,
|
||||
Ignore: []string(ignoreFlag),
|
||||
@@ -51,13 +67,14 @@ func runDownload(args []string) int {
|
||||
IncludeOptional: *includeOptional,
|
||||
SkipRecommended: *skipRecommended,
|
||||
Language: *language,
|
||||
WithWDK: *withWDK,
|
||||
}
|
||||
|
||||
manifestURL := *manifestFile
|
||||
if manifestURL == "" {
|
||||
url, err := download.FetchChannelManifest(*major, *preview)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
manifestURL = url
|
||||
@@ -67,18 +84,28 @@ func runDownload(args []string) int {
|
||||
|
||||
manifest, err := download.FetchInstallerManifest(manifestURL)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if opts.HostArch == "" {
|
||||
opts.HostArch = detectHostArch()
|
||||
}
|
||||
fmt.Println("Install packages for", opts.HostArch, "host architecture")
|
||||
fmt.Println(i18n.T("download.host_arch", opts.HostArch))
|
||||
|
||||
idx := download.BuildIndex(manifest, opts.HostArch, opts.Language)
|
||||
|
||||
if !*acceptLicense {
|
||||
if *listWorkloads || *listComponents {
|
||||
if *listWorkloads {
|
||||
printPackageList("download.workloads_header", download.PackagesByType(idx, "Workload"), opts.Language)
|
||||
}
|
||||
if *listComponents {
|
||||
printPackageList("download.components_header", download.PackagesByType(idx, "Component"), opts.Language)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if !*acceptLicense && !*printDepsTree {
|
||||
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
|
||||
@@ -89,13 +116,18 @@ func runDownload(args []string) int {
|
||||
}
|
||||
|
||||
if err := download.ResolveSelection(opts, idx); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if *printDepsTree {
|
||||
download.PrintDependencyTree(os.Stdout, idx, opts)
|
||||
return 0
|
||||
}
|
||||
|
||||
selected, err := download.ExpandSelection(idx, opts)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
var downloadSize, installSize int64
|
||||
@@ -103,15 +135,15 @@ func runDownload(args []string) int {
|
||||
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))
|
||||
fmt.Print(i18n.T("download.selected",
|
||||
len(selected), download.HumanizeBytes(downloadSize), download.HumanizeBytes(installSize)))
|
||||
|
||||
cache := *cacheDir
|
||||
removeCache := false
|
||||
if cache == "" {
|
||||
tmp, err := os.MkdirTemp("", "msvc-go-wine-cache-")
|
||||
tmp, err := os.MkdirTemp("", "vintner-cache-")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
cache = tmp
|
||||
@@ -124,15 +156,15 @@ func runDownload(args []string) int {
|
||||
if !*onlyDownload && *dest == "" {
|
||||
def, err := defaultToolchainDir()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
*dest = def
|
||||
fmt.Println("--dest not set, using default:", *dest)
|
||||
fmt.Println(i18n.T("download.default_dest", *dest))
|
||||
}
|
||||
|
||||
if err := download.FetchPayloads(selected, cache, *onlyDownload); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
if *onlyDownload {
|
||||
@@ -141,7 +173,7 @@ func runDownload(args []string) int {
|
||||
|
||||
destAbs, err := filepath.Abs(*dest)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -150,7 +182,7 @@ func runDownload(args []string) int {
|
||||
unpack = filepath.Join(destAbs, "unpack")
|
||||
}
|
||||
if err := download.UnpackSelectedPackages(selected, cache, unpack); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -159,14 +191,14 @@ func runDownload(args []string) int {
|
||||
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)
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
if !*onlyUnpack {
|
||||
if err := download.RelocateBuildTools(unpack, destAbs); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
if !*keepUnpack {
|
||||
@@ -174,16 +206,81 @@ func runDownload(args []string) int {
|
||||
}
|
||||
if !*skipPatch && *major == 18 {
|
||||
if err := download.ApplyCompatibilityFixes(destAbs); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Done. Next: msvc-go-wine install", destAbs)
|
||||
if opts.WithWDK && !*onlyUnpack {
|
||||
if err := downloadWDK(opts, selected, cache, destAbs, *major); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println(i18n.T("download.done", destAbs))
|
||||
return 0
|
||||
}
|
||||
|
||||
// downloadWDK fetches the WDK NuGet package(s) matching opts.Architecture
|
||||
// (only x64 and arm64 have one - there's no WDK package for x86/arm
|
||||
// targets) into destAbs/wdk/<arch>, preferring a version matching the
|
||||
// Windows SDK actually selected. See wdk.go for why this is a separate
|
||||
// download path from the rest of ExpandSelection/FetchPayloads/Unpack.
|
||||
func downloadWDK(opts *download.Options, selected []*download.Package, cache, destAbs string, major int) error {
|
||||
sdkBuild := download.SDKBuildPrefix(selected)
|
||||
vsVersion := fmt.Sprintf("%d.0", major)
|
||||
var archs []string
|
||||
for _, a := range []string{"x64", "arm64"} {
|
||||
if contains(opts.Architecture, a) {
|
||||
archs = append(archs, a)
|
||||
}
|
||||
}
|
||||
if len(archs) == 0 {
|
||||
fmt.Println(i18n.T("download.wdk_skip"))
|
||||
return nil
|
||||
}
|
||||
for _, arch := range archs {
|
||||
version, err := download.FetchLatestWDKVersion(arch, sdkBuild)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wdkDir, err := download.DownloadWDK(arch, version, cache, destAbs, vsVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Print(i18n.T("download.wdk_installed", arch, version, wdkDir))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func contains(list []string, v string) bool {
|
||||
for _, s := range list {
|
||||
if s == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// printPackageList prints one line per package: its ID, and (when the
|
||||
// manifest carries one) its human-readable title in the requested language.
|
||||
// headerKey is an i18n catalog key taking the package count as its one arg.
|
||||
func printPackageList(headerKey string, pkgs []*download.Package, language string) {
|
||||
fmt.Print(i18n.T(headerKey, len(pkgs)))
|
||||
for _, p := range pkgs {
|
||||
if lr := p.Localized(language); lr != nil && lr.Title != "" {
|
||||
fmt.Printf(" %-65s %s\n", p.ID, lr.Title)
|
||||
} else {
|
||||
fmt.Printf(" %s\n", p.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var validArchitectures = map[string]bool{"x86": true, "x64": true, "arm": true, "arm64": true, "host": true}
|
||||
var validHostArchs = map[string]bool{"x86": true, "x64": true, "arm64": true}
|
||||
|
||||
func detectHostArch() string {
|
||||
if runtime.GOARCH == "arm64" {
|
||||
return "arm64"
|
||||
@@ -192,7 +289,7 @@ func detectHostArch() string {
|
||||
}
|
||||
|
||||
func promptAcceptLicense(license string) bool {
|
||||
fmt.Printf("Do you accept the license at %s (yes/no)? ", license)
|
||||
fmt.Print(i18n.T("download.license_prompt", license))
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
switch scanner.Text() {
|
||||
@@ -201,7 +298,7 @@ func promptAcceptLicense(license string) bool {
|
||||
case "no":
|
||||
return false
|
||||
}
|
||||
fmt.Print("Do you accept the license? Answer \"yes\" or \"no\": ")
|
||||
fmt.Print(i18n.T("download.license_reprompt"))
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestRunDownloadRejectsInvalidArchFlags exercises the validation added
|
||||
// after the flags are parsed, which must reject typos before runDownload
|
||||
// gets anywhere near the network (FetchChannelManifest) - these tests would
|
||||
// hang/fail on network access if that ordering ever regressed.
|
||||
func TestRunDownloadRejectsInvalidArchFlags(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"bad architecture", []string{"--architecture", "x866"}},
|
||||
{"bad architecture, valid mixed with invalid", []string{"--architecture", "x64", "--architecture", "sparc"}},
|
||||
{"bad host-arch", []string{"--host-arch", "sparc"}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if code := runDownload(tc.args); code != 2 {
|
||||
t.Errorf("runDownload(%v) = %d, want 2", tc.args, code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidArchitectureSets(t *testing.T) {
|
||||
for _, a := range []string{"x86", "x64", "arm", "arm64", "host"} {
|
||||
if !validArchitectures[a] {
|
||||
t.Errorf("validArchitectures[%q] = false, want true", a)
|
||||
}
|
||||
}
|
||||
for _, a := range []string{"x86", "x64", "arm64"} {
|
||||
if !validHostArchs[a] {
|
||||
t.Errorf("validHostArchs[%q] = false, want true", a)
|
||||
}
|
||||
}
|
||||
if validHostArchs["arm"] {
|
||||
t.Error(`validHostArchs["arm"] = true, want false (no 32-bit ARM host toolchain exists)`)
|
||||
}
|
||||
}
|
||||
@@ -6,39 +6,40 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/internal/wineenv"
|
||||
"github.com/Cheviiot/vintner/internal/i18n"
|
||||
"github.com/Cheviiot/vintner/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>)"
|
||||
// Usage: eval "$(vintner 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`")
|
||||
bin := fs.String("bin", "", "the <dest>/bin/<arch> directory produced by `vintner 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>")
|
||||
fmt.Fprintln(os.Stderr, i18n.T("env.usage"))
|
||||
return 1
|
||||
}
|
||||
|
||||
cfg, err := wineenv.Load(*bin)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine env:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner env:", err)
|
||||
return 1
|
||||
}
|
||||
baseUnix, err := wineenv.FindBaseUnix(*bin)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine env:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner 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)
|
||||
fmt.Fprint(os.Stderr, i18n.T("env.unknown_arch", cfg.Arch))
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRunEnvRequiresBin(t *testing.T) {
|
||||
if code := runEnv(nil); code != 1 {
|
||||
t.Errorf("runEnv(nil) = %d, want 1", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunEnvRejectsMissingBinDir(t *testing.T) {
|
||||
if code := runEnv([]string{"--bin", "/nonexistent/path/for/vintner/tests"}); code != 1 {
|
||||
t.Errorf("runEnv with a nonexistent --bin = %d, want 1", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToUnixPathList(t *testing.T) {
|
||||
got := toUnixPathList(`z:\vc\include;z:\kits\10\include`)
|
||||
want := "/vc/include;/kits/10/include"
|
||||
if got != want {
|
||||
t.Errorf("toUnixPathList(...) = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/internal/install"
|
||||
"github.com/Cheviiot/vintner/internal/i18n"
|
||||
"github.com/Cheviiot/vintner/internal/install"
|
||||
)
|
||||
|
||||
func runInstall(args []string) int {
|
||||
if len(args) > 1 || (len(args) == 1 && (args[0] == "-h" || args[0] == "--help")) {
|
||||
fmt.Fprintln(os.Stderr, "usage: msvc-go-wine install [dest] (default: ~/.msvc-go-wine)")
|
||||
fmt.Fprintln(os.Stderr, i18n.T("install.usage"))
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -19,23 +20,23 @@ func runInstall(args []string) int {
|
||||
} else {
|
||||
def, err := defaultToolchainDir()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine install:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner install:", err)
|
||||
return 1
|
||||
}
|
||||
dest = def
|
||||
fmt.Println("No directory given, using default:", dest)
|
||||
fmt.Println(i18n.T("install.default_dir", dest))
|
||||
}
|
||||
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine install:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner install:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := install.Install(dest, self); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine install:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner install:", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Println("Done. Add", dest+"/bin/<arch> to PATH to use cl, link, lib, ...")
|
||||
fmt.Println(i18n.T("install.done", dest+"/bin/<arch>"))
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRunInstallRejectsExtraArgs(t *testing.T) {
|
||||
if code := runInstall([]string{"one", "two"}); code != 1 {
|
||||
t.Errorf("runInstall with two args = %d, want 1", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunInstallHelp(t *testing.T) {
|
||||
for _, flag := range []string{"-h", "--help"} {
|
||||
if code := runInstall([]string{flag}); code != 1 {
|
||||
t.Errorf("runInstall([%q]) = %d, want 1", flag, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Command vintner 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 `vintner install`), and
|
||||
// otherwise exposes the `download`/`install`/`env`/`version` management
|
||||
// subcommands.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Cheviiot/vintner/internal/i18n"
|
||||
"github.com/Cheviiot/vintner/internal/wrapper"
|
||||
)
|
||||
|
||||
// version is set at build time via -ldflags "-X main.version=X.Y.Z";
|
||||
// left as "dev" for plain `go build`/`go run`.
|
||||
var version = "dev"
|
||||
|
||||
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", "dl":
|
||||
return runDownload(args[1:])
|
||||
case "install", "i":
|
||||
return runInstall(args[1:])
|
||||
case "env", "e":
|
||||
return runEnv(args[1:])
|
||||
case "completion":
|
||||
return runCompletion(args[1:])
|
||||
case "version", "v", "--version":
|
||||
fmt.Println("vintner " + version)
|
||||
return 0
|
||||
case "-h", "--help", "help", "h":
|
||||
printUsage()
|
||||
return 0
|
||||
default:
|
||||
fmt.Fprint(os.Stderr, i18n.T("main.unknown_subcommand", args[0]))
|
||||
printUsage()
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Fprint(os.Stderr, i18n.T("main.usage"))
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestRunCLIDispatch covers the subset of runCLI's switch that has no side
|
||||
// effects (no filesystem/network touched) - the actual subcommand bodies
|
||||
// (download/install/env) get their own focused tests.
|
||||
func TestRunCLIDispatch(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
want int
|
||||
}{
|
||||
{"no args prints usage", nil, 1},
|
||||
{"unknown subcommand", []string{"frobnicate"}, 1},
|
||||
{"help long", []string{"--help"}, 0},
|
||||
{"help short flag", []string{"-h"}, 0},
|
||||
{"help word", []string{"help"}, 0},
|
||||
{"help alias", []string{"h"}, 0},
|
||||
{"version word", []string{"version"}, 0},
|
||||
{"version alias", []string{"v"}, 0},
|
||||
{"version flag", []string{"--version"}, 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := runCLI(tc.args); got != tc.want {
|
||||
t.Errorf("runCLI(%v) = %d, want %d", tc.args, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,12 @@ import (
|
||||
)
|
||||
|
||||
// defaultToolchainDir is where `download`/`install` operate when the user
|
||||
// doesn't specify a directory: a hidden ~/.msvc-go-wine, so it doesn't
|
||||
// doesn't specify a directory: a hidden ~/.vintner, so it doesn't
|
||||
// clutter a plain `ls ~`.
|
||||
func defaultToolchainDir() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, ".msvc-go-wine"), nil
|
||||
return filepath.Join(home, ".vintner"), nil
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultToolchainDir(t *testing.T) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Skipf("no home directory available: %v", err)
|
||||
}
|
||||
got, err := defaultToolchainDir()
|
||||
if err != nil {
|
||||
t.Fatalf("defaultToolchainDir() error: %v", err)
|
||||
}
|
||||
want := filepath.Join(home, ".vintner")
|
||||
if got != want {
|
||||
t.Errorf("defaultToolchainDir() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
module github.com/Cheviiot/msvc-go-wine
|
||||
module github.com/Cheviiot/vintner
|
||||
|
||||
go 1.23
|
||||
|
||||
@@ -84,6 +84,9 @@ func FetchPayloads(selected []*Package, cacheDir string, allowHashMismatch bool)
|
||||
func fetchOnePayloadWithRetries(payload Payload, dest, fileID string, allowHashMismatch bool) (int64, error) {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < maxDownloadAttempts; attempt++ {
|
||||
if attempt > 0 {
|
||||
time.Sleep(retryBackoff(attempt))
|
||||
}
|
||||
n, err := tryDownloadPayload(payload, dest, fileID, allowHashMismatch)
|
||||
if err == nil {
|
||||
return n, nil
|
||||
@@ -94,6 +97,17 @@ func fetchOnePayloadWithRetries(payload Payload, dest, fileID string, allowHashM
|
||||
return 0, fmt.Errorf("giving up on %s after %d attempts: %w", fileID, maxDownloadAttempts, lastErr)
|
||||
}
|
||||
|
||||
// retryBackoff gives a transient failure (network blip, momentary rate
|
||||
// limiting) a little room to clear before hammering the same URL again:
|
||||
// 1s, 2s, 4s, 8s, capped at 10s.
|
||||
func retryBackoff(attempt int) time.Duration {
|
||||
d := time.Second << uint(attempt-1)
|
||||
if d > 10*time.Second {
|
||||
d = 10 * time.Second
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package download
|
||||
|
||||
import "sort"
|
||||
|
||||
// PackagesByType returns the best (arch/language-matching) variant of every
|
||||
// package in idx whose Type equals kind (e.g. "Workload" or "Component"),
|
||||
// sorted by ID.
|
||||
func PackagesByType(idx Index, kind string) []*Package {
|
||||
var ret []*Package
|
||||
for _, variants := range idx {
|
||||
p := variants[0]
|
||||
if p.Type == kind {
|
||||
ret = append(ret, p)
|
||||
}
|
||||
}
|
||||
sort.Slice(ret, func(i, j int) bool { return ret[i].ID < ret[j].ID })
|
||||
return ret
|
||||
}
|
||||
@@ -40,11 +40,15 @@ type Dependency struct {
|
||||
Type string // "", "Optional" or "Recommended"
|
||||
}
|
||||
|
||||
// LocalizedResource carries the license URL shown before accepting a
|
||||
// package's terms.
|
||||
// LocalizedResource carries a package's human-readable title/description
|
||||
// (shown by --list-workloads/--list-components) and the license URL shown
|
||||
// before accepting a package's terms.
|
||||
type LocalizedResource struct {
|
||||
Language string `json:"language"`
|
||||
License string `json:"license"`
|
||||
Language string `json:"language"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
License string `json:"license"`
|
||||
}
|
||||
|
||||
// Package is one entry from the installer manifest's "packages" array.
|
||||
@@ -108,6 +112,39 @@ func (p *Package) Key() string {
|
||||
return key
|
||||
}
|
||||
|
||||
// Localized returns p's LocalizedResource best matching language ("" means
|
||||
// "en"), preferring an exact match, then any en-* entry, then whatever's
|
||||
// first. Returns nil if p has no localized resources at all.
|
||||
func (p *Package) Localized(language string) *LocalizedResource {
|
||||
if len(p.LocalizedResources) == 0 {
|
||||
return nil
|
||||
}
|
||||
if language == "" {
|
||||
language = "en"
|
||||
}
|
||||
language = strings.ToLower(language)
|
||||
best := &p.LocalizedResources[0]
|
||||
bestScore := -1
|
||||
for i := range p.LocalizedResources {
|
||||
lr := &p.LocalizedResources[i]
|
||||
lang := strings.ToLower(lr.Language)
|
||||
score := 0
|
||||
switch {
|
||||
case lang == language:
|
||||
score = 3
|
||||
case strings.HasPrefix(lang, language+"-"):
|
||||
score = 2
|
||||
case strings.HasPrefix(lang, "en"):
|
||||
score = 1
|
||||
}
|
||||
if score > bestScore {
|
||||
bestScore = score
|
||||
best = lr
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
func (p *Package) InstalledSize() int64 {
|
||||
var sum int64
|
||||
for _, v := range p.InstallSizes {
|
||||
@@ -147,11 +184,23 @@ type Manifest struct {
|
||||
// restart of `download`.
|
||||
var httpClient = &http.Client{Timeout: 5 * time.Minute}
|
||||
|
||||
func init() {
|
||||
// --manifest points at a local file, fetched through this same client
|
||||
// via a "file:" URL (see cmd/vintner's runDownload) - so it needs a
|
||||
// registered "file" handler alongside the default http/https transport.
|
||||
t := http.DefaultTransport.(*http.Transport).Clone()
|
||||
t.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
|
||||
httpClient.Transport = t
|
||||
}
|
||||
|
||||
const maxManifestAttempts = 5
|
||||
|
||||
func httpGet(url string) ([]byte, error) {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < maxManifestAttempts; attempt++ {
|
||||
if attempt > 0 {
|
||||
time.Sleep(retryBackoff(attempt))
|
||||
}
|
||||
data, err := tryHTTPGet(url)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/assets"
|
||||
"github.com/Cheviiot/vintner/assets"
|
||||
)
|
||||
|
||||
// ApplyCompatibilityFixes applies the embedded Wine compatibility patches
|
||||
@@ -63,7 +63,7 @@ func applyGitPatch(dest, embeddedPath, target string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp("", "msvc-go-wine-*.patch")
|
||||
tmp, err := os.CreateTemp("", "vintner-*.patch")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package download
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -13,8 +15,7 @@ var reSDKVersion = regexp.MustCompile(`^\d+\.\d+\.\d+`)
|
||||
// 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 }
|
||||
func on() TriState { v := true; return &v }
|
||||
|
||||
// Options holds every flag that feeds package selection and download.
|
||||
type Options struct {
|
||||
@@ -41,7 +42,15 @@ type Options struct {
|
||||
SkipRecommended bool
|
||||
Language string
|
||||
|
||||
WithWDKInstallers string
|
||||
// WithWDK selects the PlatformToolset registration
|
||||
// (Component.Microsoft.Windows.DriverKit.BuildTools) that lets MSBuild
|
||||
// recognize WindowsKernelModeDriver10.0/WindowsUserModeDriver10.0
|
||||
// PlatformToolsets. It's not part of the manifest-driven default
|
||||
// selection - unlike everything else it depends on, it's opt-in via
|
||||
// --with-wdk, since the actual driver headers/libs come from a separate
|
||||
// NuGet package download handled outside ExpandSelection entirely (see
|
||||
// wdk.go and cmd/vintner's runDownload).
|
||||
WithWDK bool
|
||||
}
|
||||
|
||||
func addIfWanted(opts *Options, flag TriState, pkg string) {
|
||||
@@ -240,7 +249,7 @@ func ResolveSelection(opts *Options, idx Index) error {
|
||||
addIfWanted(opts, opts.WithDevCmd, "Microsoft.VisualStudio.VC.vcvars")
|
||||
addIfWanted(opts, opts.WithDevCmd, "Microsoft.VisualStudio.PackageGroup.VsDevCmd")
|
||||
|
||||
if opts.WithWDKInstallers != "" {
|
||||
if opts.WithWDK {
|
||||
opts.Package = append(opts.Package, "Component.Microsoft.Windows.DriverKit.BuildTools")
|
||||
}
|
||||
|
||||
@@ -293,6 +302,7 @@ func selectSDK(opts *Options, idx Index) error {
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
sort.Strings(versions)
|
||||
return fmt.Errorf("WinSDK version %s not found (available: %s)", opts.SDKVersion, strings.Join(versions, ", "))
|
||||
}
|
||||
}
|
||||
@@ -337,7 +347,14 @@ func collectDependencyClosure(idx Index, included map[string]bool, target string
|
||||
included[key] = true
|
||||
|
||||
ret := []*Package{p}
|
||||
for target, dep := range p.Dependencies() {
|
||||
deps := p.Dependencies()
|
||||
targets := make([]string, 0, len(deps))
|
||||
for target := range deps {
|
||||
targets = append(targets, target)
|
||||
}
|
||||
sort.Strings(targets)
|
||||
for _, target := range targets {
|
||||
dep := deps[target]
|
||||
id := target
|
||||
if dep.TargetID != "" {
|
||||
id = dep.TargetID
|
||||
@@ -367,3 +384,73 @@ func ExpandSelection(idx Index, opts *Options) ([]*Package, error) {
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// PrintDependencyTree writes an indented tree of opts.Package and everything
|
||||
// they transitively depend on to w, applying the exact same
|
||||
// arch/--ignore/Optional/Recommended filtering collectDependencyClosure
|
||||
// (used by ExpandSelection) does, so what's printed matches what an actual
|
||||
// download would select. A package already printed once elsewhere in the
|
||||
// tree is shown again as a leaf ("(see above)") rather than re-expanded, to
|
||||
// keep the output finite for packages multiple components depend on.
|
||||
func PrintDependencyTree(w io.Writer, idx Index, opts *Options) {
|
||||
printed := map[string]bool{}
|
||||
for _, id := range opts.Package {
|
||||
printDepNode(w, idx, id, nil, "", opts, 0, printed)
|
||||
}
|
||||
}
|
||||
|
||||
func printDepNode(w io.Writer, idx Index, target string, constraints map[string]string, depType string, opts *Options, depth int, printed map[string]bool) {
|
||||
if contains(opts.Ignore, strings.ToLower(target)) {
|
||||
return
|
||||
}
|
||||
indent := strings.Repeat(" ", depth)
|
||||
annotation := ""
|
||||
if depType != "" {
|
||||
annotation = " [" + depType + "]"
|
||||
}
|
||||
|
||||
p := idx.Find(target, constraints)
|
||||
if p == nil {
|
||||
fmt.Fprintf(w, "%s%s (not found)%s\n", indent, target, annotation)
|
||||
return
|
||||
}
|
||||
if opts.OnlyHost && !HostArchCompatible(p, opts.HostArch) {
|
||||
return
|
||||
}
|
||||
if !TargetArchCompatible(p, opts.Architecture) {
|
||||
return
|
||||
}
|
||||
|
||||
key := p.Key()
|
||||
if printed[key] {
|
||||
fmt.Fprintf(w, "%s%s%s (see above)\n", indent, p.ID, annotation)
|
||||
return
|
||||
}
|
||||
printed[key] = true
|
||||
fmt.Fprintf(w, "%s%s@%s%s\n", indent, p.ID, p.Version, annotation)
|
||||
|
||||
deps := p.Dependencies()
|
||||
targets := make([]string, 0, len(deps))
|
||||
for t := range deps {
|
||||
targets = append(targets, t)
|
||||
}
|
||||
sort.Strings(targets)
|
||||
for _, depTarget := range targets {
|
||||
dep := deps[depTarget]
|
||||
id := depTarget
|
||||
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
|
||||
}
|
||||
printDepNode(w, idx, id, c, dep.Type, opts, depth+1, printed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +193,40 @@ func TestAggregateDependsHostArchMismatchExcludes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExpandSelectionDeterministic guards against collectDependencyClosure
|
||||
// iterating a package's dependency map directly (Go map iteration order is
|
||||
// randomized per range statement, so a regression here wouldn't necessarily
|
||||
// show up on the first run - looping catches it reliably in practice).
|
||||
func TestExpandSelectionDeterministic(t *testing.T) {
|
||||
idx := fixtureIndex(t, "x86")
|
||||
opts := &Options{
|
||||
Package: []string{"Microsoft.VisualStudio.Workload.VCTools"},
|
||||
HostArch: "x86",
|
||||
OnlyHost: true,
|
||||
IncludeOptional: true,
|
||||
}
|
||||
first, err := ExpandSelection(idx, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := idsOf(first)
|
||||
for i := 0; i < 50; i++ {
|
||||
got, err := ExpandSelection(idx, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gotIDs := idsOf(got)
|
||||
if len(gotIDs) != len(want) {
|
||||
t.Fatalf("run %d: got %v, want %v", i, gotIDs, want)
|
||||
}
|
||||
for j := range want {
|
||||
if gotIDs[j] != want[j] {
|
||||
t.Fatalf("run %d: order changed: got %v, want %v", i, gotIDs, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func idsOf(pkgs []*Package) []string {
|
||||
var ids []string
|
||||
for _, p := range pkgs {
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The actual driver headers/libs (ntddk.h, wdf, km/um import libs) aren't
|
||||
// part of the VS installer manifest at all - Component.Microsoft.Windows.
|
||||
// DriverKit.BuildTools (see select.go) only registers the
|
||||
// WindowsKernelModeDriver10.0/WindowsUserModeDriver10.0 PlatformToolsets in
|
||||
// the MSBuild tree. The content those toolsets actually need ships as a
|
||||
// separate per-architecture NuGet package, independently of the vsman
|
||||
// pipeline: https://www.nuget.org/packages/Microsoft.Windows.WDK.x64 (and
|
||||
// .ARM64). This file fetches and unpacks that package directly through the
|
||||
// NuGet v3 flat-container API, without needing nuget.exe or a project
|
||||
// restore.
|
||||
|
||||
const nugetFlatContainer = "https://api.nuget.org/v3-flatcontainer"
|
||||
|
||||
// WDKNuGetID returns the nuget.org package id providing WDK content for
|
||||
// arch ("x86"/"x64" both use the x64 package - there's no 32-bit WDK
|
||||
// package - "arm64" uses the ARM64 one).
|
||||
func WDKNuGetID(arch string) string {
|
||||
if arch == "arm64" {
|
||||
return "Microsoft.Windows.WDK.ARM64"
|
||||
}
|
||||
return "Microsoft.Windows.WDK.x64"
|
||||
}
|
||||
|
||||
// nugetVersionIndex is the "versions" list nuget.org's flat-container index
|
||||
// returns, oldest first.
|
||||
type nugetVersionIndex struct {
|
||||
Versions []string `json:"versions"`
|
||||
}
|
||||
|
||||
var reNuGetPrerelease = regexp.MustCompile(`-`)
|
||||
|
||||
// FetchLatestWDKVersion returns the newest stable (non-prerelease) version
|
||||
// of arch's WDK NuGet package, preferring one whose version starts with
|
||||
// sdkBuild (e.g. "10.0.26100", to match an already-selected Windows SDK) if
|
||||
// any such version exists; otherwise it returns the newest stable version
|
||||
// overall.
|
||||
func FetchLatestWDKVersion(arch, sdkBuild string) (string, error) {
|
||||
id := strings.ToLower(WDKNuGetID(arch))
|
||||
data, err := httpGet(nugetFlatContainer + "/" + id + "/index.json")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("listing %s versions: %w", WDKNuGetID(arch), err)
|
||||
}
|
||||
var idx nugetVersionIndex
|
||||
if err := json.Unmarshal(data, &idx); err != nil {
|
||||
return "", fmt.Errorf("parsing %s version index: %w", WDKNuGetID(arch), err)
|
||||
}
|
||||
|
||||
var latestMatching, latestAny string
|
||||
for _, v := range idx.Versions {
|
||||
if reNuGetPrerelease.MatchString(v) {
|
||||
continue
|
||||
}
|
||||
latestAny = v
|
||||
if sdkBuild != "" && strings.HasPrefix(v, sdkBuild) {
|
||||
latestMatching = v
|
||||
}
|
||||
}
|
||||
if latestMatching != "" {
|
||||
return latestMatching, nil
|
||||
}
|
||||
if latestAny != "" {
|
||||
return latestAny, nil
|
||||
}
|
||||
return "", fmt.Errorf("no stable release of %s found", WDKNuGetID(arch))
|
||||
}
|
||||
|
||||
// SDKBuildPrefix extracts the "10.0.26100"-style build prefix from a
|
||||
// selected Win10SDK/Win11SDK package's version (e.g. "10.0.26100.1742" ->
|
||||
// "10.0.26100"), used to pick a matching WDK NuGet version. Returns "" if
|
||||
// selected contains no such package.
|
||||
func SDKBuildPrefix(selected []*Package) string {
|
||||
for _, p := range selected {
|
||||
id := strings.ToLower(p.ID)
|
||||
if strings.HasPrefix(id, "win10sdk") || strings.HasPrefix(id, "win11sdk") {
|
||||
parts := strings.SplitN(p.Version, ".", 4)
|
||||
if len(parts) >= 3 {
|
||||
return strings.Join(parts[:3], ".")
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DownloadWDK fetches (or reuses a cached copy of) arch's WDK NuGet package
|
||||
// at version, then unpacks its "c" directory - headers, libs, and the
|
||||
// per-SDK-build MSBuild props/targets the DriverKit.BuildTools PlatformToolset
|
||||
// registration imports via $(WDKContentRoot) - into destDir/wdk/<arch>/c.
|
||||
// Kept as its own self-contained tree rather than merged into the regular
|
||||
// SDK dirs, matching how $(WDKContentRoot) is meant to be used.
|
||||
//
|
||||
// vsVersion is the installed MSBuild's own $(VisualStudioVersion) (e.g.
|
||||
// "18.0"): the package's WindowsDriver.common.targets loads its custom
|
||||
// build tasks from an assembly named for that property
|
||||
// (Microsoft.DriverKit.Build.Tasks.$(VisualStudioVersion).dll), but the
|
||||
// package only ships one built for whatever (older) VS generation it
|
||||
// targeted - see duplicateVersionedBuildTaskAssemblies.
|
||||
func DownloadWDK(arch, version, cacheDir, destDir, vsVersion string) (string, error) {
|
||||
id := WDKNuGetID(arch)
|
||||
idLower := strings.ToLower(id)
|
||||
nupkgURL := fmt.Sprintf("%s/%s/%s/%s.%s.nupkg", nugetFlatContainer, idLower, version, idLower, version)
|
||||
|
||||
cacheFile := filepath.Join(cacheDir, fmt.Sprintf("%s-%s.nupkg", idLower, version))
|
||||
if !isFile(cacheFile) {
|
||||
fmt.Printf("Downloading %s %s\n", id, version)
|
||||
if err := httpDownloadFile(nupkgURL, cacheFile); err != nil {
|
||||
return "", fmt.Errorf("downloading %s %s: %w", id, version, err)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Using existing file %s\n", filepath.Base(cacheFile))
|
||||
}
|
||||
|
||||
wdkDir := filepath.Join(destDir, "wdk", arch)
|
||||
cDir := filepath.Join(wdkDir, "c")
|
||||
if err := extractNuGetPackageDir(cacheFile, "c/", cDir); err != nil {
|
||||
return "", fmt.Errorf("unpacking %s %s: %w", id, version, err)
|
||||
}
|
||||
if err := duplicateVersionedBuildTaskAssemblies(cDir, vsVersion); err != nil {
|
||||
return "", fmt.Errorf("adapting %s %s to VisualStudioVersion %s: %w", id, version, vsVersion, err)
|
||||
}
|
||||
if err := fillMissingHostTools(cDir); err != nil {
|
||||
return "", fmt.Errorf("filling in missing x86 host tools for %s %s: %w", id, version, err)
|
||||
}
|
||||
return wdkDir, nil
|
||||
}
|
||||
|
||||
// fillMissingHostTools copies every file from each cDir/bin/<sdkver>/x64
|
||||
// directory into its sibling .../x86 directory, adding whatever's missing
|
||||
// without overwriting anything already there. The WDK NuGet package's x86
|
||||
// host-tool directory only ships a handful of genuinely x86-specific files
|
||||
// (Inf2Cat.exe among them); most driver build tools (stampinf.exe included)
|
||||
// only ship as x64 binaries, but WindowsDriver.Common.targets hardcodes an
|
||||
// x86 tool path (WDKBinRoot_x86) with no fallback. This isn't "faking" an
|
||||
// x86 binary - it's just running the real x64 PE binaries from a directory
|
||||
// MSBuild happens to call "x86"; Wine doesn't care what the folder is
|
||||
// named, only the PE header when it execs the file, and we're always on an
|
||||
// x64 host/Wine setup here.
|
||||
func fillMissingHostTools(cDir string) error {
|
||||
binDir := filepath.Join(cDir, "bin")
|
||||
entries, err := os.ReadDir(binDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
x64Dir := filepath.Join(binDir, e.Name(), "x64")
|
||||
if !isDir(x64Dir) {
|
||||
continue
|
||||
}
|
||||
x86Dir := filepath.Join(binDir, e.Name(), "x86")
|
||||
if err := os.MkdirAll(x86Dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
files, err := os.ReadDir(x64Dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
dst := filepath.Join(x86Dir, f.Name())
|
||||
if isFile(dst) {
|
||||
continue
|
||||
}
|
||||
if err := copyFile(filepath.Join(x64Dir, f.Name()), dst, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var reVersionedDLL = regexp.MustCompile(`^(.*)\.\d+\.\d+\.dll$`)
|
||||
|
||||
// duplicateVersionedBuildTaskAssemblies copies every "*.<oldver>.dll" file
|
||||
// under cDir/build (the driver build task assemblies, e.g.
|
||||
// Microsoft.DriverKit.Build.Tasks.17.0.dll) to a sibling
|
||||
// "*.<targetVSVersion>.dll" when one doesn't already exist. The WDK NuGet
|
||||
// package bundles these named for whatever VS generation it was built
|
||||
// against; our installed MSBuild may be newer and looks them up by its own
|
||||
// $(VisualStudioVersion). The assemblies aren't VS-version-specific in
|
||||
// behavior - only the file name encodes an expected caller - so serving the
|
||||
// same bytes under the new name is safe.
|
||||
func duplicateVersionedBuildTaskAssemblies(cDir, targetVSVersion string) error {
|
||||
buildDir := filepath.Join(cDir, "build")
|
||||
if !isDir(buildDir) {
|
||||
return nil
|
||||
}
|
||||
return filepath.WalkDir(buildDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return err
|
||||
}
|
||||
m := reVersionedDLL.FindStringSubmatch(d.Name())
|
||||
if m == nil || strings.HasSuffix(d.Name(), "."+targetVSVersion+".dll") {
|
||||
return nil
|
||||
}
|
||||
target := filepath.Join(filepath.Dir(path), m[1]+"."+targetVSVersion+".dll")
|
||||
if isFile(target) {
|
||||
return nil
|
||||
}
|
||||
return copyFile(path, target, 0o644)
|
||||
})
|
||||
}
|
||||
|
||||
// extractNuGetPackageDir extracts every entry of nupkgFile whose name starts
|
||||
// with prefix into dest, stripping prefix from each resulting path.
|
||||
func extractNuGetPackageDir(nupkgFile, prefix, dest string) error {
|
||||
r, err := zip.OpenReader(nupkgFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
if err := os.MkdirAll(dest, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range r.File {
|
||||
name, err := url.PathUnescape(f.Name)
|
||||
if err != nil {
|
||||
name = f.Name
|
||||
}
|
||||
name = strings.ReplaceAll(name, `\`, "/")
|
||||
if !strings.HasPrefix(name, prefix) {
|
||||
continue
|
||||
}
|
||||
rel := strings.TrimPrefix(name, prefix)
|
||||
if rel == "" {
|
||||
continue
|
||||
}
|
||||
target := filepath.Join(dest, rel)
|
||||
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
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
// Package i18n provides minimal message localization for vintner's own
|
||||
// CLI chrome (usage text, progress lines, prompts). It does not localize
|
||||
// error messages bubbled up from deeper packages - those stay in English,
|
||||
// matching how most cross-platform dev tools keep diagnostic text technical
|
||||
// regardless of UI language.
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Lang string
|
||||
|
||||
const (
|
||||
EN Lang = "en"
|
||||
RU Lang = "ru"
|
||||
)
|
||||
|
||||
var current = detect()
|
||||
|
||||
// detect picks the active language from VINTNER_LANG (checked first, so
|
||||
// it always overrides the locale), falling back to the POSIX locale
|
||||
// variables in their usual priority order (LC_ALL, LC_MESSAGES, LANG). Any
|
||||
// value starting with "ru" (case-insensitive) selects Russian; anything else
|
||||
// falls back to English.
|
||||
func detect() Lang {
|
||||
for _, key := range []string{"VINTNER_LANG", "LC_ALL", "LC_MESSAGES", "LANG"} {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(v), "ru") {
|
||||
return RU
|
||||
}
|
||||
return EN
|
||||
}
|
||||
return EN
|
||||
}
|
||||
|
||||
// Current returns the active language, for callers that need to branch
|
||||
// beyond a simple T() lookup.
|
||||
func Current() Lang { return current }
|
||||
|
||||
// T looks up key in the message catalog and formats it (via fmt.Sprintf)
|
||||
// with args, if any. Keys with no translation for the current language fall
|
||||
// back to English; keys missing from the catalog entirely are returned
|
||||
// as-is, so a missing translation degrades to a visible-but-harmless string
|
||||
// rather than a panic.
|
||||
func T(key string, args ...any) string {
|
||||
msg := key
|
||||
if entry, ok := catalog[key]; ok {
|
||||
if m, ok := entry[current]; ok {
|
||||
msg = m
|
||||
} else {
|
||||
msg = entry[EN]
|
||||
}
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return msg
|
||||
}
|
||||
return fmt.Sprintf(msg, args...)
|
||||
}
|
||||
|
||||
var catalog = map[string]map[Lang]string{
|
||||
"main.usage": {
|
||||
EN: `vintner - cross compile with MSVC on Linux via Wine
|
||||
|
||||
Usage:
|
||||
vintner download (dl) --accept-license [--dest <dir>] [options]
|
||||
fetch and unpack MSVC/WinSDK/WDK
|
||||
vintner install (i) [dir] wire up wrappers for a downloaded MSVC
|
||||
vintner env (e) --bin <dir/bin/arch> print INCLUDE/LIB for native clang-cl/lld-link use
|
||||
vintner version (v) print the version
|
||||
vintner help (h) show this message
|
||||
vintner completion bash|zsh print a shell completion script
|
||||
|
||||
Run "vintner <command> --help" for that command's own options - download
|
||||
has many, including --with-wdk, --list-workloads, --list-components and
|
||||
--print-deps-tree.
|
||||
|
||||
--dest/[dir] default to ~/.vintner if omitted.
|
||||
Language: set VINTNER_LANG=ru (or LANG=ru_RU...) for Russian output.
|
||||
Completion: source <(vintner completion bash) # or zsh
|
||||
|
||||
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
|
||||
`,
|
||||
RU: `vintner — кросс-компиляция настоящим MSVC на Linux через Wine
|
||||
|
||||
Использование:
|
||||
vintner download (dl) --accept-license [--dest <каталог>] [опции]
|
||||
скачать и распаковать MSVC/WinSDK/WDK
|
||||
vintner install (i) [каталог] настроить обёртки для скачанного MSVC
|
||||
vintner env (e) --bin <dir/bin/arch> вывести INCLUDE/LIB для clang-cl/lld-link напрямую
|
||||
vintner version (v) показать версию
|
||||
vintner help (h) показать эту справку
|
||||
vintner completion bash|zsh вывести скрипт автодополнения для оболочки
|
||||
|
||||
Запустите «vintner <команда> --help» для параметров конкретной команды —
|
||||
у download их много, включая --with-wdk, --list-workloads, --list-components
|
||||
и --print-deps-tree.
|
||||
|
||||
--dest/[каталог] по умолчанию — ~/.vintner.
|
||||
Язык: установите VINTNER_LANG=en (или LANG=en_US...) для вывода на английском.
|
||||
Автодополнение: source <(vintner completion bash) # или zsh
|
||||
|
||||
После установки добавьте <dir>/bin/<arch> в PATH и вызывайте инструменты напрямую:
|
||||
cl, link, lib, ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd, findstr
|
||||
`,
|
||||
},
|
||||
"main.unknown_subcommand": {
|
||||
EN: "vintner: unknown subcommand %q\n\n",
|
||||
RU: "vintner: неизвестная подкоманда %q\n\n",
|
||||
},
|
||||
|
||||
"install.usage": {
|
||||
EN: "usage: vintner install (i) [dest] (default: ~/.vintner)",
|
||||
RU: "использование: vintner install (i) [каталог] (по умолчанию: ~/.vintner)",
|
||||
},
|
||||
"install.default_dir": {
|
||||
EN: "No directory given, using default: %s",
|
||||
RU: "Каталог не указан, используется значение по умолчанию: %s",
|
||||
},
|
||||
"install.done": {
|
||||
EN: "Done. Add %s to PATH to use cl, link, lib, ...",
|
||||
RU: "Готово. Добавьте %s в PATH, чтобы использовать cl, link, lib и т.д.",
|
||||
},
|
||||
|
||||
"env.usage": {
|
||||
EN: "usage: vintner env (e) --bin <dest>/bin/<arch>",
|
||||
RU: "использование: vintner env (e) --bin <dest>/bin/<arch>",
|
||||
},
|
||||
"env.unknown_arch": {
|
||||
EN: "vintner env: unknown arch %q\n",
|
||||
RU: "vintner env: неизвестная архитектура %q\n",
|
||||
},
|
||||
|
||||
"download.host_arch": {
|
||||
EN: "Install packages for %s host architecture",
|
||||
RU: "Установка пакетов для архитектуры хоста %s",
|
||||
},
|
||||
"download.selected": {
|
||||
EN: "Selected %d packages, for a total download size of %s, install size of %s\n",
|
||||
RU: "Выбрано пакетов: %d, общий размер загрузки %s, размер после установки %s\n",
|
||||
},
|
||||
"download.default_dest": {
|
||||
EN: "--dest not set, using default: %s",
|
||||
RU: "--dest не указан, используется значение по умолчанию: %s",
|
||||
},
|
||||
"download.done": {
|
||||
EN: "Done. Next: vintner install %s",
|
||||
RU: "Готово. Далее: vintner install %s",
|
||||
},
|
||||
"download.wdk_skip": {
|
||||
EN: "--with-wdk: no x64/arm64 target architecture selected, skipping (no WDK package exists for x86/arm)",
|
||||
RU: "--with-wdk: не выбрана целевая архитектура x64/arm64, пропускаем (для x86/arm пакета WDK не существует)",
|
||||
},
|
||||
"download.wdk_installed": {
|
||||
EN: "Installed WDK (%s) %s at %s\n",
|
||||
RU: "WDK (%s) %s установлен в %s\n",
|
||||
},
|
||||
"download.workloads_header": {
|
||||
EN: "Available Workloads (%d):\n",
|
||||
RU: "Доступные рабочие нагрузки (Workload) (%d):\n",
|
||||
},
|
||||
"download.components_header": {
|
||||
EN: "Available Components (%d):\n",
|
||||
RU: "Доступные компоненты (Component) (%d):\n",
|
||||
},
|
||||
"download.license_prompt": {
|
||||
EN: "Do you accept the license at %s (yes/no)? ",
|
||||
RU: "Вы принимаете лицензию по адресу %s (yes/no)? ",
|
||||
},
|
||||
"download.license_reprompt": {
|
||||
EN: "Do you accept the license? Answer \"yes\" or \"no\": ",
|
||||
RU: "Вы принимаете лицензию? Ответьте «yes» или «no»: ",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDetect(t *testing.T) {
|
||||
envKeys := []string{"VINTNER_LANG", "LC_ALL", "LC_MESSAGES", "LANG"}
|
||||
clear := func() {
|
||||
for _, k := range envKeys {
|
||||
t.Setenv(k, "")
|
||||
// t.Setenv("", "") leaves the var set-but-empty, which detect()
|
||||
// already treats as "unset" (its loop skips v == "") - matches
|
||||
// how a genuinely-unset env var behaves for this function.
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
want Lang
|
||||
}{
|
||||
{"nothing set defaults to English", nil, EN},
|
||||
{"VINTNER_LANG=ru", map[string]string{"VINTNER_LANG": "ru"}, RU},
|
||||
{"VINTNER_LANG=en", map[string]string{"VINTNER_LANG": "en"}, EN},
|
||||
{"VINTNER_LANG wins over a Russian LANG", map[string]string{"VINTNER_LANG": "en", "LANG": "ru_RU.UTF-8"}, EN},
|
||||
{"LC_ALL wins over LANG", map[string]string{"LC_ALL": "ru_RU.UTF-8", "LANG": "en_US.UTF-8"}, RU},
|
||||
{"LANG=ru_RU.UTF-8 alone", map[string]string{"LANG": "ru_RU.UTF-8"}, RU},
|
||||
{"LANG=en_US.UTF-8 alone", map[string]string{"LANG": "en_US.UTF-8"}, EN},
|
||||
{"unrelated locale defaults to English", map[string]string{"LANG": "de_DE.UTF-8"}, EN},
|
||||
{"case-insensitive RU prefix", map[string]string{"VINTNER_LANG": "RU"}, RU},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
clear()
|
||||
for k, v := range tc.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
if got := detect(); got != tc.want {
|
||||
t.Errorf("detect() = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCatalogCompleteness guards against adding an EN string without its RU
|
||||
// counterpart (or vice versa) - a silent gap here degrades to showing the
|
||||
// wrong language's text via T()'s EN-fallback rather than failing loudly.
|
||||
func TestCatalogCompleteness(t *testing.T) {
|
||||
for key, entry := range catalog {
|
||||
en, hasEN := entry[EN]
|
||||
if !hasEN || strings.TrimSpace(en) == "" {
|
||||
t.Errorf("catalog[%q] has no (non-empty) English translation", key)
|
||||
}
|
||||
ru, hasRU := entry[RU]
|
||||
if !hasRU || strings.TrimSpace(ru) == "" {
|
||||
t.Errorf("catalog[%q] has no (non-empty) Russian translation", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTMissingKeyReturnsKeyItself(t *testing.T) {
|
||||
got := T("no.such.key")
|
||||
if got != "no.such.key" {
|
||||
t.Errorf("T(unknown key) = %q, want the key itself", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTFallsBackToEnglish(t *testing.T) {
|
||||
const testKey = "test.fallback.only.en"
|
||||
catalog[testKey] = map[Lang]string{EN: "hello %s"}
|
||||
defer delete(catalog, testKey)
|
||||
|
||||
saved := current
|
||||
current = RU
|
||||
defer func() { current = saved }()
|
||||
|
||||
if got := T(testKey, "world"); got != "hello world" {
|
||||
t.Errorf("T(%q) with no RU entry = %q, want %q", testKey, got, "hello world")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTFormatsArgs(t *testing.T) {
|
||||
saved := current
|
||||
current = EN
|
||||
defer func() { current = saved }()
|
||||
|
||||
got := T("download.wdk_installed", "x64", "10.0.26100.1", "/dest")
|
||||
want := "Installed WDK (x64) 10.0.26100.1 at /dest\n"
|
||||
if got != want {
|
||||
t.Errorf("T(download.wdk_installed, ...) = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
+13
-13
@@ -1,4 +1,4 @@
|
||||
// Package install wires up a downloaded MSVC/WinSDK tree so the msvc-go-wine
|
||||
// Package install wires up a downloaded MSVC/WinSDK tree so the vintner
|
||||
// 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.
|
||||
@@ -14,16 +14,16 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/assets"
|
||||
"github.com/Cheviiot/msvc-go-wine/internal/wineenv"
|
||||
"github.com/Cheviiot/vintner/assets"
|
||||
"github.com/Cheviiot/vintner/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
|
||||
// `vintner 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 currently running vintner 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)
|
||||
@@ -86,7 +86,7 @@ func Install(dest, selfBinary string) error {
|
||||
|
||||
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)
|
||||
return fmt.Errorf("%s not found - expected a Windows SDK already unpacked by `vintner download`", kits10)
|
||||
}
|
||||
if err := lnS("Lib", filepath.Join(kits10, "lib")); err != nil {
|
||||
return err
|
||||
@@ -148,7 +148,7 @@ func Install(dest, selfBinary string) error {
|
||||
if err := os.MkdirAll(destBin, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
sharedBinary := filepath.Join(destBin, "msvc-go-wine")
|
||||
sharedBinary := filepath.Join(destBin, "vintner")
|
||||
if err := copyFile(selfBinary, sharedBinary, 0o755); err != nil {
|
||||
return fmt.Errorf("installing shared binary: %w", err)
|
||||
}
|
||||
@@ -288,7 +288,7 @@ func renameHostDirs(binDir string) error {
|
||||
}
|
||||
|
||||
// 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
|
||||
// vintner 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
|
||||
@@ -297,23 +297,23 @@ func renameHostDirs(binDir string) error {
|
||||
// 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)
|
||||
// symlink (cl -> vintner) resolves to a binary that's still in the
|
||||
// right arch dir; a symlink to a binary one level up (cl -> ../vintner)
|
||||
// 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")
|
||||
localBinary := filepath.Join(archDir, "vintner")
|
||||
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 {
|
||||
if err := lnS("vintner", filepath.Join(archDir, name)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := lnS("msvc-go-wine", filepath.Join(archDir, name+".exe")); err != nil {
|
||||
if err := lnS("vintner", filepath.Join(archDir, name+".exe")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// ConfigFileName is the per-architecture config dropped next to the tool
|
||||
// symlinks by `msvc-go-wine install`.
|
||||
// symlinks by `vintner install`.
|
||||
const ConfigFileName = "env.json"
|
||||
|
||||
// Config is the per-architecture info generated at install time.
|
||||
|
||||
@@ -16,6 +16,14 @@ type Paths struct {
|
||||
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
|
||||
|
||||
// Windows-notation ("z:\...") equivalents of the paths above, needed to
|
||||
// populate the MSBuild-specific environment variables its toolset/SDK
|
||||
// detection props read (see msbuildEnv in the wrapper package).
|
||||
BaseWin string // z:\<dest>
|
||||
MSVCBaseWin string // z:\<dest>\vc
|
||||
MSVCDirWin string // z:\<dest>\vc\tools\msvc\<ver>
|
||||
SDKBaseWin string // z:\<dest>\kits\10
|
||||
|
||||
Include string
|
||||
Lib string
|
||||
LibPath string
|
||||
@@ -89,6 +97,11 @@ func NewPaths(cfg *Config, baseUnix string) *Paths {
|
||||
SDKBinDir: sdkBinDir,
|
||||
MSBuildBinDir: msbuildBinDir,
|
||||
|
||||
BaseWin: winBase,
|
||||
MSVCBaseWin: msvcBase,
|
||||
MSVCDirWin: msvcDirWin,
|
||||
SDKBaseWin: sdkBase,
|
||||
|
||||
Include: include,
|
||||
Lib: lib,
|
||||
LibPath: lib,
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Cheviiot/vintner/internal/wineenv"
|
||||
)
|
||||
|
||||
var reToolsetDir = regexp.MustCompile(`^v(\d+)$`)
|
||||
|
||||
// msbuildEnv returns the extra environment variables MSBuild's own
|
||||
// SDK/toolset-detection property sheets need. The generic INCLUDE/LIB/
|
||||
// WINEPATH set by buildEnv are enough for cl/link/lib invoked directly, but
|
||||
// MSBuild resolves the compiler location and Windows SDK through a
|
||||
// different, registry-oriented mechanism - DisableRegistryUse=true
|
||||
// redirects that lookup to these variables instead of a (nonexistent)
|
||||
// Windows Registry.
|
||||
func msbuildEnv(cfg *wineenv.Config, paths *wineenv.Paths) map[string]string {
|
||||
env := map[string]string{
|
||||
// WDK driver builds stamp the INF's DriverVer with StampInf (which
|
||||
// uses the local wall-clock date) and then validate it with
|
||||
// Inf2Cat (which checks against UTC "now"). For any timezone east
|
||||
// of UTC, local-vs-UTC disagree on the calendar date for most of
|
||||
// the day, so Inf2Cat rejects the just-stamped date as "postdated"
|
||||
// (MSB6006, "DriverVer set to a date in the future"). Forcing both
|
||||
// tools onto the same UTC clock removes the mismatch.
|
||||
"TZ": "UTC",
|
||||
|
||||
"DisableRegistryUse": "true",
|
||||
"VCToolsVersion": cfg.MSVCVer,
|
||||
"VsInstallRoot": paths.BaseWin + `\`,
|
||||
"VSInstallDir": paths.BaseWin + `\`,
|
||||
|
||||
"SDKReferenceDirectoryRoot": paths.BaseWin + `\`,
|
||||
"SDKExtensionDirectoryRoot": paths.BaseWin + `\`,
|
||||
"MSBUILDSDKREFERENCEDIRECTORY": paths.BaseWin + `\`,
|
||||
"MSBUILDMULTIPLATFORMSDKREFERENCEDIRECTORY": paths.BaseWin + `\`,
|
||||
|
||||
"WindowsSdkDir_10": paths.SDKBaseWin + `\`,
|
||||
"UniversalCRTSdkDir_10": paths.SDKBaseWin + `\`,
|
||||
"WindowsSdkDir": paths.SDKBaseWin + `\`,
|
||||
"UniversalCRTSdkDir": paths.SDKBaseWin + `\`,
|
||||
"WindowsTargetPlatformVersion": cfg.SDKVer,
|
||||
"UCRTContentRoot": paths.SDKBaseWin + `\`,
|
||||
"NETFXKitsDir": paths.SDKBaseWin + `\`,
|
||||
"NETFXSDKDir": paths.SDKBaseWin + `\`,
|
||||
|
||||
// WDK-specific properties; harmless when not building a driver.
|
||||
"WDKKitVersion": "10",
|
||||
"Driver_SpectreMitigation": "false",
|
||||
"SignMode": "off",
|
||||
"Inf2CatNoCatalog": "true",
|
||||
"ApiValidator_Enable": "False",
|
||||
|
||||
"Platform": msbuildPlatform(cfg.Arch),
|
||||
}
|
||||
|
||||
// Microsoft.Cpp.props resolves the compiler/toolset location through
|
||||
// VCInstallDir_<N>/VCToolsInstallDir_<N>, where <N> is whatever numeric
|
||||
// suffix the installed MSBuild toolset property sheets use (e.g.
|
||||
// .../MSBuild/Microsoft/VC/v180 -> "180"). Populate every one actually
|
||||
// present, so a project pinned to any of them resolves to the one real
|
||||
// toolchain that's installed.
|
||||
matches, _ := filepath.Glob(filepath.Join(paths.BaseUnix, "MSBuild", "Microsoft", "VC", "v*"))
|
||||
for _, m := range matches {
|
||||
sub := reToolsetDir.FindStringSubmatch(filepath.Base(m))
|
||||
if sub == nil {
|
||||
continue
|
||||
}
|
||||
env["VCInstallDir_"+sub[1]] = paths.MSVCBaseWin + `\`
|
||||
env["VCToolsInstallDir_"+sub[1]] = paths.MSVCDirWin + `\`
|
||||
}
|
||||
|
||||
if strings.HasSuffix(paths.MSBuildBinDir, "amd64") {
|
||||
env["PreferredToolArchitecture"] = "x64"
|
||||
}
|
||||
|
||||
// The WindowsKernelModeDriver10.0/WindowsUserModeDriver10.0
|
||||
// PlatformToolsets (registered by `download --with-wdk`, see
|
||||
// internal/download/wdk.go) resolve WDKContentRoot through the
|
||||
// (nonexistent, under Wine) registry unless it's already set - same
|
||||
// DisableRegistryUse workaround as WindowsSdkDir_10 above. WDKBuildFolder
|
||||
// picks the per-SDK-build subtree (c/build/<ver>/...) the NuGet
|
||||
// package's content is organized under.
|
||||
wdkContentRoot := filepath.Join(paths.BaseUnix, "wdk", cfg.Arch, "c")
|
||||
if fi, err := os.Stat(wdkContentRoot); err == nil && fi.IsDir() {
|
||||
env["WDKContentRoot"] = wineenv.ToWinPath(wdkContentRoot) + `\`
|
||||
env["WDKBuildFolder"] = cfg.SDKVer
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
func msbuildPlatform(arch string) string {
|
||||
switch arch {
|
||||
case "x86":
|
||||
return "Win32"
|
||||
case "arm":
|
||||
return "ARM"
|
||||
case "arm64":
|
||||
return "ARM64"
|
||||
default:
|
||||
return arch
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,13 @@ func execInherit(args []string) int {
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
setNewProcessGroup(cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return 127
|
||||
}
|
||||
stopSignals := forwardSignals(cmd.Process)
|
||||
defer stopSignals()
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return exitErr.ExitCode()
|
||||
}
|
||||
|
||||
+112
-38
@@ -10,11 +10,21 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/internal/wineenv"
|
||||
"github.com/Cheviiot/vintner/internal/wineenv"
|
||||
)
|
||||
|
||||
// toolRelayName is where `msvc-go-wine install` places the compiled
|
||||
// pipeDrainGrace bounds how long we wait for a tool's stdout/stderr copy
|
||||
// goroutines to see EOF after the tool's own process has already exited.
|
||||
// Wine keeps wineserver and its service processes (services.exe,
|
||||
// winedevice.exe, explorer.exe, ...) running in the background for reuse
|
||||
// across invocations, and they inherit our pipes' write ends - so EOF can
|
||||
// otherwise never arrive, hanging any caller piping our output (`| tee`,
|
||||
// `| tail`, CI log capture) long after the actual build finished.
|
||||
const pipeDrainGrace = 500 * time.Millisecond
|
||||
|
||||
// toolRelayName is where `vintner install` places the compiled
|
||||
// toolrelay.exe helper, shared across all arch bin dirs.
|
||||
const toolRelayName = "toolrelay.exe"
|
||||
|
||||
@@ -27,7 +37,7 @@ func Run(tool string, args []string) int {
|
||||
|
||||
s, ok := Tools[tool]
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "msvc-go-wine: unknown tool %q\n", tool)
|
||||
fmt.Fprintf(os.Stderr, "vintner: unknown tool %q\n", tool)
|
||||
return 127
|
||||
}
|
||||
|
||||
@@ -40,19 +50,19 @@ func Run(tool string, args []string) int {
|
||||
// <dest>/bin/<arch>, not <dest>/bin.
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner:", 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)
|
||||
fmt.Fprintln(os.Stderr, "vintner: 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)
|
||||
fmt.Fprintln(os.Stderr, "vintner: locating installation root:", err)
|
||||
return 1
|
||||
}
|
||||
paths := wineenv.NewPaths(cfg, baseUnix)
|
||||
@@ -61,7 +71,7 @@ func Run(tool string, args []string) int {
|
||||
|
||||
wineBin, err := wineenv.FindWine()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -70,13 +80,19 @@ func Run(tool string, args []string) int {
|
||||
var exitCode int
|
||||
switch {
|
||||
case s.rawStdout:
|
||||
// MSBuild: skip all filtering/toolrelay, inherit stdio directly.
|
||||
// MSBuild: skip all filtering/toolrelay (its output is meant to be
|
||||
// read as-is), and add the extra environment MSBuild's own
|
||||
// toolset/SDK-detection props need on top of the generic
|
||||
// INCLUDE/LIB/WINEPATH.
|
||||
cmd := exec.Command(wineBin, append([]string{toolExePath}, rewritten...)...)
|
||||
cmd.Env = buildEnv(paths)
|
||||
env := buildEnv(paths)
|
||||
for k, v := range msbuildEnv(cfg, paths) {
|
||||
env = append(env, k+"="+v)
|
||||
}
|
||||
cmd.Env = env
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
exitCode = runAndWait(cmd)
|
||||
setNewProcessGroup(cmd)
|
||||
exitCode = runRawStdout(cmd)
|
||||
default:
|
||||
relay := filepath.Join(paths.BaseUnix, "bin", toolRelayName)
|
||||
if fi, err := os.Stat(relay); err == nil && !fi.IsDir() {
|
||||
@@ -85,6 +101,7 @@ func Run(tool string, args []string) int {
|
||||
cmd := exec.Command(wineBin, append([]string{toolExePath}, rewritten...)...)
|
||||
cmd.Env = buildEnv(paths)
|
||||
cmd.Stdin = os.Stdin
|
||||
setNewProcessGroup(cmd)
|
||||
exitCode = runFiltered(cmd, s.stdoutFilter, s.stderrFilter)
|
||||
}
|
||||
}
|
||||
@@ -104,17 +121,17 @@ func Run(tool string, args []string) int {
|
||||
// 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()))
|
||||
stdoutFifo := filepath.Join(os.TempDir(), fmt.Sprintf("vintner.stdout.%d", os.Getpid()))
|
||||
stderrFifo := filepath.Join(os.TempDir(), fmt.Sprintf("vintner.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)
|
||||
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||
return 1
|
||||
}
|
||||
defer os.Remove(stdoutFifo)
|
||||
if err := syscall.Mkfifo(stderrFifo, 0o600); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||
return 1
|
||||
}
|
||||
defer os.Remove(stderrFifo)
|
||||
@@ -122,6 +139,7 @@ func runViaToolRelay(wineBin, relayExe, exePath string, args []string, paths *wi
|
||||
cmdArgs := append([]string{relayExe, exePath}, args...)
|
||||
cmd := exec.Command(wineBin, cmdArgs...)
|
||||
cmd.Env = append(buildEnv(paths), "MSVCGOWINE_STDOUT="+stdoutFifo, "MSVCGOWINE_STDERR="+stderrFifo)
|
||||
setNewProcessGroup(cmd)
|
||||
if devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0); err == nil {
|
||||
defer devNull.Close()
|
||||
cmd.Stdout = devNull
|
||||
@@ -129,9 +147,11 @@ func runViaToolRelay(wineBin, relayExe, exePath string, args []string, paths *wi
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||
return 1
|
||||
}
|
||||
stopSignals := forwardSignals(cmd.Process)
|
||||
defer stopSignals()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
@@ -161,12 +181,65 @@ func runViaToolRelay(wineBin, relayExe, exePath string, args []string, paths *wi
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return exitErr.ExitCode()
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// runRawStdout runs cmd, copying its stdout/stderr through byte-for-byte
|
||||
// (MSBuild's own console formatting is meant to reach the user as-is). It
|
||||
// pipes rather than inheriting os.Stdout/os.Stderr directly so that only our
|
||||
// own copy goroutines - not the caller's terminal or pipe - are exposed to
|
||||
// Wine's background processes holding those descriptors open; see
|
||||
// pipeDrainGrace.
|
||||
func runRawStdout(cmd *exec.Cmd) int {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||
return 1
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||
return 1
|
||||
}
|
||||
stopSignals := forwardSignals(cmd.Process)
|
||||
defer stopSignals()
|
||||
|
||||
doneOut := make(chan struct{})
|
||||
doneErr := make(chan struct{})
|
||||
go func() { io.Copy(os.Stdout, stdout); close(doneOut) }()
|
||||
go func() { io.Copy(os.Stderr, stderr); close(doneErr) }()
|
||||
|
||||
err = cmd.Wait()
|
||||
drain(doneOut)
|
||||
drain(doneErr)
|
||||
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return exitErr.ExitCode()
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// drain waits for a pipe-copy goroutine to see EOF, but not past
|
||||
// pipeDrainGrace - see its doc comment for why EOF can otherwise never come.
|
||||
func drain(done <-chan struct{}) {
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(pipeDrainGrace):
|
||||
}
|
||||
}
|
||||
|
||||
func buildEnv(p *wineenv.Paths) []string {
|
||||
overrides := map[string]string{
|
||||
"INCLUDE": p.Include,
|
||||
@@ -199,31 +272,36 @@ func buildEnv(p *wineenv.Paths) []string {
|
||||
func runFiltered(cmd *exec.Cmd, stdoutF, stderrF lineFilter) int {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||
return 1
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||
return 1
|
||||
}
|
||||
stopSignals := forwardSignals(cmd.Process)
|
||||
defer stopSignals()
|
||||
|
||||
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()
|
||||
doneOut := make(chan struct{})
|
||||
doneErr := make(chan struct{})
|
||||
go func() { pumpLines(stdout, os.Stdout, stdoutF); close(doneOut) }()
|
||||
go func() { pumpLines(stderr, os.Stderr, stderrF); close(doneErr) }()
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
err = cmd.Wait()
|
||||
drain(doneOut)
|
||||
drain(doneErr)
|
||||
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return exitErr.ExitCode()
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
@@ -241,15 +319,11 @@ func pumpLines(r io.Reader, w *os.File, filter lineFilter) {
|
||||
}
|
||||
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
|
||||
// bufio.Scanner silently stops (dropping the rest of the stream) once a
|
||||
// single line exceeds its 16MB buffer - surface that rather than letting
|
||||
// build output vanish without explanation (heavily templated C++ error
|
||||
// messages are the realistic way to hit this).
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Fprintf(w, "vintner: output truncated: %v\n", err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// killGrace bounds how long a forwarded SIGINT/SIGTERM gets to make a
|
||||
// subprocess tree exit on its own before escalating to SIGKILL - long
|
||||
// enough for wineserver to tear down a Windows process tree cleanly, short
|
||||
// enough that an unresponsive one doesn't hang vintner's own shutdown.
|
||||
const killGrace = 5 * time.Second
|
||||
|
||||
// setNewProcessGroup puts cmd's eventual child in its own process group
|
||||
// (pgid = its own pid) instead of inheriting vintner's. Without this, a
|
||||
// caller that signals vintner by PID alone (a CI runner enforcing a
|
||||
// timeout, a supervisor's `kill <pid>`) never reaches the wine/wineserver
|
||||
// tree underneath it, which is then reparented to init and keeps running -
|
||||
// wasting CPU, holding file locks, leaving stray FIFOs/temp files behind.
|
||||
// (Interactive Ctrl-C already reaches every process in the terminal's
|
||||
// foreground group regardless of this, but forwardSignals below handles
|
||||
// that case too now that the child has moved to its own group.)
|
||||
func setNewProcessGroup(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
}
|
||||
|
||||
// forwardSignals relays SIGINT/SIGTERM received by vintner itself to
|
||||
// proc's entire process group (proc must have been started via a cmd that
|
||||
// called setNewProcessGroup, making proc.Pid also the group id), escalating
|
||||
// to SIGKILL after killGrace if the group hasn't exited by then. Callers
|
||||
// must call the returned stop func once the process has actually exited
|
||||
// (e.g. right after cmd.Wait() returns), both to stop listening for
|
||||
// signals and to cancel a pending escalation.
|
||||
func forwardSignals(proc *os.Process) (stop func()) {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
pgid := -proc.Pid
|
||||
for {
|
||||
select {
|
||||
case sig := <-sigCh:
|
||||
s, ok := sig.(syscall.Signal)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
_ = syscall.Kill(pgid, s)
|
||||
select {
|
||||
case <-time.After(killGrace):
|
||||
_ = syscall.Kill(pgid, syscall.SIGKILL)
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
signal.Stop(sigCh)
|
||||
close(done)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestForwardSignalsKillsChild verifies the actual mechanism that keeps a
|
||||
// wine subprocess from being orphaned: a SIGTERM delivered to the current
|
||||
// process (mimicking `kill <vintner-pid>`, not an interactive Ctrl-C) must
|
||||
// reach a child started with setNewProcessGroup, even though it's no longer
|
||||
// in the same process group.
|
||||
func TestForwardSignalsKillsChild(t *testing.T) {
|
||||
cmd := exec.Command("sleep", "30")
|
||||
setNewProcessGroup(cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("starting sleep: %v", err)
|
||||
}
|
||||
stop := forwardSignals(cmd.Process)
|
||||
defer stop()
|
||||
|
||||
// signal.Notify (inside forwardSignals) intercepts this rather than
|
||||
// letting it terminate the test binary itself.
|
||||
if err := syscall.Kill(os.Getpid(), syscall.SIGTERM); err != nil {
|
||||
t.Fatalf("signaling self: %v", err)
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- cmd.Wait() }()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err == nil {
|
||||
t.Fatal("expected the child to be killed by the forwarded signal, but it exited successfully")
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
cmd.Process.Kill()
|
||||
t.Fatal("child was still running 3s after the signal should have been forwarded")
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
// filtering its output.
|
||||
package wrapper
|
||||
|
||||
import "github.com/Cheviiot/msvc-go-wine/internal/wineenv"
|
||||
import "github.com/Cheviiot/vintner/internal/wineenv"
|
||||
|
||||
// dirKind selects which install directory a tool's real .exe lives in.
|
||||
type dirKind int
|
||||
|
||||
Reference in New Issue
Block a user