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 }}"
|
tag="${{ inputs.tag || github.ref_name }}"
|
||||||
version="${tag#v}"
|
version="${tag#v}"
|
||||||
go build -trimpath -ldflags="-s -w -X main.version=${version}" \
|
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
|
- name: Verify it runs
|
||||||
if: matrix.goarch == 'amd64'
|
if: matrix.goarch == 'amd64'
|
||||||
run: ./msvc-go-wine-linux-amd64 version
|
run: ./vintner-linux-amd64 version
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: msvc-go-wine-linux-${{ matrix.goarch }}
|
name: vintner-linux-${{ matrix.goarch }}
|
||||||
path: msvc-go-wine-linux-${{ matrix.goarch }}
|
path: vintner-linux-${{ matrix.goarch }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
@@ -71,11 +71,11 @@ jobs:
|
|||||||
- name: Download all architectures
|
- name: Download all architectures
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
pattern: msvc-go-wine-linux-*
|
pattern: vintner-linux-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Checksums
|
- name: Checksums
|
||||||
run: sha256sum msvc-go-wine-linux-* > SHA256SUMS
|
run: sha256sum vintner-linux-* > SHA256SUMS
|
||||||
|
|
||||||
- name: Create or update GitHub Release
|
- name: Create or update GitHub Release
|
||||||
env:
|
env:
|
||||||
@@ -85,10 +85,10 @@ jobs:
|
|||||||
tag="${{ inputs.tag || github.ref_name }}"
|
tag="${{ inputs.tag || github.ref_name }}"
|
||||||
if ! gh release view "$tag" >/dev/null 2>&1; then
|
if ! gh release view "$tag" >/dev/null 2>&1; then
|
||||||
gh release create "$tag" \
|
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."
|
--notes "Linux amd64/arm64 binaries built by CI from this tag. See README.md for install instructions."
|
||||||
fi
|
fi
|
||||||
gh release upload "$tag" \
|
gh release upload "$tag" \
|
||||||
msvc-go-wine-linux-* \
|
vintner-linux-* \
|
||||||
SHA256SUMS \
|
SHA256SUMS \
|
||||||
--clobber
|
--clobber
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
/msvc-go-wine
|
/vintner
|
||||||
*.test
|
*.test
|
||||||
.claude/
|
.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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
This license covers msvc-go-wine's own source code. It does not cover the
|
This license covers vintner's own source code. It does not cover the
|
||||||
Microsoft Visual C++ Build Tools / Windows SDK that `msvc-go-wine download`
|
Microsoft Visual C++ Build Tools / Windows SDK that `vintner download`
|
||||||
fetches and unpacks, which remain governed by Microsoft's own license terms.
|
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
|
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
|
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.
|
implemented independently.
|
||||||
|
|
||||||
Once installed, you invoke the real Microsoft toolchain exactly like on
|
Once installed, you invoke the real Microsoft toolchain exactly like on
|
||||||
Windows: `cl`, `link`, `lib`, `rc`, `midl`, `mt`, `dumpbin`, `msbuild`,
|
Windows: `cl`, `link`, `lib`, `rc`, `midl`, `mc`, `mt`, `dumpbin`, `msbuild`,
|
||||||
`nmake`, `ml`, `ml64`, `armasm`, `armasm64` all just work from your `PATH`.
|
`nmake`, `ml`, `ml64`, `armasm`, `armasm64`, plus trivial `cmd`/`findstr`
|
||||||
|
shims, all just work from your `PATH`.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
`msvc-go-wine` is one Go binary that behaves differently depending on the
|
`vintner` is one Go binary that behaves differently depending on the name
|
||||||
name it's invoked as (a "multi-call binary", like busybox):
|
it's invoked as (a "multi-call binary", like busybox):
|
||||||
|
|
||||||
- Invoked as `cl`, `link`, `lib`, ... → it loads a small per-architecture
|
- Invoked as `cl`, `link`, `lib`, ... → it loads a small per-architecture
|
||||||
`env.json`, builds the `INCLUDE`/`LIB`/`WINEPATH` environment Wine needs,
|
`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
|
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
|
back from `z:\...` paths to plain unix paths so your build system's error
|
||||||
parsing keeps working.
|
parsing keeps working.
|
||||||
- Invoked as `msvc-go-wine` → it exposes the `download`, `install`, `env` and
|
- Invoked as `vintner` → it exposes the `download`, `install`, `env` and
|
||||||
`version` management subcommands described below.
|
`version` management subcommands described below (each also has a short
|
||||||
|
alias: `dl`, `i`, `e`, `v`; `help`/`h` prints usage).
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Download and unpack MSVC + Windows SDK (requires accepting Microsoft's
|
# 1. Download and unpack MSVC + Windows SDK into ~/.vintner (requires
|
||||||
# Visual Studio Build Tools license, and msitools for unpacking .msi payloads)
|
# accepting Microsoft's Visual Studio Build Tools license, and msitools
|
||||||
msvc-go-wine download --accept-license --dest ~/my_msvc
|
# for unpacking .msi payloads). Pass --dest <dir> for a different location.
|
||||||
|
vintner download --accept-license
|
||||||
|
|
||||||
# 2. Wire up the tool wrappers
|
# 2. Wire up the tool wrappers
|
||||||
msvc-go-wine install ~/my_msvc
|
vintner install
|
||||||
|
|
||||||
# 3. Add the toolchain to PATH and build
|
# 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
|
cl /nologo /EHsc hello.cpp
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -55,17 +58,48 @@ pkcon install wine msitools
|
|||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```
|
```
|
||||||
msvc-go-wine download --dest <dir> [options] fetch and unpack MSVC/WinSDK
|
vintner download (dl) --accept-license [--dest <dir>] [options] fetch and unpack MSVC/WinSDK/WDK
|
||||||
msvc-go-wine install <dir> wire up wrappers for a downloaded MSVC
|
vintner install (i) [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
|
vintner env (e) --bin <dir>/bin/<arch> print INCLUDE/LIB for native clang-cl/lld-link use
|
||||||
msvc-go-wine version print the version
|
vintner version (v) print the version
|
||||||
|
vintner help (h) print usage
|
||||||
```
|
```
|
||||||
|
|
||||||
`download` supports `--msvc-version`, `--sdk-version`, `--architecture`,
|
`--dest`/`[dir]` both default to `~/.vintner` when omitted.
|
||||||
`--host-arch`, `--with-*` component toggles, `--ignore`, `--only-download`,
|
|
||||||
`--only-unpack`, `--keep-unpack`, `--cache`, `--language`,
|
`download`'s main options: `--msvc-version`, `--sdk-version`,
|
||||||
`--include-optional`, `--skip-recommended`, `--major`, `--preview`,
|
`--architecture`, `--host-arch`, `--only-host`, `--with-wdk` (also fetch the
|
||||||
`--manifest`. Run `msvc-go-wine download -h` for the full list.
|
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
|
### 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:
|
headers and libraries with Clang/LLD in MSVC-compatible mode:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
eval "$(msvc-go-wine env --bin ~/my_msvc/bin/x64)"
|
eval "$(vintner env --bin ~/.vintner/bin/x64)"
|
||||||
clang-cl -c hello.c
|
clang-cl -c hello.c
|
||||||
lld-link hello.obj -out:hello.exe
|
lld-link hello.obj -out:hello.exe
|
||||||
```
|
```
|
||||||
@@ -81,7 +115,7 @@ lld-link hello.obj -out:hello.exe
|
|||||||
## Building from source
|
## Building from source
|
||||||
|
|
||||||
```bash
|
```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
|
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
|
## Known gaps
|
||||||
|
|
||||||
- `download` doesn't yet support printing the dependency/reverse-dependency
|
None currently tracked. Download/select/unpack/install, general MSBuild
|
||||||
tree, listing available workloads/components/packages, or installing the
|
projects, WDK driver builds, dependency-tree printing, and
|
||||||
Windows Driver Kit via `--with-wdk-installers`; the core selection/
|
workload/component listing are all implemented and verified against real
|
||||||
download/unpack/install pipeline is fully implemented.
|
projects.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT, see [LICENSE.txt](LICENSE.txt) - covers msvc-go-wine's own source only.
|
MIT, see [LICENSE.txt](LICENSE.txt) - covers vintner's own source only. The
|
||||||
The MSVC Build Tools / Windows SDK that `download` fetches remain governed
|
MSVC Build Tools / Windows SDK / WDK that `download` fetches remain governed
|
||||||
by Microsoft's own license (accepted via `--accept-license`), same as with
|
by Microsoft's own license (accepted via `--accept-license`), same as with
|
||||||
any other way of obtaining them.
|
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
|
There's no telemetry endpoint reachable (or wanted) under Wine. Rather than
|
||||||
touching the conditional logic below, just set the opt-out switch the
|
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
|
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
|
-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
|
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
|
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
|
Not every MSVC/WinSDK download selection includes the Linux
|
||||||
ConnectionManagerExe component; a missing optional component shouldn't
|
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
|
Same reasoning as the ConnectionManagerExe fix: a missing optional
|
||||||
component shouldn't abort the whole Developer Command Prompt setup.
|
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
|
architecture from the VS install layout directly, since there's no
|
||||||
Windows Registry under Wine for the stock .props files to query.
|
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
|
GetLatestSDKTargetPlatformVersion normally resolves the SDK via the
|
||||||
registry, which has nothing to find under Wine. Pass it an extra list of
|
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.
|
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
|
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
|
// 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).
|
// buildToolRelay and internal/wrapper's runViaToolRelay).
|
||||||
//
|
//
|
||||||
// It exists to solve two problems that are only solvable from inside a
|
// 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"
|
"path/filepath"
|
||||||
"runtime"
|
"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 {
|
func runDownload(args []string) int {
|
||||||
fs := flag.NewFlagSet("download", flag.ContinueOnError)
|
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)")
|
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")
|
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")
|
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")
|
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")
|
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")
|
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
|
var archsFlag stringList
|
||||||
fs.Var(&archsFlag, "architecture", "target architecture to include (x86, x64, arm, arm64, host); repeatable")
|
fs.Var(&archsFlag, "architecture", "target architecture to include (x86, x64, arm, arm64, host); repeatable")
|
||||||
var ignoreFlag stringList
|
var ignoreFlag stringList
|
||||||
@@ -51,13 +56,14 @@ func runDownload(args []string) int {
|
|||||||
IncludeOptional: *includeOptional,
|
IncludeOptional: *includeOptional,
|
||||||
SkipRecommended: *skipRecommended,
|
SkipRecommended: *skipRecommended,
|
||||||
Language: *language,
|
Language: *language,
|
||||||
|
WithWDK: *withWDK,
|
||||||
}
|
}
|
||||||
|
|
||||||
manifestURL := *manifestFile
|
manifestURL := *manifestFile
|
||||||
if manifestURL == "" {
|
if manifestURL == "" {
|
||||||
url, err := download.FetchChannelManifest(*major, *preview)
|
url, err := download.FetchChannelManifest(*major, *preview)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
manifestURL = url
|
manifestURL = url
|
||||||
@@ -67,18 +73,28 @@ func runDownload(args []string) int {
|
|||||||
|
|
||||||
manifest, err := download.FetchInstallerManifest(manifestURL)
|
manifest, err := download.FetchInstallerManifest(manifestURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.HostArch == "" {
|
if opts.HostArch == "" {
|
||||||
opts.HostArch = detectHostArch()
|
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)
|
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"
|
license := "the Visual Studio Build Tools license"
|
||||||
if p := idx.Find("Microsoft.VisualStudio.Product.BuildTools", nil); p != nil && len(p.LocalizedResources) > 0 {
|
if p := idx.Find("Microsoft.VisualStudio.Product.BuildTools", nil); p != nil && len(p.LocalizedResources) > 0 {
|
||||||
license = p.LocalizedResources[0].License
|
license = p.LocalizedResources[0].License
|
||||||
@@ -89,13 +105,18 @@ func runDownload(args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := download.ResolveSelection(opts, idx); err != nil {
|
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
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *printDepsTree {
|
||||||
|
download.PrintDependencyTree(os.Stdout, idx, opts)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
selected, err := download.ExpandSelection(idx, opts)
|
selected, err := download.ExpandSelection(idx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
var downloadSize, installSize int64
|
var downloadSize, installSize int64
|
||||||
@@ -103,15 +124,15 @@ func runDownload(args []string) int {
|
|||||||
downloadSize += p.DownloadSize()
|
downloadSize += p.DownloadSize()
|
||||||
installSize += p.InstalledSize()
|
installSize += p.InstalledSize()
|
||||||
}
|
}
|
||||||
fmt.Printf("Selected %d packages, for a total download size of %s, install size of %s\n",
|
fmt.Print(i18n.T("download.selected",
|
||||||
len(selected), download.HumanizeBytes(downloadSize), download.HumanizeBytes(installSize))
|
len(selected), download.HumanizeBytes(downloadSize), download.HumanizeBytes(installSize)))
|
||||||
|
|
||||||
cache := *cacheDir
|
cache := *cacheDir
|
||||||
removeCache := false
|
removeCache := false
|
||||||
if cache == "" {
|
if cache == "" {
|
||||||
tmp, err := os.MkdirTemp("", "msvc-go-wine-cache-")
|
tmp, err := os.MkdirTemp("", "vintner-cache-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
cache = tmp
|
cache = tmp
|
||||||
@@ -122,12 +143,17 @@ func runDownload(args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !*onlyDownload && *dest == "" {
|
if !*onlyDownload && *dest == "" {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download: --dest is required unless --only-download is set")
|
def, err := defaultToolchainDir()
|
||||||
return 1
|
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 {
|
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
|
return 1
|
||||||
}
|
}
|
||||||
if *onlyDownload {
|
if *onlyDownload {
|
||||||
@@ -136,7 +162,7 @@ func runDownload(args []string) int {
|
|||||||
|
|
||||||
destAbs, err := filepath.Abs(*dest)
|
destAbs, err := filepath.Abs(*dest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err)
|
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +171,7 @@ func runDownload(args []string) int {
|
|||||||
unpack = filepath.Join(destAbs, "unpack")
|
unpack = filepath.Join(destAbs, "unpack")
|
||||||
}
|
}
|
||||||
if err := download.UnpackSelectedPackages(selected, cache, unpack); err != nil {
|
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
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,14 +180,14 @@ func runDownload(args []string) int {
|
|||||||
for _, hostArch := range []string{"amd64", "arm64"} {
|
for _, hostArch := range []string{"amd64", "arm64"} {
|
||||||
msbuildExe := filepath.Join(unpack, "MSBuild", "Current", "Bin", hostArch, "MSBuild.exe")
|
msbuildExe := filepath.Join(unpack, "MSBuild", "Current", "Bin", hostArch, "MSBuild.exe")
|
||||||
if err := download.CopyRedirectedAssemblies(msbuildExe); err != nil {
|
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
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !*onlyUnpack {
|
if !*onlyUnpack {
|
||||||
if err := download.RelocateBuildTools(unpack, destAbs); err != nil {
|
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
|
return 1
|
||||||
}
|
}
|
||||||
if !*keepUnpack {
|
if !*keepUnpack {
|
||||||
@@ -169,16 +195,78 @@ func runDownload(args []string) int {
|
|||||||
}
|
}
|
||||||
if !*skipPatch && *major == 18 {
|
if !*skipPatch && *major == 18 {
|
||||||
if err := download.ApplyCompatibilityFixes(destAbs); err != nil {
|
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
|
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
|
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 {
|
func detectHostArch() string {
|
||||||
if runtime.GOARCH == "arm64" {
|
if runtime.GOARCH == "arm64" {
|
||||||
return "arm64"
|
return "arm64"
|
||||||
@@ -187,7 +275,7 @@ func detectHostArch() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func promptAcceptLicense(license string) bool {
|
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)
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
switch scanner.Text() {
|
switch scanner.Text() {
|
||||||
@@ -196,7 +284,7 @@ func promptAcceptLicense(license string) bool {
|
|||||||
case "no":
|
case "no":
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
fmt.Print("Do you accept the license? Answer \"yes\" or \"no\": ")
|
fmt.Print(i18n.T("download.license_reprompt"))
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -6,39 +6,40 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"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
|
// runEnv prints shell `export` statements for INCLUDE/LIB (converted from
|
||||||
// wine's "z:\..." notation to plain unix paths) and TARGET_TRIPLE, for
|
// wine's "z:\..." notation to plain unix paths) and TARGET_TRIPLE, for
|
||||||
// driving clang-cl/lld-link directly without Wine.
|
// 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 {
|
func runEnv(args []string) int {
|
||||||
fs := flag.NewFlagSet("env", flag.ContinueOnError)
|
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 {
|
if err := fs.Parse(args); err != nil {
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
if *bin == "" {
|
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
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := wineenv.Load(*bin)
|
cfg, err := wineenv.Load(*bin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine env:", err)
|
fmt.Fprintln(os.Stderr, "vintner env:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
baseUnix, err := wineenv.FindBaseUnix(*bin)
|
baseUnix, err := wineenv.FindBaseUnix(*bin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine env:", err)
|
fmt.Fprintln(os.Stderr, "vintner env:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
paths := wineenv.NewPaths(cfg, baseUnix)
|
paths := wineenv.NewPaths(cfg, baseUnix)
|
||||||
|
|
||||||
triple, ok := targetTriples[cfg.Arch]
|
triple, ok := targetTriples[cfg.Arch]
|
||||||
if !ok {
|
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
|
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`,
|
// 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
|
// `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
|
// otherwise exposes the `download`/`install`/`env`/`version` management
|
||||||
// subcommands.
|
// subcommands.
|
||||||
package main
|
package main
|
||||||
@@ -12,7 +12,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"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";
|
// 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] {
|
switch args[0] {
|
||||||
case "download":
|
case "download", "dl":
|
||||||
return runDownload(args[1:])
|
return runDownload(args[1:])
|
||||||
case "install":
|
case "install", "i":
|
||||||
return runInstall(args[1:])
|
return runInstall(args[1:])
|
||||||
case "env":
|
case "env", "e":
|
||||||
return runEnv(args[1:])
|
return runEnv(args[1:])
|
||||||
case "version":
|
case "version", "v", "--version":
|
||||||
fmt.Println("msvc-go-wine " + version)
|
fmt.Println("vintner " + version)
|
||||||
return 0
|
return 0
|
||||||
case "-h", "--help", "help":
|
case "-h", "--help", "help", "h":
|
||||||
printUsage()
|
printUsage()
|
||||||
return 0
|
return 0
|
||||||
default:
|
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()
|
printUsage()
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printUsage() {
|
func printUsage() {
|
||||||
fmt.Fprint(os.Stderr, `msvc-go-wine - cross compile with MSVC on Linux via Wine
|
fmt.Fprint(os.Stderr, i18n.T("main.usage"))
|
||||||
|
|
||||||
Usage:
|
|
||||||
msvc-go-wine download --dest <dir> [options] fetch and unpack MSVC/WinSDK
|
|
||||||
msvc-go-wine install <dir> wire up wrappers for a downloaded MSVC
|
|
||||||
msvc-go-wine env --bin <dir/bin/arch> print INCLUDE/LIB for native clang-cl/lld-link use
|
|
||||||
msvc-go-wine version print the version
|
|
||||||
|
|
||||||
Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
|
|
||||||
cl, link, lib, ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd, findstr
|
|
||||||
`)
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,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
|
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,11 +40,15 @@ type Dependency struct {
|
|||||||
Type string // "", "Optional" or "Recommended"
|
Type string // "", "Optional" or "Recommended"
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalizedResource carries the license URL shown before accepting a
|
// LocalizedResource carries a package's human-readable title/description
|
||||||
// package's terms.
|
// (shown by --list-workloads/--list-components) and the license URL shown
|
||||||
|
// before accepting a package's terms.
|
||||||
type LocalizedResource struct {
|
type LocalizedResource struct {
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
License string `json:"license"`
|
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.
|
// Package is one entry from the installer manifest's "packages" array.
|
||||||
@@ -108,6 +112,39 @@ func (p *Package) Key() string {
|
|||||||
return key
|
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 {
|
func (p *Package) InstalledSize() int64 {
|
||||||
var sum int64
|
var sum int64
|
||||||
for _, v := range p.InstallSizes {
|
for _, v := range p.InstallSizes {
|
||||||
@@ -147,6 +184,15 @@ type Manifest struct {
|
|||||||
// restart of `download`.
|
// restart of `download`.
|
||||||
var httpClient = &http.Client{Timeout: 5 * time.Minute}
|
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
|
const maxManifestAttempts = 5
|
||||||
|
|
||||||
func httpGet(url string) ([]byte, error) {
|
func httpGet(url string) ([]byte, error) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Cheviiot/msvc-go-wine/assets"
|
"github.com/Cheviiot/vintner/assets"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ApplyCompatibilityFixes applies the embedded Wine compatibility patches
|
// ApplyCompatibilityFixes applies the embedded Wine compatibility patches
|
||||||
@@ -63,7 +63,7 @@ func applyGitPatch(dest, embeddedPath, target string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tmp, err := os.CreateTemp("", "msvc-go-wine-*.patch")
|
tmp, err := os.CreateTemp("", "vintner-*.patch")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package download
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,7 +43,15 @@ type Options struct {
|
|||||||
SkipRecommended bool
|
SkipRecommended bool
|
||||||
Language string
|
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) {
|
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.VC.vcvars")
|
||||||
addIfWanted(opts, opts.WithDevCmd, "Microsoft.VisualStudio.PackageGroup.VsDevCmd")
|
addIfWanted(opts, opts.WithDevCmd, "Microsoft.VisualStudio.PackageGroup.VsDevCmd")
|
||||||
|
|
||||||
if opts.WithWDKInstallers != "" {
|
if opts.WithWDK {
|
||||||
opts.Package = append(opts.Package, "Component.Microsoft.Windows.DriverKit.BuildTools")
|
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
|
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
|
// wrapper commands can find it: locating the installed toolchain/SDK
|
||||||
// versions, fixing up header/library name casing, laying out the
|
// versions, fixing up header/library name casing, laying out the
|
||||||
// per-architecture tool symlinks, and building the toolrelay helper.
|
// per-architecture tool symlinks, and building the toolrelay helper.
|
||||||
@@ -14,16 +14,16 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Cheviiot/msvc-go-wine/assets"
|
"github.com/Cheviiot/vintner/assets"
|
||||||
"github.com/Cheviiot/msvc-go-wine/internal/wineenv"
|
"github.com/Cheviiot/vintner/internal/wineenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var archs = []string{"x86", "x64", "arm", "arm64"}
|
var archs = []string{"x86", "x64", "arm", "arm64"}
|
||||||
|
|
||||||
// Install wires up dest (a directory previously populated by
|
// 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
|
// 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.
|
// the arch-specific tool symlinks have something to point at.
|
||||||
func Install(dest, selfBinary string) error {
|
func Install(dest, selfBinary string) error {
|
||||||
dest, err := filepath.Abs(dest)
|
dest, err := filepath.Abs(dest)
|
||||||
@@ -86,7 +86,7 @@ func Install(dest, selfBinary string) error {
|
|||||||
|
|
||||||
kits10 := filepath.Join(dest, "kits", "10")
|
kits10 := filepath.Join(dest, "kits", "10")
|
||||||
if !isDir(kits10) {
|
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 {
|
if err := lnS("Lib", filepath.Join(kits10, "lib")); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -148,7 +148,7 @@ func Install(dest, selfBinary string) error {
|
|||||||
if err := os.MkdirAll(destBin, 0o755); err != nil {
|
if err := os.MkdirAll(destBin, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
sharedBinary := filepath.Join(destBin, "msvc-go-wine")
|
sharedBinary := filepath.Join(destBin, "vintner")
|
||||||
if err := copyFile(selfBinary, sharedBinary, 0o755); err != nil {
|
if err := copyFile(selfBinary, sharedBinary, 0o755); err != nil {
|
||||||
return fmt.Errorf("installing shared binary: %w", err)
|
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
|
// 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.
|
// symlinks every tool name to that LOCAL copy.
|
||||||
//
|
//
|
||||||
// This matters: the wrapper runtime locates its own install root via
|
// 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
|
// 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
|
// 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
|
// 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
|
// symlink (cl -> vintner) resolves to a binary that's still in the
|
||||||
// right arch dir; a symlink to a binary one level up (cl -> ../msvc-go-wine)
|
// right arch dir; a symlink to a binary one level up (cl -> ../vintner)
|
||||||
// would not be.
|
// would not be.
|
||||||
func setupWrapperDir(destBin, selfBinary, arch, host, dotnetHost, msvcVer, sdkVer string) error {
|
func setupWrapperDir(destBin, selfBinary, arch, host, dotnetHost, msvcVer, sdkVer string) error {
|
||||||
archDir := filepath.Join(destBin, arch)
|
archDir := filepath.Join(destBin, arch)
|
||||||
if err := os.MkdirAll(archDir, 0o755); err != nil {
|
if err := os.MkdirAll(archDir, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
localBinary := filepath.Join(archDir, "msvc-go-wine")
|
localBinary := filepath.Join(archDir, "vintner")
|
||||||
if err := copyFile(selfBinary, localBinary, 0o755); err != nil {
|
if err := copyFile(selfBinary, localBinary, 0o755); err != nil {
|
||||||
return fmt.Errorf("installing per-arch binary: %w", err)
|
return fmt.Errorf("installing per-arch binary: %w", err)
|
||||||
}
|
}
|
||||||
for name := range toolNames {
|
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
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ConfigFileName is the per-architecture config dropped next to the tool
|
// 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"
|
const ConfigFileName = "env.json"
|
||||||
|
|
||||||
// Config is the per-architecture info generated at install time.
|
// 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
|
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
|
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
|
Include string
|
||||||
Lib string
|
Lib string
|
||||||
LibPath string
|
LibPath string
|
||||||
@@ -89,6 +97,11 @@ func NewPaths(cfg *Config, baseUnix string) *Paths {
|
|||||||
SDKBinDir: sdkBinDir,
|
SDKBinDir: sdkBinDir,
|
||||||
MSBuildBinDir: msbuildBinDir,
|
MSBuildBinDir: msbuildBinDir,
|
||||||
|
|
||||||
|
BaseWin: winBase,
|
||||||
|
MSVCBaseWin: msvcBase,
|
||||||
|
MSVCDirWin: msvcDirWin,
|
||||||
|
SDKBaseWin: sdkBase,
|
||||||
|
|
||||||
Include: include,
|
Include: include,
|
||||||
Lib: lib,
|
Lib: lib,
|
||||||
LibPath: 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"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"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.
|
// toolrelay.exe helper, shared across all arch bin dirs.
|
||||||
const toolRelayName = "toolrelay.exe"
|
const toolRelayName = "toolrelay.exe"
|
||||||
|
|
||||||
@@ -27,7 +37,7 @@ func Run(tool string, args []string) int {
|
|||||||
|
|
||||||
s, ok := Tools[tool]
|
s, ok := Tools[tool]
|
||||||
if !ok {
|
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
|
return 127
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,19 +50,19 @@ func Run(tool string, args []string) int {
|
|||||||
// <dest>/bin/<arch>, not <dest>/bin.
|
// <dest>/bin/<arch>, not <dest>/bin.
|
||||||
exePath, err := os.Executable()
|
exePath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
scriptDir := filepath.Dir(exePath)
|
scriptDir := filepath.Dir(exePath)
|
||||||
|
|
||||||
cfg, err := wineenv.Load(scriptDir)
|
cfg, err := wineenv.Load(scriptDir)
|
||||||
if err != nil {
|
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
|
return 1
|
||||||
}
|
}
|
||||||
baseUnix, err := wineenv.FindBaseUnix(scriptDir)
|
baseUnix, err := wineenv.FindBaseUnix(scriptDir)
|
||||||
if err != nil {
|
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
|
return 1
|
||||||
}
|
}
|
||||||
paths := wineenv.NewPaths(cfg, baseUnix)
|
paths := wineenv.NewPaths(cfg, baseUnix)
|
||||||
@@ -61,7 +71,7 @@ func Run(tool string, args []string) int {
|
|||||||
|
|
||||||
wineBin, err := wineenv.FindWine()
|
wineBin, err := wineenv.FindWine()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,13 +80,18 @@ func Run(tool string, args []string) int {
|
|||||||
var exitCode int
|
var exitCode int
|
||||||
switch {
|
switch {
|
||||||
case s.rawStdout:
|
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 := 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.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
exitCode = runRawStdout(cmd)
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
exitCode = runAndWait(cmd)
|
|
||||||
default:
|
default:
|
||||||
relay := filepath.Join(paths.BaseUnix, "bin", toolRelayName)
|
relay := filepath.Join(paths.BaseUnix, "bin", toolRelayName)
|
||||||
if fi, err := os.Stat(relay); err == nil && !fi.IsDir() {
|
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
|
// observes the real 32-bit exit code via Win32 before translating and
|
||||||
// re-exiting with a value that fits in a byte.
|
// 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 {
|
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()))
|
stdoutFifo := filepath.Join(os.TempDir(), fmt.Sprintf("vintner.stdout.%d", os.Getpid()))
|
||||||
stderrFifo := filepath.Join(os.TempDir(), fmt.Sprintf("msvc-go-wine.stderr.%d", os.Getpid()))
|
stderrFifo := filepath.Join(os.TempDir(), fmt.Sprintf("vintner.stderr.%d", os.Getpid()))
|
||||||
os.Remove(stdoutFifo)
|
os.Remove(stdoutFifo)
|
||||||
os.Remove(stderrFifo)
|
os.Remove(stderrFifo)
|
||||||
if err := syscall.Mkfifo(stdoutFifo, 0o600); err != nil {
|
if err := syscall.Mkfifo(stdoutFifo, 0o600); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
defer os.Remove(stdoutFifo)
|
defer os.Remove(stdoutFifo)
|
||||||
if err := syscall.Mkfifo(stderrFifo, 0o600); err != nil {
|
if err := syscall.Mkfifo(stderrFifo, 0o600); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
defer os.Remove(stderrFifo)
|
defer os.Remove(stderrFifo)
|
||||||
@@ -129,7 +144,7 @@ func runViaToolRelay(wineBin, relayExe, exePath string, args []string, paths *wi
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,12 +176,63 @@ func runViaToolRelay(wineBin, relayExe, exePath string, args []string, paths *wi
|
|||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
return exitErr.ExitCode()
|
return exitErr.ExitCode()
|
||||||
}
|
}
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return 0
|
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 {
|
func buildEnv(p *wineenv.Paths) []string {
|
||||||
overrides := map[string]string{
|
overrides := map[string]string{
|
||||||
"INCLUDE": p.Include,
|
"INCLUDE": p.Include,
|
||||||
@@ -199,31 +265,34 @@ func buildEnv(p *wineenv.Paths) []string {
|
|||||||
func runFiltered(cmd *exec.Cmd, stdoutF, stderrF lineFilter) int {
|
func runFiltered(cmd *exec.Cmd, stdoutF, stderrF lineFilter) int {
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
stderr, err := cmd.StderrPipe()
|
stderr, err := cmd.StderrPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
doneOut := make(chan struct{})
|
||||||
wg.Add(2)
|
doneErr := make(chan struct{})
|
||||||
go func() { defer wg.Done(); pumpLines(stdout, os.Stdout, stdoutF) }()
|
go func() { pumpLines(stdout, os.Stdout, stdoutF); close(doneOut) }()
|
||||||
go func() { defer wg.Done(); pumpLines(stderr, os.Stderr, stderrF) }()
|
go func() { pumpLines(stderr, os.Stderr, stderrF); close(doneErr) }()
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
err = cmd.Wait()
|
||||||
|
drain(doneOut)
|
||||||
|
drain(doneErr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
return exitErr.ExitCode()
|
return exitErr.ExitCode()
|
||||||
}
|
}
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
@@ -242,14 +311,3 @@ func pumpLines(r io.Reader, w *os.File, filter lineFilter) {
|
|||||||
fmt.Fprintln(w, line)
|
fmt.Fprintln(w, line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runAndWait(cmd *exec.Cmd) int {
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
||||||
return exitErr.ExitCode()
|
|
||||||
}
|
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// filtering its output.
|
// filtering its output.
|
||||||
package wrapper
|
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.
|
// dirKind selects which install directory a tool's real .exe lives in.
|
||||||
type dirKind int
|
type dirKind int
|
||||||
|
|||||||
Reference in New Issue
Block a user