mirror of
https://github.com/Cheviiot/Vintner.git
synced 2026-08-03 15:57:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25f751e874 | ||
|
|
4795c7c105 | ||
|
|
91471397fa | ||
|
|
94a19e6b43 | ||
|
|
6186837fd2 |
@@ -8,8 +8,9 @@ vintner cross-compiles with the real MSVC toolchain on Linux, using Wine.
|
||||
One Go binary drops in as `cl`, `link`, `lib`, `rc`, `midl`, `mc`, `mt`,
|
||||
`dumpbin`, `msbuild`, `nmake`, `ml`, `ml64`, `armasm`, `armasm64`, plus
|
||||
`cmd`/`findstr` shims, so once installed you invoke the real Microsoft
|
||||
tools exactly like on Windows. It handles full MSBuild projects, and with
|
||||
`--with-wdk`, real KMDF/UMDF Windows drivers.
|
||||
tools exactly like on Windows. It handles full MSBuild projects, with
|
||||
`--with-wdk` real KMDF/UMDF Windows drivers, and with `--with-dxsdk` the
|
||||
real D3DX9 headers/libs.
|
||||
|
||||
Inspired by [mstorsjo/msvc-wine](https://github.com/mstorsjo/msvc-wine)'s
|
||||
approach: download the real MSVC/WinSDK, wrap the compiler under Wine.
|
||||
@@ -21,6 +22,7 @@ approach: download the real MSVC/WinSDK, wrap the compiler under Wine.
|
||||
- [Quick start](#quick-start)
|
||||
- [Commands](#commands)
|
||||
- [Building drivers (WDK)](#building-drivers-wdk)
|
||||
- [Building against D3DX9 (DirectX SDK)](#building-against-d3dx9-directx-sdk)
|
||||
- [Language](#language)
|
||||
- [Shell completion](#shell-completion)
|
||||
- [Using clang-cl/lld-link instead of Wine](#using-clang-cllld-link-instead-of-wine)
|
||||
@@ -112,12 +114,13 @@ vintner completion bash|zsh print a shell
|
||||
|
||||
`download`'s main options: `--msvc-version`, `--sdk-version`,
|
||||
`--architecture` (repeatable: `x86`/`x64`/`arm`/`arm64`/`host`),
|
||||
`--host-arch`, `--only-host`, `--with-wdk` (see below), `--ignore`
|
||||
(repeatable), `--only-download`, `--only-unpack`, `--keep-unpack`,
|
||||
`--skip-patch`, `--cache`, `--language`, `--include-optional`,
|
||||
`--skip-recommended`, `--major`, `--preview`, `--manifest`,
|
||||
`--list-workloads`, `--list-components`, `--print-deps-tree`. Run
|
||||
`vintner download -h` for the full list with descriptions.
|
||||
`--host-arch`, `--only-host`, `--with-wdk` (see below), `--with-dxsdk`
|
||||
(see below), `--ignore` (repeatable), `--only-download`, `--only-unpack`,
|
||||
`--keep-unpack`, `--skip-patch`, `--cache`, `--language`,
|
||||
`--include-optional`, `--skip-recommended`, `--major`, `--preview`,
|
||||
`--manifest`, `--list-workloads`, `--list-components`,
|
||||
`--print-deps-tree`. Run `vintner download -h` for the full list with
|
||||
descriptions.
|
||||
|
||||
`--list-workloads`/`--list-components` print every workload/component id
|
||||
and its human-readable title from the fetched manifest, then exit
|
||||
@@ -143,6 +146,22 @@ work under Wine. Tested against a real sample driver from
|
||||
Only x64 and arm64 targets have a WDK package upstream; there's no x86 or
|
||||
arm one.
|
||||
|
||||
## Building against D3DX9 (DirectX SDK)
|
||||
|
||||
`--with-dxsdk` fetches the DirectX SDK (June 2010) — the last standalone
|
||||
release of D3DX9/10/11, XInput and XAudio2, dropped from the Windows SDK
|
||||
entirely once D3DX was deprecated. It unpacks the real headers and x86/x64
|
||||
import libs (`d3dx9.h`/`d3dx9.lib` included) to `<dest>/DXSDK`.
|
||||
|
||||
```bash
|
||||
vintner download --accept-license --with-dxsdk
|
||||
```
|
||||
|
||||
Point your project's `IncludePath`/`LibraryPath` at
|
||||
`<dest>/DXSDK/Include` and `<dest>/DXSDK/Lib/x86` or `<dest>/DXSDK/Lib/x64`.
|
||||
Requires `cabextract` on `PATH` (the installer is a self-extracting CAB
|
||||
archive).
|
||||
|
||||
## Language
|
||||
|
||||
CLI text (usage, progress lines, prompts) defaults to English. Set
|
||||
@@ -157,6 +176,8 @@ Error text from internal packages stays in English regardless.
|
||||
|
||||
## Shell completion
|
||||
|
||||
Already set up if you installed via Nivora. Otherwise:
|
||||
|
||||
```bash
|
||||
source <(vintner completion bash) # or add to ~/.bashrc
|
||||
source <(vintner completion zsh) # or add to ~/.zshrc
|
||||
|
||||
Vendored
+10
-2
@@ -79,8 +79,16 @@ HANDLE MakeKillOnCloseJob() {
|
||||
}
|
||||
|
||||
bool IsMtExe(const wchar_t *path) {
|
||||
const wchar_t *name = wcsrchr(path, L'\\');
|
||||
name = name ? name + 1 : path;
|
||||
// vintner passes toolExePath straight through from Go's filepath.Join,
|
||||
// which uses forward slashes even for a path that's about to be handed
|
||||
// to a native Windows process - so the separator here isn't reliably
|
||||
// '\\'. Check both; using whichever comes later in the string covers a
|
||||
// mixed-separator path too.
|
||||
const wchar_t *back = wcsrchr(path, L'\\');
|
||||
const wchar_t *fwd = wcsrchr(path, L'/');
|
||||
const wchar_t *sep = back;
|
||||
if (fwd && (!sep || fwd > sep)) sep = fwd;
|
||||
const wchar_t *name = sep ? sep + 1 : path;
|
||||
return _wcsicmp(name, L"mt.exe") == 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ func runDownload(args []string) int {
|
||||
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)")
|
||||
withDXSDK := fs.Bool("with-dxsdk", false, "also fetch and install the DirectX SDK (June 2010): real D3DX9/10/11, XInput and XAudio2 headers and import libs, dropped from the modern Windows SDK")
|
||||
var archsFlag stringList
|
||||
fs.Var(&archsFlag, "architecture", "target architecture to include (x86, x64, arm, arm64, host); repeatable")
|
||||
var ignoreFlag stringList
|
||||
@@ -219,6 +220,13 @@ func runDownload(args []string) int {
|
||||
}
|
||||
}
|
||||
|
||||
if *withDXSDK && !*onlyUnpack {
|
||||
if err := downloadDXSDK(cache, destAbs); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println(i18n.T("download.done", destAbs))
|
||||
return 0
|
||||
}
|
||||
@@ -255,6 +263,18 @@ func downloadWDK(opts *download.Options, selected []*download.Package, cache, de
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadDXSDK fetches and unpacks the DirectX SDK (June 2010) into
|
||||
// destAbs/DXSDK. See internal/download/dxsdk.go for why this is a separate
|
||||
// download path from the rest of ExpandSelection/FetchPayloads/Unpack.
|
||||
func downloadDXSDK(cache, destAbs string) error {
|
||||
dxsdkDir, err := download.DownloadDXSDK(cache, destAbs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Print(i18n.T("download.dxsdk_installed", dxsdkDir))
|
||||
return nil
|
||||
}
|
||||
|
||||
func contains(list []string, v string) bool {
|
||||
for _, s := range list {
|
||||
if s == v {
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ func runCLI(args []string) int {
|
||||
case "completion":
|
||||
return runCompletion(args[1:])
|
||||
case "version", "v", "--version":
|
||||
fmt.Println("vintner " + version)
|
||||
fmt.Println(versionString())
|
||||
return 0
|
||||
case "-h", "--help", "help", "h":
|
||||
printUsage()
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
// versionString renders "vintner <version>" plus, when available, the git
|
||||
// commit and build time Go's toolchain embeds automatically (since Go
|
||||
// 1.18, `go build` stamps vcs.revision/vcs.time/vcs.modified into the
|
||||
// binary on its own - no -ldflags needed for this part, so it works the
|
||||
// same whether the binary came from CI or a plain local `go build`).
|
||||
// Knowing the exact commit a reported bug was built from, not just the
|
||||
// X.Y.Z tag, is the point: two builds of the same tag could still differ
|
||||
// if the tag was ever moved, or if someone built from an uncommitted tree.
|
||||
//
|
||||
// Note for anyone testing this: `go build` stamps vcs.* build settings,
|
||||
// but `go test` binaries don't get them - there's no environment where a
|
||||
// `go test` run can exercise the revision-formatting branch below, hence
|
||||
// formatVersion is split out and tested directly instead.
|
||||
func versionString() string {
|
||||
revision, buildTime, dirty := "", "", false
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
for _, s := range info.Settings {
|
||||
switch s.Key {
|
||||
case "vcs.revision":
|
||||
revision = s.Value
|
||||
case "vcs.time":
|
||||
buildTime = s.Value
|
||||
case "vcs.modified":
|
||||
dirty = s.Value == "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
return formatVersion(version, revision, buildTime, dirty)
|
||||
}
|
||||
|
||||
// formatVersion is the pure part of versionString: given a revision (full
|
||||
// git SHA, may be empty), it's shortened to 12 chars and marked "-dirty" if
|
||||
// the build tree had uncommitted changes.
|
||||
func formatVersion(ver, revision, buildTime string, dirty bool) string {
|
||||
v := "vintner " + ver
|
||||
if revision == "" {
|
||||
return v
|
||||
}
|
||||
if len(revision) > 12 {
|
||||
revision = revision[:12]
|
||||
}
|
||||
if dirty {
|
||||
revision += "-dirty"
|
||||
}
|
||||
if buildTime != "" {
|
||||
return fmt.Sprintf("%s (%s, %s)", v, revision, buildTime)
|
||||
}
|
||||
return fmt.Sprintf("%s (%s)", v, revision)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFormatVersion(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
ver, revision, buildTime string
|
||||
dirty bool
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no VCS info at all",
|
||||
ver: "dev",
|
||||
want: "vintner dev",
|
||||
},
|
||||
{
|
||||
name: "clean build with full info",
|
||||
ver: "0.3.0",
|
||||
revision: "6186837fd23616335ba8aff830801692a756799c",
|
||||
buildTime: "2026-07-25T01:33:41Z",
|
||||
want: "vintner 0.3.0 (6186837fd236, 2026-07-25T01:33:41Z)",
|
||||
},
|
||||
{
|
||||
name: "dirty tree",
|
||||
ver: "0.3.0",
|
||||
revision: "6186837fd23616335ba8aff830801692a756799c",
|
||||
dirty: true,
|
||||
want: "vintner 0.3.0 (6186837fd236-dirty)",
|
||||
},
|
||||
{
|
||||
name: "short revision left untouched",
|
||||
ver: "dev",
|
||||
revision: "abc123",
|
||||
want: "vintner dev (abc123)",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := formatVersion(tc.ver, tc.revision, tc.buildTime, tc.dirty)
|
||||
if got != tc.want {
|
||||
t.Errorf("formatVersion(%q, %q, %q, %v) = %q, want %q",
|
||||
tc.ver, tc.revision, tc.buildTime, tc.dirty, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// The DirectX SDK (June 2010) is the last standalone release of D3DX9 (and
|
||||
// D3DX10/11, XInput, XAudio2, ...). Microsoft never carried D3DX forward
|
||||
// into the Windows 10/11 SDK - it's deprecated in favor of D3DCompiler/
|
||||
// WICTextureLoader/DirectXTex, but plenty of legacy code (this included)
|
||||
// still links against the real d3dx9.h/d3dx9.lib. Like the WDK (see
|
||||
// wdk.go), it isn't part of the VS installer manifest or any package feed
|
||||
// vsman knows about, so this is a separate, self-contained download path
|
||||
// outside ExpandSelection/FetchPayloads/UnpackSelectedPackages.
|
||||
//
|
||||
// The installer is a self-extracting PE with an appended CAB archive.
|
||||
// cabextract (already a vintner prerequisite - see runMsiExtract's sibling
|
||||
// extractWindowsSDKPackage) unpacks it directly, without needing Wine or a
|
||||
// separate archive tool. Its -F/--filter flag (repeatable) restricts
|
||||
// extraction to the Include and Lib subtrees actually needed for building -
|
||||
// about 21MB out of the installer's 1.2GB uncompressed payload.
|
||||
|
||||
const dxsdkURL = "https://download.microsoft.com/download/A/E/7/AE743F1F-632B-4809-87A9-AA1BB3458E31/DXSDK_Jun10.exe"
|
||||
|
||||
// dxsdkSHA256 pins the exact installer build this code was written against
|
||||
// (verified by fully extracting it with both cabextract and 7z and cross
|
||||
// checking the file lists) - the June 2010 DirectX SDK is a frozen legacy
|
||||
// artifact Microsoft is not going to rebuild. A var, not a const, so tests
|
||||
// can point it at a small fake payload instead of the real 600MB installer.
|
||||
var dxsdkSHA256 = "705271dc83bfee54d9b94e028426e288d5f070784b7446d164f48ecfbb2a02cb"
|
||||
|
||||
// DownloadDXSDK fetches (or reuses a cached copy of) the DirectX SDK (June
|
||||
// 2010) installer into cacheDir, then unpacks its Include and Lib trees -
|
||||
// headers and x86/x64 import libs for D3DX9/10/11, XInput, XAudio2, and the
|
||||
// rest - into destDir/DXSDK. Returns that directory.
|
||||
func DownloadDXSDK(cacheDir, destDir string) (string, error) {
|
||||
if _, err := exec.LookPath("cabextract"); err != nil {
|
||||
return "", fmt.Errorf("cabextract not found in PATH (install the cabextract package): %w", err)
|
||||
}
|
||||
|
||||
cacheFile := filepath.Join(cacheDir, "DXSDK_Jun10.exe")
|
||||
if !isFile(cacheFile) {
|
||||
fmt.Println("Downloading DirectX SDK (June 2010)")
|
||||
if err := httpDownloadFile(dxsdkURL, cacheFile); err != nil {
|
||||
return "", fmt.Errorf("downloading DirectX SDK: %w", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Using existing file", filepath.Base(cacheFile))
|
||||
}
|
||||
sum, err := sha256File(cacheFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !equalFoldHex(sum, dxsdkSHA256) {
|
||||
return "", fmt.Errorf("incorrect hash for downloaded file %s, aborting", filepath.Base(cacheFile))
|
||||
}
|
||||
|
||||
scratch, err := os.MkdirTemp(destDir, "dxsdk-unpack-")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer os.RemoveAll(scratch)
|
||||
|
||||
if err := runCabextract(cacheFile, scratch, "DXSDK/Include/*", "DXSDK/Lib/*"); err != nil {
|
||||
return "", fmt.Errorf("extracting DirectX SDK: %w", err)
|
||||
}
|
||||
|
||||
dxsdkDir := filepath.Join(destDir, "DXSDK")
|
||||
if err := combineDirTrees(filepath.Join(scratch, "DXSDK"), dxsdkDir); err != nil {
|
||||
return "", fmt.Errorf("moving DirectX SDK content into place: %w", err)
|
||||
}
|
||||
return dxsdkDir, nil
|
||||
}
|
||||
|
||||
// runCabextract extracts srcFile into destDir, restricted to entries
|
||||
// matching any of patterns (cabextract's -F, repeatable, glob-matched
|
||||
// against the full in-archive path).
|
||||
func runCabextract(srcFile, destDir string, patterns ...string) error {
|
||||
if err := os.MkdirAll(destDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
args := []string{"-q", "-d", destDir}
|
||||
for _, p := range patterns {
|
||||
args = append(args, "-F", p)
|
||||
}
|
||||
args = append(args, srcFile)
|
||||
cmd := exec.Command("cabextract", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// withFakeCabextract prepends a directory containing a fake "cabextract"
|
||||
// script to PATH, so tests can exercise runCabextract/DownloadDXSDK without
|
||||
// the real tool (or a real DXSDK installer) present. The fake script
|
||||
// records the arguments it was invoked with to argsFile and creates an
|
||||
// empty DXSDK/Include and DXSDK/Lib under whatever -d directory it was
|
||||
// given, mimicking a successful (if empty) extraction.
|
||||
func withFakeCabextract(t *testing.T, argsFile string) {
|
||||
t.Helper()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("fake cabextract script requires a POSIX shell")
|
||||
}
|
||||
bin := t.TempDir()
|
||||
script := `#!/bin/sh
|
||||
echo "$@" > "` + argsFile + `"
|
||||
dest=""
|
||||
prev=""
|
||||
for a in "$@"; do
|
||||
if [ "$prev" = "-d" ]; then
|
||||
dest="$a"
|
||||
fi
|
||||
prev="$a"
|
||||
done
|
||||
mkdir -p "$dest/DXSDK/Include" "$dest/DXSDK/Lib"
|
||||
`
|
||||
path := filepath.Join(bin, "cabextract")
|
||||
if err := os.WriteFile(path, []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("PATH", bin+":"+os.Getenv("PATH"))
|
||||
}
|
||||
|
||||
func TestRunCabextractPassesFilterArgs(t *testing.T) {
|
||||
dest := t.TempDir()
|
||||
argsFile := filepath.Join(t.TempDir(), "args")
|
||||
withFakeCabextract(t, argsFile)
|
||||
|
||||
src := filepath.Join(t.TempDir(), "installer.exe")
|
||||
writeFile(t, src, "fake-installer-bytes")
|
||||
|
||||
if err := runCabextract(src, dest, "DXSDK/Include/*", "DXSDK/Lib/*"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(argsFile)
|
||||
if err != nil {
|
||||
t.Fatalf("expected the fake cabextract to have run: %v", err)
|
||||
}
|
||||
want := "-q -d " + dest + " -F DXSDK/Include/* -F DXSDK/Lib/* " + src + "\n"
|
||||
if string(got) != want {
|
||||
t.Errorf("cabextract args = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadDXSDKReusesCachedFileAndRejectsHashMismatch(t *testing.T) {
|
||||
argsFile := filepath.Join(t.TempDir(), "args")
|
||||
withFakeCabextract(t, argsFile)
|
||||
|
||||
cacheDir := t.TempDir()
|
||||
destDir := t.TempDir()
|
||||
writeFile(t, filepath.Join(cacheDir, "DXSDK_Jun10.exe"), "not the real installer")
|
||||
|
||||
_, err := DownloadDXSDK(cacheDir, destDir)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for a cached file that doesn't match dxsdkSHA256")
|
||||
}
|
||||
if _, statErr := os.Stat(argsFile); statErr == nil {
|
||||
t.Error("cabextract should not have run before the hash was verified")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadDXSDKMovesExtractedContentIntoPlace(t *testing.T) {
|
||||
argsFile := filepath.Join(t.TempDir(), "args")
|
||||
withFakeCabextract(t, argsFile)
|
||||
|
||||
cacheDir := t.TempDir()
|
||||
destDir := t.TempDir()
|
||||
cacheFile := filepath.Join(cacheDir, "DXSDK_Jun10.exe")
|
||||
writeFile(t, cacheFile, "fake-installer-bytes-for-hash-test")
|
||||
|
||||
// Patch the expected hash to match our fake cached file, since we can't
|
||||
// (and shouldn't) fetch or embed the real 600MB installer in a test.
|
||||
sum, err := sha256File(cacheFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
restore := dxsdkSHA256
|
||||
dxsdkSHA256 = sum
|
||||
defer func() { dxsdkSHA256 = restore }()
|
||||
|
||||
dxsdkDir, err := DownloadDXSDK(cacheDir, destDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if dxsdkDir != filepath.Join(destDir, "DXSDK") {
|
||||
t.Errorf("DownloadDXSDK returned %q, want %q", dxsdkDir, filepath.Join(destDir, "DXSDK"))
|
||||
}
|
||||
for _, sub := range []string{"Include", "Lib"} {
|
||||
if !isDir(filepath.Join(dxsdkDir, sub)) {
|
||||
t.Errorf("expected %s/%s to exist after extraction", dxsdkDir, sub)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeFile(t *testing.T, path, content string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCombineDirTreesNonexistentSrcIsNoop(t *testing.T) {
|
||||
dest := t.TempDir()
|
||||
if err := combineDirTrees(filepath.Join(t.TempDir(), "does-not-exist"), dest); err != nil {
|
||||
t.Fatalf("combineDirTrees with a nonexistent src returned an error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCombineDirTreesRenamesWholesaleWhenDestMissing(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
src := filepath.Join(root, "src")
|
||||
dest := filepath.Join(root, "nested", "dest")
|
||||
writeFile(t, filepath.Join(src, "file.txt"), "hello")
|
||||
|
||||
if err := combineDirTrees(src, dest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.ReadFile(filepath.Join(dest, "file.txt")); err != nil {
|
||||
t.Errorf("expected %s/file.txt to exist after combine: %v", dest, err)
|
||||
}
|
||||
if isDir(src) {
|
||||
t.Error("src should have been moved (renamed), not copied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCombineDirTreesMergesNewSubdir(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
src, dest := filepath.Join(root, "src"), filepath.Join(root, "dest")
|
||||
writeFile(t, filepath.Join(src, "NewDir", "a.txt"), "a")
|
||||
writeFile(t, filepath.Join(dest, "Existing.txt"), "keep me")
|
||||
|
||||
if err := combineDirTrees(src, dest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.ReadFile(filepath.Join(dest, "NewDir", "a.txt")); err != nil {
|
||||
t.Errorf("expected merged NewDir/a.txt: %v", err)
|
||||
}
|
||||
if _, err := os.ReadFile(filepath.Join(dest, "Existing.txt")); err != nil {
|
||||
t.Errorf("pre-existing dest file was lost: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCombineDirTreesMergesCaseInsensitiveCollision(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
src, dest := filepath.Join(root, "src"), filepath.Join(root, "dest")
|
||||
// src has "Include" (capital I), dest already has "include" (lowercase) -
|
||||
// this is exactly the MSVC/WinSDK casing-inconsistency scenario the
|
||||
// function's doc comment describes.
|
||||
writeFile(t, filepath.Join(src, "Include", "new.h"), "new")
|
||||
writeFile(t, filepath.Join(dest, "include", "old.h"), "old")
|
||||
|
||||
if err := combineDirTrees(src, dest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.ReadFile(filepath.Join(dest, "include", "new.h")); err != nil {
|
||||
t.Errorf("new.h should have merged into the existing lowercase 'include' dir: %v", err)
|
||||
}
|
||||
if _, err := os.ReadFile(filepath.Join(dest, "include", "old.h")); err != nil {
|
||||
t.Errorf("old.h should still be there: %v", err)
|
||||
}
|
||||
if isDir(filepath.Join(dest, "Include")) {
|
||||
t.Error("a separate capital-I 'Include' dir should not have been created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCombineDirTreesRecursesIntoExactNameMatch(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
src, dest := filepath.Join(root, "src"), filepath.Join(root, "dest")
|
||||
writeFile(t, filepath.Join(src, "lib", "x64", "new.lib"), "new")
|
||||
writeFile(t, filepath.Join(dest, "lib", "x64", "old.lib"), "old")
|
||||
|
||||
if err := combineDirTrees(src, dest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range []string{"new.lib", "old.lib"} {
|
||||
if _, err := os.ReadFile(filepath.Join(dest, "lib", "x64", f)); err != nil {
|
||||
t.Errorf("expected lib/x64/%s to survive the merge: %v", f, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyRedirectedAssembliesNoConfigIsNoop(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
app := filepath.Join(dir, "MSBuild.exe")
|
||||
if err := CopyRedirectedAssemblies(app); err != nil {
|
||||
t.Fatalf("with no .config file present, expected no error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyRedirectedAssembliesCopiesReferencedDLL(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
app := filepath.Join(dir, "MSBuild.exe")
|
||||
writeFile(t, app+".config", `<?xml version="1.0"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<codeBase version="1.0.0.0" href="amd64\Some.Assembly.dll"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>`)
|
||||
writeFile(t, filepath.Join(dir, "amd64", "Some.Assembly.dll"), "binary-content")
|
||||
|
||||
if err := CopyRedirectedAssemblies(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(dir, "Some.Assembly.dll"))
|
||||
if err != nil {
|
||||
t.Fatalf("expected Some.Assembly.dll copied next to MSBuild.exe: %v", err)
|
||||
}
|
||||
if string(got) != "binary-content" {
|
||||
t.Errorf("copied file content = %q, want %q", got, "binary-content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyRedirectedAssembliesSkipsMissingTarget(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
app := filepath.Join(dir, "MSBuild.exe")
|
||||
writeFile(t, app+".config", `<?xml version="1.0"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<codeBase href="nowhere\Missing.dll"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>`)
|
||||
|
||||
if err := CopyRedirectedAssemblies(app); err != nil {
|
||||
t.Fatalf("a redirect pointing at a nonexistent file should be silently skipped, got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRetryBackoff(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
attempt int
|
||||
want time.Duration
|
||||
}{
|
||||
{1, 1 * time.Second},
|
||||
{2, 2 * time.Second},
|
||||
{3, 4 * time.Second},
|
||||
{4, 8 * time.Second},
|
||||
{5, 10 * time.Second}, // capped
|
||||
{10, 10 * time.Second},
|
||||
} {
|
||||
if got := retryBackoff(tc.attempt); got != tc.want {
|
||||
t.Errorf("retryBackoff(%d) = %v, want %v", tc.attempt, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSHA256File(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "f")
|
||||
if err := os.WriteFile(path, []byte("hello"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := sha256File(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// echo -n hello | sha256sum
|
||||
want := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
|
||||
if got != want {
|
||||
t.Errorf("sha256File(hello) = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualFoldHex(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
a, b string
|
||||
want bool
|
||||
}{
|
||||
{"ABCDEF", "abcdef", true},
|
||||
{"abc123", "ABC123", true},
|
||||
{"abc123", "abc124", false},
|
||||
{"abc", "abcd", false},
|
||||
{"", "", true},
|
||||
} {
|
||||
if got := equalFoldHex(tc.a, tc.b); got != tc.want {
|
||||
t.Errorf("equalFoldHex(%q, %q) = %v, want %v", tc.a, tc.b, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPayloadName(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
fileName string
|
||||
want string
|
||||
}{
|
||||
{"payload.msi", "payload.msi"},
|
||||
{"folder/payload.msi", "payload.msi"},
|
||||
{`folder\payload.msi`, "payload.msi"},
|
||||
{`a\b/c\payload.msi`, "payload.msi"},
|
||||
{"", ""},
|
||||
} {
|
||||
p := Payload{FileName: tc.fileName}
|
||||
if got := p.Name(); got != tc.want {
|
||||
t.Errorf("Payload{FileName: %q}.Name() = %q, want %q", tc.fileName, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageKey(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
p Package
|
||||
want string
|
||||
}{
|
||||
{"id only", Package{ID: "Foo"}, "Foo"},
|
||||
{"id+version", Package{ID: "Foo", Version: "1.0"}, "Foo-1.0"},
|
||||
{
|
||||
"id+version+all arches",
|
||||
Package{ID: "Foo", Version: "1.0", Chip: "x64", MachineArch: "x86", ProductArch: "neutral"},
|
||||
"Foo-1.0-chip.x64-machineArch.x86-productArch.neutral",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.p.Key(); got != tc.want {
|
||||
t.Errorf("Key() = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageLocalized(t *testing.T) {
|
||||
noResources := Package{}
|
||||
if got := noResources.Localized("en"); got != nil {
|
||||
t.Errorf("Localized() on a package with no LocalizedResources = %v, want nil", got)
|
||||
}
|
||||
|
||||
p := Package{LocalizedResources: []LocalizedResource{
|
||||
{Language: "de-DE", Title: "Deutsch"},
|
||||
{Language: "en-US", Title: "English"},
|
||||
{Language: "ru-RU", Title: "Русский"},
|
||||
}}
|
||||
|
||||
for _, tc := range []struct {
|
||||
lang string
|
||||
wantTitle string
|
||||
}{
|
||||
{"ru", "Русский"},
|
||||
{"ru-RU", "Русский"},
|
||||
{"en", "English"},
|
||||
{"", "English"}, // "" defaults to "en"
|
||||
{"fr", "English"}, // no fr variant, falls back to the en-* one
|
||||
} {
|
||||
t.Run("lang="+tc.lang, func(t *testing.T) {
|
||||
got := p.Localized(tc.lang)
|
||||
if got == nil {
|
||||
t.Fatalf("Localized(%q) = nil", tc.lang)
|
||||
}
|
||||
if got.Title != tc.wantTitle {
|
||||
t.Errorf("Localized(%q).Title = %q, want %q", tc.lang, got.Title, tc.wantTitle)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageSizes(t *testing.T) {
|
||||
p := Package{
|
||||
InstallSizes: map[string]int64{"x86": 100, "x64": 200},
|
||||
Payloads: []Payload{{Size: 10}, {Size: 20}, {Size: 30}},
|
||||
}
|
||||
if got := p.InstalledSize(); got != 300 {
|
||||
t.Errorf("InstalledSize() = %d, want 300", got)
|
||||
}
|
||||
if got := p.DownloadSize(); got != 60 {
|
||||
t.Errorf("DownloadSize() = %d, want 60", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageDependenciesNormalizesBothShapes(t *testing.T) {
|
||||
p := Package{DependenciesRaw: map[string]json.RawMessage{
|
||||
"Bare.Version": json.RawMessage(`"1.0"`),
|
||||
"Full.Object": json.RawMessage(`{"version":"2.0","type":"Optional","id":"Real.Target"}`),
|
||||
"Recommended.Dep": json.RawMessage(`{"version":"3.0","type":"Recommended"}`),
|
||||
}}
|
||||
deps := p.Dependencies()
|
||||
|
||||
if d := deps["Bare.Version"]; d.Version != "1.0" || d.TargetID != "" || d.Type != "" {
|
||||
t.Errorf("Bare.Version = %+v, want Version=1.0 TargetID='' Type=''", d)
|
||||
}
|
||||
if d := deps["Full.Object"]; d.Version != "2.0" || d.TargetID != "Real.Target" || d.Type != "Optional" {
|
||||
t.Errorf("Full.Object = %+v, want Version=2.0 TargetID=Real.Target Type=Optional", d)
|
||||
}
|
||||
if d := deps["Recommended.Dep"]; d.Version != "3.0" || d.Type != "Recommended" {
|
||||
t.Errorf("Recommended.Dep = %+v, want Version=3.0 Type=Recommended", d)
|
||||
}
|
||||
|
||||
// Calling Dependencies() again must return the same cached map, not
|
||||
// re-parse (and must not panic on the second call).
|
||||
if d2 := p.Dependencies(); len(d2) != len(deps) {
|
||||
t.Errorf("second Dependencies() call returned a different map: %v vs %v", d2, deps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHumanizeBytes(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
size int64
|
||||
want string
|
||||
}{
|
||||
{500, "500 bytes"},
|
||||
{2048, "2.0 KB"},
|
||||
{5 * 1024 * 1024, "5.0 MB"},
|
||||
{2 * 1024 * 1024 * 1024, "2.0 GB"},
|
||||
} {
|
||||
if got := HumanizeBytes(tc.size); got != tc.want {
|
||||
t.Errorf("HumanizeBytes(%d) = %q, want %q", tc.size, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWDKNuGetID(t *testing.T) {
|
||||
for _, tc := range []struct{ arch, want string }{
|
||||
{"x64", "Microsoft.Windows.WDK.x64"},
|
||||
{"x86", "Microsoft.Windows.WDK.x64"}, // no 32-bit package exists
|
||||
{"arm64", "Microsoft.Windows.WDK.ARM64"},
|
||||
} {
|
||||
if got := WDKNuGetID(tc.arch); got != tc.want {
|
||||
t.Errorf("WDKNuGetID(%q) = %q, want %q", tc.arch, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDKBuildPrefix(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
selected []*Package
|
||||
want string
|
||||
}{
|
||||
{"no SDK package", []*Package{{ID: "Something.Else"}}, ""},
|
||||
{"win10sdk", []*Package{{ID: "Win10SDK_10.0.26100", Version: "10.0.26100.1742"}}, "10.0.26100"},
|
||||
{"win11sdk case-insensitive id", []*Package{{ID: "WIN11SDK_10.0.22621", Version: "10.0.22621.5"}}, "10.0.22621"},
|
||||
{"short version", []*Package{{ID: "Win10SDK_x", Version: "10.0"}}, ""},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := SDKBuildPrefix(tc.selected); got != tc.want {
|
||||
t.Errorf("SDKBuildPrefix(...) = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFillMissingHostToolsCopiesWithoutOverwriting(t *testing.T) {
|
||||
cDir := t.TempDir()
|
||||
writeFile(t, filepath.Join(cDir, "bin", "10.0.26100.0", "x64", "stampinf.exe"), "x64-stampinf")
|
||||
writeFile(t, filepath.Join(cDir, "bin", "10.0.26100.0", "x64", "inf2cat.exe"), "x64-inf2cat")
|
||||
// x86 already ships its own real inf2cat.exe - must not be clobbered.
|
||||
writeFile(t, filepath.Join(cDir, "bin", "10.0.26100.0", "x86", "inf2cat.exe"), "real-x86-inf2cat")
|
||||
|
||||
if err := fillMissingHostTools(cDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
x86Dir := filepath.Join(cDir, "bin", "10.0.26100.0", "x86")
|
||||
stampinf, err := os.ReadFile(filepath.Join(x86Dir, "stampinf.exe"))
|
||||
if err != nil {
|
||||
t.Fatalf("expected stampinf.exe to be copied into x86: %v", err)
|
||||
}
|
||||
if string(stampinf) != "x64-stampinf" {
|
||||
t.Errorf("copied stampinf.exe content = %q, want the x64 copy's content", stampinf)
|
||||
}
|
||||
|
||||
inf2cat, err := os.ReadFile(filepath.Join(x86Dir, "inf2cat.exe"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(inf2cat) != "real-x86-inf2cat" {
|
||||
t.Errorf("inf2cat.exe = %q, want the original x86 file preserved (not overwritten by the x64 copy)", inf2cat)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFillMissingHostToolsNoBinDirIsNoop(t *testing.T) {
|
||||
if err := fillMissingHostTools(t.TempDir()); err != nil {
|
||||
t.Fatalf("missing bin/ dir should be a no-op, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateVersionedBuildTaskAssemblies(t *testing.T) {
|
||||
cDir := t.TempDir()
|
||||
buildDir := filepath.Join(cDir, "build")
|
||||
writeFile(t, filepath.Join(buildDir, "Microsoft.DriverKit.Build.Tasks.17.0.dll"), "task-dll-bytes")
|
||||
writeFile(t, filepath.Join(buildDir, "Microsoft.DriverKit.Build.Tasks.18.0.dll"), "already-there")
|
||||
writeFile(t, filepath.Join(buildDir, "unrelated.dll"), "unrelated")
|
||||
|
||||
if err := duplicateVersionedBuildTaskAssemblies(cDir, "18.0"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Already had an 18.0 copy - must not have been overwritten.
|
||||
got, err := os.ReadFile(filepath.Join(buildDir, "Microsoft.DriverKit.Build.Tasks.18.0.dll"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "already-there" {
|
||||
t.Errorf("pre-existing 18.0 dll was overwritten: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateVersionedBuildTaskAssembliesCreatesMissingCopy(t *testing.T) {
|
||||
cDir := t.TempDir()
|
||||
buildDir := filepath.Join(cDir, "build")
|
||||
writeFile(t, filepath.Join(buildDir, "sub", "Foo.Bar.17.0.dll"), "bytes")
|
||||
|
||||
if err := duplicateVersionedBuildTaskAssemblies(cDir, "18.0"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(buildDir, "sub", "Foo.Bar.18.0.dll"))
|
||||
if err != nil {
|
||||
t.Fatalf("expected a Foo.Bar.18.0.dll duplicate: %v", err)
|
||||
}
|
||||
if string(got) != "bytes" {
|
||||
t.Errorf("duplicated dll content = %q, want %q", got, "bytes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateVersionedBuildTaskAssembliesNoBuildDirIsNoop(t *testing.T) {
|
||||
if err := duplicateVersionedBuildTaskAssemblies(t.TempDir(), "18.0"); err != nil {
|
||||
t.Fatalf("missing build/ dir should be a no-op, got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -77,8 +77,8 @@ Usage:
|
||||
vintner completion bash|zsh print a shell completion script
|
||||
|
||||
Run "vintner <command> --help" for that command's own options - download
|
||||
has many, including --with-wdk, --list-workloads, --list-components and
|
||||
--print-deps-tree.
|
||||
has many, including --with-wdk, --with-dxsdk, --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.
|
||||
@@ -99,8 +99,8 @@ Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
|
||||
vintner completion bash|zsh вывести скрипт автодополнения для оболочки
|
||||
|
||||
Запустите «vintner <команда> --help» для параметров конкретной команды —
|
||||
у download их много, включая --with-wdk, --list-workloads, --list-components
|
||||
и --print-deps-tree.
|
||||
у download их много, включая --with-wdk, --with-dxsdk, --list-workloads,
|
||||
--list-components и --print-deps-tree.
|
||||
|
||||
--dest/[каталог] по умолчанию — ~/.vintner.
|
||||
Язык: установите VINTNER_LANG=en (или LANG=en_US...) для вывода на английском.
|
||||
@@ -161,6 +161,10 @@ Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
|
||||
EN: "Installed WDK (%s) %s at %s\n",
|
||||
RU: "WDK (%s) %s установлен в %s\n",
|
||||
},
|
||||
"download.dxsdk_installed": {
|
||||
EN: "Installed DirectX SDK (June 2010) at %s\n",
|
||||
RU: "DirectX SDK (июнь 2010) установлен в %s\n",
|
||||
},
|
||||
"download.workloads_header": {
|
||||
EN: "Available Workloads (%d):\n",
|
||||
RU: "Доступные рабочие нагрузки (Workload) (%d):\n",
|
||||
|
||||
@@ -62,6 +62,10 @@ func Install(dest, selfBinary string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := aliasPlatformToolsets(dest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
includeDir := filepath.Join(msvcDir, "include")
|
||||
if err := Lowercase(includeDir, LowercaseOptions{Symlink: true}); err != nil {
|
||||
return fmt.Errorf("lowercasing %s: %w", includeDir, err)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/Cheviiot/vintner/internal/wineenv"
|
||||
)
|
||||
|
||||
var rePlatformToolsetDir = regexp.MustCompile(`^v(\d+)$`)
|
||||
|
||||
// aliasPlatformToolsets makes every historical PlatformToolset name in
|
||||
// wineenv.KnownPlatformToolsets resolve to the one compiler `download`
|
||||
// actually fetched.
|
||||
//
|
||||
// MSBuild decides whether a PlatformToolset is "installed" at all - the
|
||||
// check behind MSB8020 - by testing whether
|
||||
// MSBuild/Microsoft/VC/v<schema>/Platforms/<arch>/PlatformToolsets/<toolset>/
|
||||
// exists on disk (Microsoft.Cpp.props, via
|
||||
// ToolLocationHelper.FindRootFolderWhereAllFilesExist). That's a plain file
|
||||
// lookup, not influenced by any environment variable - unlike the later,
|
||||
// env-var-driven VCInstallDir_<N> checks internal/wrapper's msbuildEnv
|
||||
// covers, this one needs the actual directory to exist under dest.
|
||||
// Microsoft's own downloaded MSBuild package only ships a PlatformToolsets
|
||||
// entry for the exact generation matching the fetched compiler, so any
|
||||
// project pinned to an older PlatformToolset (v142 for a project last saved
|
||||
// under VS2019, say) fails this check outright even though the one real
|
||||
// toolchain installed could easily build it.
|
||||
//
|
||||
// Toolset.props/Toolset.targets don't hardcode a version number (they just
|
||||
// import version-agnostic files like Microsoft.Cpp.MSVC.Toolset.<arch>.props),
|
||||
// so a symlink under any other historical name is a correct, transparent
|
||||
// alias rather than a divergent copy.
|
||||
func aliasPlatformToolsets(dest string) error {
|
||||
schemaDirs, err := filepath.Glob(filepath.Join(dest, "MSBuild", "Microsoft", "VC", "v*"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, schemaDir := range schemaDirs {
|
||||
archDirs, err := filepath.Glob(filepath.Join(schemaDir, "Platforms", "*", "PlatformToolsets"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, toolsetsDir := range archDirs {
|
||||
if err := aliasOneDir(toolsetsDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// aliasOneDir symlinks every name in wineenv.KnownPlatformToolsets that
|
||||
// doesn't already exist in toolsetsDir onto whichever real v<N> toolset
|
||||
// subdirectory is actually present there.
|
||||
func aliasOneDir(toolsetsDir string) error {
|
||||
entries, err := os.ReadDir(toolsetsDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var real string
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
if rePlatformToolsetDir.MatchString(e.Name()) {
|
||||
real = e.Name()
|
||||
break
|
||||
}
|
||||
}
|
||||
if real == "" {
|
||||
// Nothing numeric here (e.g. only the WindowsKernelModeDriver10.0-style
|
||||
// WDK toolsets) - nothing to alias.
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, n := range wineenv.KnownPlatformToolsets {
|
||||
alias := "v" + n
|
||||
if alias == real {
|
||||
continue
|
||||
}
|
||||
aliasPath := filepath.Join(toolsetsDir, alias)
|
||||
if exists(aliasPath) {
|
||||
continue
|
||||
}
|
||||
if err := os.Symlink(real, aliasPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/Cheviiot/vintner/internal/wineenv"
|
||||
)
|
||||
|
||||
func TestAliasPlatformToolsetsAliasesRealToolset(t *testing.T) {
|
||||
dest := t.TempDir()
|
||||
toolsetsDir := filepath.Join(dest, "MSBuild", "Microsoft", "VC", "v180", "Platforms", "x64", "PlatformToolsets")
|
||||
realDir := filepath.Join(toolsetsDir, "v145")
|
||||
if err := os.MkdirAll(realDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(realDir, "Toolset.props"), []byte("<Project/>"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A WDK toolset entry alongside it - must not be mistaken for the real
|
||||
// numeric toolset or itself get aliased over.
|
||||
if err := os.MkdirAll(filepath.Join(toolsetsDir, "WindowsKernelModeDriver10.0"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := aliasPlatformToolsets(dest); err != nil {
|
||||
t.Fatalf("aliasPlatformToolsets: %v", err)
|
||||
}
|
||||
|
||||
for _, n := range wineenv.KnownPlatformToolsets {
|
||||
alias := filepath.Join(toolsetsDir, "v"+n)
|
||||
fi, err := os.Lstat(alias)
|
||||
if err != nil {
|
||||
t.Errorf("expected v%s alias to exist: %v", n, err)
|
||||
continue
|
||||
}
|
||||
if fi.Mode()&os.ModeSymlink == 0 {
|
||||
t.Errorf("v%s should be a symlink, got mode %v", n, fi.Mode())
|
||||
continue
|
||||
}
|
||||
target, err := os.Readlink(alias)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if target != "v145" {
|
||||
t.Errorf("v%s symlink target = %q, want \"v145\"", n, target)
|
||||
}
|
||||
// Follow the alias and confirm it actually reaches the real content.
|
||||
if !isFile(filepath.Join(toolsetsDir, "v"+n, "Toolset.props")) {
|
||||
t.Errorf("v%s/Toolset.props not reachable through the alias", n)
|
||||
}
|
||||
}
|
||||
|
||||
if exists(filepath.Join(toolsetsDir, "WindowsKernelModeDriver10.0", "v"+wineenv.KnownPlatformToolsets[0])) {
|
||||
t.Error("WDK toolset directory should not have been touched")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasPlatformToolsetsDoesNotOverwriteExisting(t *testing.T) {
|
||||
dest := t.TempDir()
|
||||
toolsetsDir := filepath.Join(dest, "MSBuild", "Microsoft", "VC", "v180", "Platforms", "x64", "PlatformToolsets")
|
||||
if err := os.MkdirAll(filepath.Join(toolsetsDir, "v145"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// v142 already genuinely installed (e.g. a real VS install with several
|
||||
// side-by-side toolsets) - must be left alone, not replaced with an alias.
|
||||
real142 := filepath.Join(toolsetsDir, "v142")
|
||||
if err := os.MkdirAll(real142, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(real142, "Toolset.props"), []byte("<!-- real v142 -->"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := aliasPlatformToolsets(dest); err != nil {
|
||||
t.Fatalf("aliasPlatformToolsets: %v", err)
|
||||
}
|
||||
|
||||
fi, err := os.Lstat(real142)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fi.Mode()&os.ModeSymlink != 0 {
|
||||
t.Error("pre-existing v142 directory should not have been replaced with a symlink")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasPlatformToolsetsNoNumericToolset(t *testing.T) {
|
||||
dest := t.TempDir()
|
||||
// Only a WDK-style toolset present, nothing numeric to alias from.
|
||||
toolsetsDir := filepath.Join(dest, "MSBuild", "Microsoft", "VC", "v180", "Platforms", "x64", "PlatformToolsets")
|
||||
if err := os.MkdirAll(filepath.Join(toolsetsDir, "WindowsUserModeDriver10.0"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := aliasPlatformToolsets(dest); err != nil {
|
||||
t.Fatalf("aliasPlatformToolsets: %v", err)
|
||||
}
|
||||
|
||||
for _, n := range wineenv.KnownPlatformToolsets {
|
||||
if exists(filepath.Join(toolsetsDir, "v"+n)) {
|
||||
t.Errorf("v%s should not have been created with no real numeric toolset present", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package wineenv
|
||||
|
||||
// KnownPlatformToolsets are every numeric PlatformToolset short name
|
||||
// Microsoft.Cpp.Default.props has ever defined a
|
||||
// _PlatformToolsetShortNameFor_v<N> entry for (VS2013 through the VS2022
|
||||
// initial release; excludes the _xp/_wp80/_wp81 variants, which aren't
|
||||
// purely numeric). vintner only ever installs one compiler generation, but
|
||||
// real .vcxproj files in the wild are pinned to whichever generation they
|
||||
// were last edited under - v142 (VS2019) for anything not yet retargeted is
|
||||
// extremely common. Shared between internal/install (which symlinks these
|
||||
// names onto the one real MSBuild PlatformToolsets directory) and
|
||||
// internal/wrapper (which mirrors the same names onto VCInstallDir_<N>/
|
||||
// VCToolsInstallDir_<N> for the older, environment-variable-driven toolset
|
||||
// redirect chain) - see doc comments there for why both are needed.
|
||||
var KnownPlatformToolsets = []string{"90", "100", "110", "120", "140", "141", "142", "143"}
|
||||
@@ -0,0 +1,61 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClPostProcessRewritesLineDirectivesInPreprocessedOutput(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fi := filepath.Join(dir, "out.i")
|
||||
// A #line directive as cl.exe's /P emits it: z:-prefixed, backslash
|
||||
// path, doubled ("escaped") backslashes, CRLF line ending.
|
||||
input := "#line 1 \"z:\\\\home\\\\user\\\\src\\\\hello.c\"\r\n" +
|
||||
"int main(void) { return 0; }\r\n"
|
||||
if err := os.WriteFile(fi, []byte(input), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
clPostProcess([]string{"/P", "/Fi" + fi, "hello.c"})
|
||||
|
||||
got, err := os.ReadFile(fi)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "#line 1 \"/home/user/src/hello.c\"\n" +
|
||||
"int main(void) { return 0; }\n"
|
||||
if string(got) != want {
|
||||
t.Errorf("clPostProcess output = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClPostProcessNoopWithoutP(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fi := filepath.Join(dir, "out.i")
|
||||
original := "#line 1 \"z:\\\\foo.c\"\r\n"
|
||||
if err := os.WriteFile(fi, []byte(original), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// No "/P" flag - should leave the file untouched even though -Fi is present.
|
||||
clPostProcess([]string{"/Fi" + fi, "foo.c"})
|
||||
|
||||
got, err := os.ReadFile(fi)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != original {
|
||||
t.Errorf("file was modified without /P: got %q, want unchanged %q", got, original)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClPostProcessNoopWithoutFi(t *testing.T) {
|
||||
// Must not panic or error when -Fi wasn't passed - just silently skip.
|
||||
clPostProcess([]string{"/P", "foo.c"})
|
||||
}
|
||||
|
||||
func TestClPostProcessMissingFileIsSilent(t *testing.T) {
|
||||
// The referenced -Fi file doesn't exist - clPostProcess must not panic.
|
||||
clPostProcess([]string{"/P", "/Fi" + filepath.Join(t.TempDir(), "missing.i"), "foo.c"})
|
||||
}
|
||||
+138
-14
@@ -29,7 +29,28 @@ func msbuildEnv(cfg *wineenv.Config, paths *wineenv.Paths) map[string]string {
|
||||
// tools onto the same UTC clock removes the mismatch.
|
||||
"TZ": "UTC",
|
||||
|
||||
// VCToolsVersion must be a real version string, not left unset:
|
||||
// Microsoft.Cpp.VCTools.props itself falls back to the literal
|
||||
// placeholder "VCToolsVersion_is_not_defined" whenever it's empty,
|
||||
// and that placeholder then reaches unconditional (not gated behind
|
||||
// CheckMSVCComponents) version-string comparisons elsewhere in
|
||||
// Microsoft.CppBuild.targets, e.g. the SegmentHeap manifest check's
|
||||
// VersionGreaterThanOrEquals(), which throws MSB4184 on a
|
||||
// non-version string.
|
||||
//
|
||||
// CheckMSVCComponents=false is what actually makes an aliased
|
||||
// PlatformToolset safe to combine with that real version: without
|
||||
// it, CheckVCToolsetVersion (Microsoft.CppBuild.targets, MSB8052)
|
||||
// rejects the combination whenever VCToolsVersion's numeric
|
||||
// generation doesn't match PlatformToolset's - exactly the legacy-
|
||||
// project case toolsetSuffixes/aliasPlatformToolsets exist for (a
|
||||
// v142 project against the one real, newer compiler actually
|
||||
// installed). Everything else CheckMSVCComponents gates
|
||||
// (Microsoft.CppBuild.targets ~495-535) is diagnostic-only - MFC/ATL/
|
||||
// Spectre component presence warnings, none of it feeding into the
|
||||
// actual compile/link - so disabling it costs nothing here.
|
||||
"DisableRegistryUse": "true",
|
||||
"CheckMSVCComponents": "false",
|
||||
"VCToolsVersion": cfg.MSVCVer,
|
||||
"VsInstallRoot": paths.BaseWin + `\`,
|
||||
"VSInstallDir": paths.BaseWin + `\`,
|
||||
@@ -58,20 +79,29 @@ func msbuildEnv(cfg *wineenv.Config, paths *wineenv.Paths) map[string]string {
|
||||
"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 + `\`
|
||||
// VCInstallDir_<N>/VCToolsInstallDir_<N> are consulted under two
|
||||
// completely different numbering schemes, both needing the single real
|
||||
// toolchain behind every <N> they might ask for:
|
||||
//
|
||||
// - Microsoft.Cpp.Default.props keys its early "is this toolset even
|
||||
// installed" check (the one MSB8020 comes from) off <N> = the
|
||||
// PlatformToolset suffix a .vcxproj actually declares (v142, v143,
|
||||
// v145, ...) - the same short name Microsoft stamps on
|
||||
// VC/Auxiliary/Build/Microsoft.VCToolsVersion.v<N>.default.props for
|
||||
// the downloaded compiler.
|
||||
// - Microsoft.CppBuild.targets (MSB8070) instead keys off <N> = the
|
||||
// MSBuild targets-schema version whose Microsoft.Cpp.props ended up
|
||||
// imported for this run (MSBuild/Microsoft/VC/v150|v160|v170|v180 -
|
||||
// fixed, shipped identically with every MSBuild release, unrelated to
|
||||
// which compiler is installed), to locate the specific toolset
|
||||
// version subfolder.
|
||||
//
|
||||
// Populate every <N> from both sources, all pointing at the one real
|
||||
// toolchain that's installed, so a project pinned to any PlatformToolset
|
||||
// resolves at every stage MSBuild checks it.
|
||||
for _, n := range toolsetSuffixes(paths.BaseUnix) {
|
||||
env["VCInstallDir_"+n] = paths.MSVCBaseWin + `\`
|
||||
env["VCToolsInstallDir_"+n] = paths.MSVCDirWin + `\`
|
||||
}
|
||||
|
||||
if strings.HasSuffix(paths.MSBuildBinDir, "amd64") {
|
||||
@@ -94,6 +124,100 @@ func msbuildEnv(cfg *wineenv.Config, paths *wineenv.Paths) map[string]string {
|
||||
return env
|
||||
}
|
||||
|
||||
// toolsetSuffixes collects every numeric <N> that either VCInstallDir_<N>
|
||||
// lookup mechanism (see msbuildEnv) might be asked to resolve for this
|
||||
// installation: PlatformToolset short names from
|
||||
// vc/Auxiliary/Build/Microsoft.VCToolsVersion.v<N>.default.props, MSBuild
|
||||
// targets-schema versions from MSBuild/Microsoft/VC/v<N>, and every
|
||||
// historical PlatformToolset name (see wineenv.KnownPlatformToolsets) - a
|
||||
// project pinned to any of them all resolves to the one real toolchain
|
||||
// installed.
|
||||
func toolsetSuffixes(baseUnix string) []string {
|
||||
seen := map[string]bool{}
|
||||
var suffixes []string
|
||||
|
||||
record := func(n string) {
|
||||
if seen[n] {
|
||||
return
|
||||
}
|
||||
seen[n] = true
|
||||
suffixes = append(suffixes, n)
|
||||
}
|
||||
|
||||
add := func(dir, prefix, suffix string) {
|
||||
matches, _ := filepath.Glob(filepath.Join(dir, "*"))
|
||||
for _, m := range matches {
|
||||
name := filepath.Base(m)
|
||||
if prefix != "" {
|
||||
if !strings.HasPrefix(name, prefix) {
|
||||
continue
|
||||
}
|
||||
name = strings.TrimPrefix(name, prefix)
|
||||
}
|
||||
name = strings.TrimSuffix(name, suffix)
|
||||
sub := reToolsetDir.FindStringSubmatch(name)
|
||||
if sub == nil {
|
||||
continue
|
||||
}
|
||||
record(sub[1])
|
||||
}
|
||||
}
|
||||
|
||||
add(filepath.Join(baseUnix, "vc", "Auxiliary", "Build"), "Microsoft.VCToolsVersion.", ".default.props")
|
||||
add(filepath.Join(baseUnix, "MSBuild", "Microsoft", "VC"), "", "")
|
||||
for _, n := range wineenv.KnownPlatformToolsets {
|
||||
record(n)
|
||||
}
|
||||
|
||||
return suffixes
|
||||
}
|
||||
|
||||
// reGlobalProp matches an MSBuild global-property command-line switch
|
||||
// ("/p:Name=...", "-property:Name=...", case-insensitive on both the
|
||||
// -p/-property spelling and the property name) so msbuildGlobalArgs can tell
|
||||
// whether the caller already pinned a given property themselves.
|
||||
func reGlobalProp(name string) *regexp.Regexp {
|
||||
return regexp.MustCompile(`(?i)^[-/](p|property):` + regexp.QuoteMeta(name) + `=`)
|
||||
}
|
||||
|
||||
// msbuildGlobalArgs returns /p: switches to prepend to an MSBuild invocation,
|
||||
// one per forced property not already present in args.
|
||||
//
|
||||
// WindowsTargetPlatformVersion is the one property that needs this rather
|
||||
// than an env var: unlike VCInstallDir_<N> (an input to a props-file
|
||||
// *lookup*, so any value msbuildEnv sets is visible no matter what a project
|
||||
// pins its PlatformToolset to), WindowsTargetPlatformVersion is itself the
|
||||
// value most legacy .vcxproj files hardcode directly in a PropertyGroup -
|
||||
// and an explicit PropertyGroup assignment always wins over an inherited
|
||||
// environment variable of the same name. A command-line global property is
|
||||
// the one thing a project file can't override, which is exactly what's
|
||||
// needed here: vintner only ever installs one Windows SDK version, so - same
|
||||
// reasoning as the PlatformToolset fallback above - any project should
|
||||
// transparently build against that one installed version rather than fail
|
||||
// outright over an exact version string it happened to be pinned to when
|
||||
// last saved from a real Windows SDK selector dropdown.
|
||||
func msbuildGlobalArgs(cfg *wineenv.Config, args []string) []string {
|
||||
forced := map[string]string{
|
||||
"WindowsTargetPlatformVersion": cfg.SDKVer,
|
||||
}
|
||||
|
||||
var out []string
|
||||
for name, value := range forced {
|
||||
re := reGlobalProp(name)
|
||||
alreadySet := false
|
||||
for _, a := range args {
|
||||
if re.MatchString(a) {
|
||||
alreadySet = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !alreadySet {
|
||||
out = append(out, "/p:"+name+"="+value)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func msbuildPlatform(arch string) string {
|
||||
switch arch {
|
||||
case "x86":
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/Cheviiot/vintner/internal/wineenv"
|
||||
)
|
||||
|
||||
func TestMsbuildPlatform(t *testing.T) {
|
||||
for _, tc := range []struct{ arch, want string }{
|
||||
{"x86", "Win32"},
|
||||
{"x64", "x64"},
|
||||
{"arm", "ARM"},
|
||||
{"arm64", "ARM64"},
|
||||
} {
|
||||
if got := msbuildPlatform(tc.arch); got != tc.want {
|
||||
t.Errorf("msbuildPlatform(%q) = %q, want %q", tc.arch, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTestPaths(t *testing.T, cfg *wineenv.Config) (*wineenv.Paths, string) {
|
||||
t.Helper()
|
||||
base := t.TempDir()
|
||||
return wineenv.NewPaths(cfg, base), base
|
||||
}
|
||||
|
||||
func TestMsbuildEnvBasics(t *testing.T) {
|
||||
cfg := &wineenv.Config{Arch: "x64", Host: "x64", DotnetHost: "amd64", MSVCVer: "14.51.36231", SDKVer: "10.0.26100.0"}
|
||||
paths, _ := newTestPaths(t, cfg)
|
||||
|
||||
env := msbuildEnv(cfg, paths)
|
||||
|
||||
if env["TZ"] != "UTC" {
|
||||
t.Errorf(`env["TZ"] = %q, want "UTC"`, env["TZ"])
|
||||
}
|
||||
if env["DisableRegistryUse"] != "true" {
|
||||
t.Errorf(`env["DisableRegistryUse"] = %q, want "true"`, env["DisableRegistryUse"])
|
||||
}
|
||||
if env["CheckMSVCComponents"] != "false" {
|
||||
t.Errorf(`env["CheckMSVCComponents"] = %q, want "false" (else CheckVCToolsetVersion errors on an aliased PlatformToolset)`, env["CheckMSVCComponents"])
|
||||
}
|
||||
// VCToolsVersion must be a real version string (see msbuildEnv's doc
|
||||
// comment on it: leaving it unset makes Microsoft.Cpp.VCTools.props
|
||||
// substitute a placeholder that then breaks unconditional version
|
||||
// comparisons elsewhere). CheckMSVCComponents=false is what keeps this
|
||||
// safe to combine with an aliased PlatformToolset.
|
||||
if env["VCToolsVersion"] != cfg.MSVCVer {
|
||||
t.Errorf(`env["VCToolsVersion"] = %q, want %q`, env["VCToolsVersion"], cfg.MSVCVer)
|
||||
}
|
||||
if env["WindowsTargetPlatformVersion"] != cfg.SDKVer {
|
||||
t.Errorf(`env["WindowsTargetPlatformVersion"] = %q, want %q`, env["WindowsTargetPlatformVersion"], cfg.SDKVer)
|
||||
}
|
||||
if env["Platform"] != "x64" {
|
||||
t.Errorf(`env["Platform"] = %q, want "x64"`, env["Platform"])
|
||||
}
|
||||
if env["SignMode"] != "off" {
|
||||
t.Errorf(`env["SignMode"] = %q, want "off" (driver builds must not attempt real signing)`, env["SignMode"])
|
||||
}
|
||||
// No WDK content on disk in this test - must not claim otherwise.
|
||||
if _, ok := env["WDKContentRoot"]; ok {
|
||||
t.Error(`env["WDKContentRoot"] set even though no wdk/<arch>/c directory exists`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsbuildEnvDiscoversEveryToolsetVersion(t *testing.T) {
|
||||
cfg := &wineenv.Config{Arch: "x64", Host: "x64", DotnetHost: "amd64", MSVCVer: "14.51.36231", SDKVer: "10.0.26100.0"}
|
||||
paths, base := newTestPaths(t, cfg)
|
||||
|
||||
// Real layout has two independent sources feeding VCInstallDir_<N>/
|
||||
// VCToolsInstallDir_<N> (see toolsetSuffixes' doc comment for why both
|
||||
// are needed): the PlatformToolset short names a downloaded compiler
|
||||
// ships default-props for, and MSBuild's own fixed schema-version dirs.
|
||||
buildDir := filepath.Join(base, "vc", "Auxiliary", "Build")
|
||||
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, name := range []string{
|
||||
"Microsoft.VCToolsVersion.v145.default.props",
|
||||
"Microsoft.VCToolsVersion.v143.default.props",
|
||||
"Microsoft.VCToolsVersion.default.props", // no version suffix - must not match
|
||||
} {
|
||||
if err := os.WriteFile(filepath.Join(buildDir, name), nil, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, v := range []string{"v180", "not-a-version"} {
|
||||
if err := os.MkdirAll(filepath.Join(base, "MSBuild", "Microsoft", "VC", v), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
env := msbuildEnv(cfg, paths)
|
||||
|
||||
for _, n := range []string{"145", "143", "180"} {
|
||||
if _, ok := env["VCInstallDir_"+n]; !ok {
|
||||
t.Errorf("expected VCInstallDir_%s to be set", n)
|
||||
}
|
||||
if _, ok := env["VCToolsInstallDir_"+n]; !ok {
|
||||
t.Errorf("expected VCToolsInstallDir_%s to be set", n)
|
||||
}
|
||||
}
|
||||
if _, ok := env["VCInstallDir_not-a-version"]; ok {
|
||||
t.Error("a directory not matching v<digits> should not have produced a VCInstallDir_ entry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolsetSuffixesDedupsOverlap(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
buildDir := filepath.Join(base, "vc", "Auxiliary", "Build")
|
||||
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(buildDir, "Microsoft.VCToolsVersion.v180.default.props"), nil, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(base, "MSBuild", "Microsoft", "VC", "v180"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := toolsetSuffixes(base)
|
||||
count := 0
|
||||
for _, n := range got {
|
||||
if n == "180" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
t.Errorf("toolsetSuffixes() returned %q with %d entries for \"180\" (from both sources), want exactly 1", got, count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsbuildGlobalArgsForcesWindowsTargetPlatformVersion(t *testing.T) {
|
||||
cfg := &wineenv.Config{SDKVer: "10.0.26100.0"}
|
||||
|
||||
got := msbuildGlobalArgs(cfg, []string{"Foo.sln", "/p:Configuration=Release"})
|
||||
want := "/p:WindowsTargetPlatformVersion=10.0.26100.0"
|
||||
found := false
|
||||
for _, a := range got {
|
||||
if a == want {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("msbuildGlobalArgs(...) = %v, want it to contain %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsbuildGlobalArgsRespectsExplicitOverride(t *testing.T) {
|
||||
cfg := &wineenv.Config{SDKVer: "10.0.26100.0"}
|
||||
|
||||
for _, explicit := range []string{
|
||||
"/p:WindowsTargetPlatformVersion=10.0.19041.0",
|
||||
"-p:WindowsTargetPlatformVersion=10.0.19041.0",
|
||||
"/property:WindowsTargetPlatformVersion=10.0.19041.0",
|
||||
} {
|
||||
got := msbuildGlobalArgs(cfg, []string{"Foo.sln", explicit})
|
||||
for _, a := range got {
|
||||
if reGlobalProp("WindowsTargetPlatformVersion").MatchString(a) {
|
||||
t.Errorf("msbuildGlobalArgs with explicit %q also injected %q - should have left the caller's value alone", explicit, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsbuildEnvDetectsWDKContentRoot(t *testing.T) {
|
||||
cfg := &wineenv.Config{Arch: "x64", Host: "x64", DotnetHost: "amd64", MSVCVer: "14.51.36231", SDKVer: "10.0.26100.0"}
|
||||
paths, base := newTestPaths(t, cfg)
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(base, "wdk", "x64", "c"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
env := msbuildEnv(cfg, paths)
|
||||
|
||||
if env["WDKContentRoot"] == "" {
|
||||
t.Error("expected WDKContentRoot to be set once wdk/x64/c exists on disk")
|
||||
}
|
||||
if env["WDKBuildFolder"] != cfg.SDKVer {
|
||||
t.Errorf(`env["WDKBuildFolder"] = %q, want %q`, env["WDKBuildFolder"], cfg.SDKVer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsbuildEnvPreferredToolArchitecture(t *testing.T) {
|
||||
// PreferredToolArchitecture should only be set when the host toolset
|
||||
// bin dir is the 64-bit ("amd64") .NET host - not for arm64.
|
||||
cfg64 := &wineenv.Config{Arch: "x64", Host: "x64", DotnetHost: "amd64", MSVCVer: "1", SDKVer: "1"}
|
||||
paths64, _ := newTestPaths(t, cfg64)
|
||||
if env := msbuildEnv(cfg64, paths64); env["PreferredToolArchitecture"] != "x64" {
|
||||
t.Errorf(`with DotnetHost=amd64, PreferredToolArchitecture = %q, want "x64"`, env["PreferredToolArchitecture"])
|
||||
}
|
||||
|
||||
cfgARM := &wineenv.Config{Arch: "arm64", Host: "arm64", DotnetHost: "arm64", MSVCVer: "1", SDKVer: "1"}
|
||||
pathsARM, _ := newTestPaths(t, cfgARM)
|
||||
if env := msbuildEnv(cfgARM, pathsARM); env["PreferredToolArchitecture"] != "" {
|
||||
t.Errorf(`with DotnetHost=arm64, PreferredToolArchitecture = %q, want unset`, env["PreferredToolArchitecture"])
|
||||
}
|
||||
}
|
||||
@@ -83,8 +83,10 @@ func Run(tool string, args []string) int {
|
||||
// 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...)...)
|
||||
// INCLUDE/LIB/WINEPATH, plus any global properties a project file
|
||||
// itself could otherwise override (see msbuildGlobalArgs).
|
||||
msArgs := append(msbuildGlobalArgs(cfg, rewritten), rewritten...)
|
||||
cmd := exec.Command(wineBin, append([]string{toolExePath}, msArgs...)...)
|
||||
env := buildEnv(paths)
|
||||
for k, v := range msbuildEnv(cfg, paths) {
|
||||
env = append(env, k+"="+v)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Cheviiot/vintner/internal/wineenv"
|
||||
)
|
||||
|
||||
func TestSpecExeDir(t *testing.T) {
|
||||
paths := &wineenv.Paths{
|
||||
BinDir: "/bin-dir",
|
||||
SDKBinDir: "/sdk-bin-dir",
|
||||
MSBuildBinDir: "/msbuild-bin-dir",
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
dir dirKind
|
||||
want string
|
||||
}{
|
||||
{"dirBin", dirBin, "/bin-dir"},
|
||||
{"dirSDK", dirSDK, "/sdk-bin-dir"},
|
||||
{"dirMSBuild", dirMSBuild, "/msbuild-bin-dir"},
|
||||
} {
|
||||
s := spec{dir: tc.dir}
|
||||
if got := s.exeDir(paths); got != tc.want {
|
||||
t.Errorf("%s: exeDir() = %q, want %q", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolsAndNativeToolsAreDisjoint(t *testing.T) {
|
||||
for name := range Tools {
|
||||
if nativeTools[name] {
|
||||
t.Errorf("%q is in both Tools and nativeTools", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEveryToolHasAnExeName(t *testing.T) {
|
||||
for name, s := range Tools {
|
||||
if s.exeName == "" {
|
||||
t.Errorf("Tools[%q] has no exeName", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user