mirror of
https://github.com/Cheviiot/Vintner.git
synced 2026-08-03 15:57:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1743e4435 | ||
|
|
23ea620ce2 | ||
|
|
8551d7fe99 | ||
|
|
44fee57ddd | ||
|
|
47471e007c | ||
|
|
867c915596 | ||
|
|
8f52e5861a |
@@ -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 ./...
|
||||
@@ -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,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,21 +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 (requires accepting Microsoft's
|
||||
# Visual Studio Build Tools license, and msitools for unpacking .msi payloads)
|
||||
msvc-go-wine download --accept-license --dest ~/my_msvc
|
||||
# 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.
|
||||
vintner download --accept-license
|
||||
|
||||
# 2. Wire up the tool wrappers
|
||||
msvc-go-wine install ~/my_msvc
|
||||
vintner install
|
||||
|
||||
# 3. Add the toolchain to PATH and build
|
||||
export PATH=~/my_msvc/bin/x64:$PATH
|
||||
export PATH=~/.vintner/bin/x64:$PATH
|
||||
cl /nologo /EHsc hello.cpp
|
||||
```
|
||||
|
||||
@@ -55,17 +58,48 @@ pkcon install wine msitools
|
||||
## Commands
|
||||
|
||||
```
|
||||
msvc-go-wine download --dest <dir> [options] fetch and unpack MSVC/WinSDK
|
||||
msvc-go-wine install <dir> wire up wrappers for a downloaded MSVC
|
||||
msvc-go-wine env --bin <dir>/bin/<arch> print INCLUDE/LIB for native clang-cl/lld-link use
|
||||
msvc-go-wine version print the version
|
||||
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
|
||||
```
|
||||
|
||||
`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.
|
||||
`--dest`/`[dir]` both default to `~/.vintner` when omitted.
|
||||
|
||||
`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
|
||||
|
||||
@@ -73,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 ~/my_msvc/bin/x64)"
|
||||
eval "$(vintner env --bin ~/.vintner/bin/x64)"
|
||||
clang-cl -c hello.c
|
||||
lld-link hello.obj -out:hello.exe
|
||||
```
|
||||
@@ -81,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
|
||||
@@ -113,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
-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,29 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/internal/install"
|
||||
)
|
||||
|
||||
func runInstall(args []string) int {
|
||||
if len(args) != 1 || args[0] == "-h" || args[0] == "--help" {
|
||||
fmt.Fprintln(os.Stderr, "usage: msvc-go-wine install <dest>")
|
||||
return 1
|
||||
}
|
||||
dest := args[0]
|
||||
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine install:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := install.Install(dest, self); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine install:", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Println("Done. Add", dest+"/bin/<arch> to PATH to use cl, link, lib, ...")
|
||||
return 0
|
||||
}
|
||||
@@ -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 (required unless --only-download)")
|
||||
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
|
||||
@@ -122,12 +143,17 @@ func runDownload(args []string) int {
|
||||
}
|
||||
|
||||
if !*onlyDownload && *dest == "" {
|
||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download: --dest is required unless --only-download is set")
|
||||
def, err := defaultToolchainDir()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
*dest = def
|
||||
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 {
|
||||
@@ -136,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
|
||||
}
|
||||
|
||||
@@ -145,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
|
||||
}
|
||||
|
||||
@@ -154,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 {
|
||||
@@ -169,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"
|
||||
@@ -187,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() {
|
||||
@@ -196,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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"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, i18n.T("install.usage"))
|
||||
return 1
|
||||
}
|
||||
|
||||
var dest string
|
||||
if len(args) == 1 {
|
||||
dest = args[0]
|
||||
} else {
|
||||
def, err := defaultToolchainDir()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "vintner install:", err)
|
||||
return 1
|
||||
}
|
||||
dest = def
|
||||
fmt.Println(i18n.T("install.default_dir", dest))
|
||||
}
|
||||
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "vintner install:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := install.Install(dest, self); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "vintner install:", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Println(i18n.T("install.done", dest+"/bin/<arch>"))
|
||||
return 0
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Command msvc-go-wine cross compiles with the real MSVC toolchain on Linux
|
||||
// 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 `msvc-go-wine install`), and
|
||||
// those names (via symlinks set up by `vintner install`), and
|
||||
// otherwise exposes the `download`/`install`/`env`/`version` management
|
||||
// subcommands.
|
||||
package main
|
||||
@@ -12,7 +12,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Cheviiot/msvc-go-wine/internal/wrapper"
|
||||
"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";
|
||||
@@ -40,35 +41,25 @@ func runCLI(args []string) int {
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "download":
|
||||
case "download", "dl":
|
||||
return runDownload(args[1:])
|
||||
case "install":
|
||||
case "install", "i":
|
||||
return runInstall(args[1:])
|
||||
case "env":
|
||||
case "env", "e":
|
||||
return runEnv(args[1:])
|
||||
case "version":
|
||||
fmt.Println("msvc-go-wine " + version)
|
||||
case "version", "v", "--version":
|
||||
fmt.Println("vintner " + version)
|
||||
return 0
|
||||
case "-h", "--help", "help":
|
||||
case "-h", "--help", "help", "h":
|
||||
printUsage()
|
||||
return 0
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "msvc-go-wine: unknown subcommand %q\n\n", args[0])
|
||||
fmt.Fprint(os.Stderr, i18n.T("main.unknown_subcommand", args[0]))
|
||||
printUsage()
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Fprint(os.Stderr, `msvc-go-wine - cross compile with MSVC on Linux via Wine
|
||||
|
||||
Usage:
|
||||
msvc-go-wine download --dest <dir> [options] fetch and unpack MSVC/WinSDK
|
||||
msvc-go-wine install <dir> wire up wrappers for a downloaded MSVC
|
||||
msvc-go-wine env --bin <dir/bin/arch> print INCLUDE/LIB for native clang-cl/lld-link use
|
||||
msvc-go-wine version print the version
|
||||
|
||||
Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
|
||||
cl, link, lib, ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd, findstr
|
||||
`)
|
||||
fmt.Fprint(os.Stderr, i18n.T("main.usage"))
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// defaultToolchainDir is where `download`/`install` operate when the user
|
||||
// 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, ".vintner"), nil
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
module github.com/Cheviiot/msvc-go-wine
|
||||
module github.com/Cheviiot/vintner
|
||||
|
||||
go 1.23
|
||||
|
||||
@@ -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,10 +40,14 @@ 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"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
License string `json:"license"`
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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
@@ -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
|
||||
}
|
||||
}
|
||||
+97
-39
@@ -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
|
||||
}
|
||||
|
||||
@@ -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