6 Commits
Author SHA1 Message Date
Cheviiot a1743e4435 Rebrand project to vintner
Renamed the GitHub repo, Go module path, binary, and default install
directory from msvc-go-wine to vintner. Updated every user-facing
string (usage text, error prefixes, README, LICENSE, CI/release
workflow) and the embedded compatibility patches' own header text to
match; the VINTNER_LANG env var replaces VSMC_GO_WINE_LANG.

Also fixes a real bug found while re-verifying the rename end-to-end:
Microsoft.Cpp.WindowsSDK.props.patch had LF-only line endings in its
hunk body while the real Microsoft-shipped file it targets is CRLF,
so `git apply` silently failed on every real install and the SDK
detection fix it's meant to provide was never actually taking effect.
Restored matching CRLF endings in the hunk (checked against the other
five patches, which already had this right). Left the patch's
internal MsvcGoWine_ExtraSdkRoots MSBuild property name alone rather
than renaming it too - changing hunk content, even just an identifier,
breaks reverse-apply idempotency for anyone re-running download
against an already-patched tree, which the fix above depends on. Added
a .remove marker so an existing install's old-named props file gets
cleaned up on the next download.

Re-verified end-to-end after the rename: general MSBuild/cl/link
still work, and a real KMDF driver build (compile, link, INF stamping,
Inf2Cat signability check) still succeeds under the renamed binary.

README also gets an accuracy pass: WDK support and the download/env
CLI flags it lists were out of date (WDK was previously listed under
"Known gaps" despite being implemented and verified), the full tool
list was missing mc/cmd/findstr, and the new command aliases and
VINTNER_LANG option are now documented.
2026-07-25 03:55:22 +10:00
Cheviiot 23ea620ce2 Add short subcommand aliases and EN/RU CLI localization
Subcommands gain one/two-letter aliases (dl, i, e, v, h) alongside
their full names. CLI-owned chrome - top-level usage, per-subcommand
usage lines, progress messages, and the license prompt - now goes
through internal/i18n, an env-driven message catalog (MSVC_GO_WINE_LANG,
falling back to the standard LC_ALL/LC_MESSAGES/LANG locale variables)
with English and Russian translations. Deeper error text from internal
packages stays in English.

Also refreshed the top-level usage text, which hadn't kept up with
--with-wdk/--list-workloads/--list-components/--print-deps-tree.
2026-07-25 03:31:59 +10:00
Cheviiot 8551d7fe99 Add Windows Driver Kit (WDK) support for building KMDF/UMDF drivers
download --with-wdk fetches the WDK headers/libs/host-tools NuGet
packages (nuget.org has no vsman-manifest entry for this content) and
lays them out where the DriverKit.BuildTools PlatformToolset expects
them. msbuildEnv wires WDKContentRoot/WDKBuildFolder through
DisableRegistryUse the same way the SDK/toolset paths already are.

Two WDK-package fixups were needed for a real driver to actually
build under Wine: the bundled build-task assembly is versioned for an
older VisualStudioVersion than ours, and the package ships no x86
host-tools directory at all (only x64/arm64), which StampInf hardcodes
a path to.

Also force TZ=UTC for msbuild invocations: StampInf stamps DriverVer
using the local wall-clock date while Inf2Cat validates it against
UTC "now", so any timezone east of UTC sees a "postdated DriverVer"
failure for most of the day.

