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.
This commit is contained in:
Cheviiot
2026-07-25 11:55:05 +10:00
parent 94a19e6b43
commit 91471397fa
8 changed files with 696 additions and 2 deletions
+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)
}
}
}