3 Commits
Author SHA1 Message Date
Cheviiot 91471397fa Fix toolrelay.exe's mt.exe detection: it never matched a real path
IsMtExe() looked for the last '\\' in the target executable path to
find the bare filename, but vintner passes toolExePath straight from
Go's filepath.Join (forward slashes) all the way through to
CreateProcessW - so the path toolrelay.exe actually receives has no
backslash in it at all, and IsMtExe() compared the *entire path*
against "mt.exe", which of course never matched. The whole point of
this file - translating mt.exe's CMake-compatibility exit code
(0x41020001 -> 0xbb) before Wine's own exit-code truncation destroys
it - has silently never fired in real usage.

This had gone unnoticed because MSBuild's rawStdout path bypasses
toolrelay.exe entirely, and every real end-to-end test so far
(msbuild-driven builds, including the KMDF driver) went through that
path. Found while getting a CMake+Ninja-generated MSVC build of a
real project (Ogre3D) past its `cmake -E vs_link_exe` manifest step,
which depends on exactly this translation.

Now checks for both '\\' and '/' and uses whichever separator occurs
last in the path. Verified directly: `mt /manifest ... /notify_update`
through the wrapper now exits 187 (0xbb) instead of 1, and the Ogre
build gets past the manifest-embedding step it was failing at.
2026-07-25 11:55:05 +10:00
Cheviiot 94a19e6b43 Show git commit and build time in vintner version
Uses Go's automatic VCS build-info stamping (vcs.revision/vcs.time/
vcs.modified, embedded by `go build` since Go 1.18 - no -ldflags
changes needed, works the same for a CI release build and a plain
local `go build`). Knowing the exact commit a bug report's binary was
built from, not just the X.Y.Z tag, is the point - two builds of the
same tag could still differ.

Split the pure formatting logic (formatVersion) from the
debug.ReadBuildInfo() call so it's actually unit-testable: `go test`
binaries don't get VCS stamping the way `go build` ones do, so there
was no way to exercise the revision-formatting branch through
versionString() itself.
2026-07-25 11:37:02 +10:00
Cheviiot 6186837fd2 docs: note that Nivora installs get shell completion automatically 2026-07-25 11:33:41 +10:00
12 changed files with 801 additions and 3 deletions
+2
View File
@@ -157,6 +157,8 @@ Error text from internal packages stays in English regardless.
## Shell completion ## Shell completion
Already set up if you installed via Nivora. Otherwise:
```bash ```bash
source <(vintner completion bash) # or add to ~/.bashrc source <(vintner completion bash) # or add to ~/.bashrc
source <(vintner completion zsh) # or add to ~/.zshrc source <(vintner completion zsh) # or add to ~/.zshrc
+10 -2
View File
@@ -79,8 +79,16 @@ HANDLE MakeKillOnCloseJob() {
} }
bool IsMtExe(const wchar_t *path) { bool IsMtExe(const wchar_t *path) {
const wchar_t *name = wcsrchr(path, L'\\'); // vintner passes toolExePath straight through from Go's filepath.Join,
name = name ? name + 1 : path; // 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; return _wcsicmp(name, L"mt.exe") == 0;
} }
+1 -1
View File
@@ -50,7 +50,7 @@ func runCLI(args []string) int {
case "completion": case "completion":
return runCompletion(args[1:]) return runCompletion(args[1:])
case "version", "v", "--version": case "version", "v", "--version":
fmt.Println("vintner " + version) fmt.Println(versionString())
return 0 return 0
case "-h", "--help", "help", "h": case "-h", "--help", "help", "h":
printUsage() printUsage()
+56
View File
@@ -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)
}
+46
View File
@@ -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)
}
})
}
}
+152
View File
@@ -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)
}
}
+59
View File
@@ -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)
}
}
}
+134
View File
@@ -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)
}
}
}
+118
View File
@@ -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)
}
}
+61
View File
@@ -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"})
}
+117
View File
@@ -0,0 +1,117 @@
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["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)
for _, v := range []string{"v145", "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", "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 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"])
}
}
+45
View File
@@ -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)
}
}
}