Verified against a real KMDF sample driver (microsoft/Windows-driver-
samples' echo_2): compiles, links, INF stamps and passes Inf2Cat's
signability check with SignMode=off.
2026-07-25 03:24:44 +10:00
Cheviiot 44fee57ddd Add download --list-workloads/--list-components/--print-deps-tree, and CI
- download --list-workloads / --list-components print every workload or
  component id (with its manifest title) without downloading anything, for
  discovering what to pass as a package id or --with-* toggle.
- download --print-deps-tree prints the dependency tree of whatever the
  current flags would actually select, sharing the exact same
  arch/--ignore/Optional/Recommended filtering ExpandSelection uses so the
  output matches a real download; diamond dependencies are shown once and
  referenced as "(see above)" afterwards to keep it finite.
- Fixed --manifest (offline/predownloaded manifest testing) never having
  worked at all: it builds a "file:" URL but the shared http.Client had no
  handler registered for that scheme.
- Added a CI workflow running gofmt/vet/build/test on every push and PR;
  previously only the tag-triggered release workflow existed.
2026-07-25 02:47:24 +10:00
Cheviiot 47471e007c Fix stdout/stderr pipe hang from lingering Wine background processes
msbuild (and the toolrelay.exe-less fallback path for cl/link/etc.) inherited
os.Stdout/os.Stderr directly into the wine subprocess. Wine's wineserver and
its service processes (services.exe, winedevice.exe, explorer.exe, ...)
inherit those same descriptors and keep running well after the actual build
finishes, so a caller piping our output (`| tee`, `| tail`, CI log capture)
would never see EOF and hang indefinitely - even though the real build
completed in seconds.

Both paths now pipe stdout/stderr through our own copy goroutines, wait for
the tool's own process (not pipe EOF) to determine completion, and grant a
bounded 500ms grace period to drain whatever's already buffered before
moving on. Verified against a real hang (msbuild building freetype.vcxproj
piped through `tail`) and confirmed instant return after the fix, both on
success and on a build error.
2026-07-25 02:39:26 +10:00
Cheviiot 867c915596 Fix MSBuild toolset/SDK detection under Wine
msbuild <project>.vcxproj previously failed with MSB8020 ("build tools for
vNNN cannot be found") because MSBuild's own toolset/SDK resolution reads
a different set of environment variables than cl/link/lib do directly
(VCInstallDir_<N>, VCToolsInstallDir_<N>, VsInstallRoot,
WindowsSdkDir_10, WindowsTargetPlatformVersion, DisableRegistryUse, etc) -
none of which the generic INCLUDE/LIB/WINEPATH env covered.

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

Verified end-to-end: msbuild successfully builds ocornut/imgui's
example_win32_directx11.vcxproj (retargeted from its original v141
PlatformToolset to this install's v145) - compiles all 8 sources and
links against d3d11.lib/d3dcompiler.lib/dxgi.lib, producing a valid
PE32+ executable.
2026-07-25 01:58:18 +10:00
33 changed files with 1140 additions and 228 deletions
+45
View File
@@ -0,0 +1,45 @@
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 ./...
+8 -8
View File
@@ -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
View File
@@ -1,3 +1,3 @@
/msvc-go-wine
/vintner
*.test
.claude/
+2 -2
View File
@@ -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.
+60 -29
View File
@@ -1,4 +1,4 @@
# 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
@@ -6,13 +6,14 @@ approach (download the real MSVC/WinSDK, wrap the compiler under Wine),
implemented independently.
Once installed, you invoke the real Microsoft toolchain exactly like on
Windows: `cl`, `link`, `lib`, `rc`, `midl`, `mt`, `dumpbin`, `msbuild`,
`nmake`, `ml`, `ml64`, `armasm`, `armasm64` all just work from your `PATH`.
Windows: `cl`, `link`, `lib`, `rc`, `midl`, `mc`, `mt`, `dumpbin`, `msbuild`,
`nmake`, `ml`, `ml64`, `armasm`, `armasm64`, plus trivial `cmd`/`findstr`
shims, all just work from your `PATH`.
## How it works
`msvc-go-wine` is one Go binary that behaves differently depending on the
name it's invoked as (a "multi-call binary", like busybox):
`vintner` is one Go binary that behaves differently depending on the name
it's invoked as (a "multi-call binary", like busybox):
- Invoked as `cl`, `link`, `lib`, ... → it loads a small per-architecture
`env.json`, builds the `INCLUDE`/`LIB`/`WINEPATH` environment Wine needs,
@@ -21,22 +22,23 @@ name it's invoked as (a "multi-call binary", like busybox):
runs the real `.exe` under `wine`/`wine64`, and rewrites the tool's output
back from `z:\...` paths to plain unix paths so your build system's error
parsing keeps working.
- Invoked as `msvc-go-wine` → it exposes the `download`, `install`, `env` and
`version` management subcommands described below.
- Invoked as `vintner` → it exposes the `download`, `install`, `env` and
`version` management subcommands described below (each also has a short
alias: `dl`, `i`, `e`, `v`; `help`/`h` prints usage).
## Quick start
```bash
# 1. Download and unpack MSVC + Windows SDK into ~/.msvc-go-wine (requires
# 1. Download and unpack MSVC + Windows SDK into ~/.vintner (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
vintner download --accept-license
# 2. Wire up the tool wrappers
msvc-go-wine install
vintner install
# 3. Add the toolchain to PATH and build
export PATH=~/.msvc-go-wine/bin/x64:$PATH
export PATH=~/.vintner/bin/x64:$PATH
cl /nologo /EHsc hello.cpp
```
@@ -56,19 +58,48 @@ pkcon install wine msitools
## 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
```
`--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`, `--host-arch`, `--only-host`, `--with-wdk` (also fetch the
Windows Driver Kit, for building KMDF/UMDF drivers), `--ignore`,
`--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.
`--list-workloads`/`--list-components` print every workload/component id
(with its human-readable title) available in the fetched manifest and exit
without downloading anything - useful for discovering what to pass as a bare
package id or via `--with-*`. `--print-deps-tree` prints the dependency tree
of whatever would actually be selected (honoring every other flag), also
without downloading.
### Building drivers (WDK)
`vintner download --with-wdk` additionally fetches the Windows Driver Kit
(headers, import libs, and the MSBuild `WindowsKernelModeDriver10.0`/
`WindowsUserModeDriver10.0` PlatformToolsets) so `msbuild` can build real
KMDF/UMDF drivers - compiling, linking, INF stamping and the `Inf2Cat`
signability check (with `SignMode=off`) all work under Wine. Verified
end-to-end 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 (no x86/arm).
### Language
CLI messages (usage text, progress lines, prompts) are in English by
default. Set `VINTNER_LANG=ru` (or have a `ru`-prefixed `LC_ALL`/
`LC_MESSAGES`/`LANG`, e.g. `ru_RU.UTF-8`) for Russian. Deeper error text
bubbled up from internal packages stays in English.
### Using clang-cl/lld-link instead of Wine
@@ -76,7 +107,7 @@ You don't need Wine at all if you drive the (nonredistributable) MSVC/WinSDK
headers and libraries with Clang/LLD in MSVC-compatible mode:
```bash
eval "$(msvc-go-wine env --bin ~/.msvc-go-wine/bin/x64)"
eval "$(vintner env --bin ~/.vintner/bin/x64)"
clang-cl -c hello.c
lld-link hello.obj -out:hello.exe
```
@@ -84,7 +115,7 @@ lld-link hello.obj -out:hello.exe
## Building from source
```bash
go build -o msvc-go-wine ./cmd/msvc-go-wine
go build -o vintner ./cmd/vintner
```
Go 1.23+ is all you need to build it; `wine`/`msitools` are only needed at
@@ -116,14 +147,14 @@ telemetry, and don't hard-fail devcmd setup when an optional component
## Known gaps
- `download` doesn't yet support printing the dependency/reverse-dependency
tree, listing available workloads/components/packages, or installing the
Windows Driver Kit via `--with-wdk-installers`; the core selection/
download/unpack/install pipeline is fully implemented.
None currently tracked. Download/select/unpack/install, general MSBuild
projects, WDK driver builds, dependency-tree printing, and
workload/component listing are all implemented and verified against real
projects.
## 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
MIT, see [LICENSE.txt](LICENSE.txt) - covers vintner's own source only. The
MSVC Build Tools / Windows SDK / WDK that `download` fetches remain governed
by 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,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
+1 -1
View File
@@ -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
-77
View File
@@ -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
`)
}
@@ -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
@@ -51,13 +56,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 +73,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 +105,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 +124,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 +145,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 +162,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 +171,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 +180,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 +195,78 @@ 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)
}
}
}
func detectHostArch() string {
if runtime.GOARCH == "arm64" {
return "arm64"
@@ -192,7 +275,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 +284,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
}
@@ -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
}
@@ -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
}
+65
View File
@@ -0,0 +1,65 @@
// 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 "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"))
}
@@ -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
}
+1 -1
View File
@@ -1,3 +1,3 @@
module github.com/Cheviiot/msvc-go-wine
module github.com/Cheviiot/vintner
go 1.23
+18
View File
@@ -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
}
+50 -4
View File
@@ -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,6 +184,15 @@ 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) {
+2 -2
View File
@@ -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
}
+82 -2
View File
@@ -2,7 +2,9 @@ package download
import (
"fmt"
"io"
"regexp"
"sort"
"strings"
)
@@ -41,7 +43,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 +250,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")
}
@@ -367,3 +377,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)
}
}
+264
View File
@@ -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
}
+176
View File
@@ -0,0 +1,176 @@
// 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
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.
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 <команда> --help» для параметров конкретной команды —
у download их много, включая --with-wdk, --list-workloads, --list-components
и --print-deps-tree.
--dest/[каталог] по умолчанию — ~/.vintner.
Язык: установите VINTNER_LANG=en (или LANG=en_US...) для вывода на английском.
После установки добавьте <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»: ",
},
}
+13 -13
View File
@@ -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
}
}
+1 -1
View File
@@ -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.
+13
View File
@@ -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,
+108
View File
@@ -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
}
}
+97 -39
View File
@@ -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,18 @@ 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)
exitCode = runRawStdout(cmd)
default:
relay := filepath.Join(paths.BaseUnix, "bin", toolRelayName)
if fi, err := os.Stat(relay); err == nil && !fi.IsDir() {
@@ -104,17 +119,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)
@@ -129,7 +144,7 @@ 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
}
@@ -161,12 +176,63 @@ 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
}
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 +265,34 @@ 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
}
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
@@ -242,14 +311,3 @@ 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
}
return 0
}
+1 -1
View File
@@ -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