mirror of
https://github.com/Cheviiot/Vintner.git
synced 2026-08-03 15:57:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91471397fa | ||
|
|
94a19e6b43 | ||
|
|
6186837fd2 |
@@ -157,6 +157,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;
|
||||
}
|
||||
|
||||
|
||||
+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,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)
|
||||
}
|
||||
}
|
||||
@@ -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"})
|
||||
}
|
||||
@@ -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"])
|
||||
}
|
||||
}
|
||||
@@ -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