Prompted by a real incident: an MSBuild node-reuse worker (its own
/nodeReuse:true default) survived a build getting interrupted, came
back deadlocked, and got reused by the next `msbuild` invocation -
which then failed with a confusing, unrelated-looking
`System.TypeLoadException` on Microsoft.VisualStudio.Telemetry on
every call for hours, until the stale process was killed by hand.
That's exactly the "unrelated blocker" noted in this repo's own
earlier session notes (CLAUDE.md) while debugging a real project's
build - it wasn't a missing dependency, it was a corrupted reused
process.
Two changes:
- vintner now forces /nodeReuse:false on every msbuild invocation
(unless the caller already passed their own /nodeReuse or /nr
switch), so a wedged worker can never poison a later, unrelated
build in the first place. Costs each invocation the couple-hundred-
ms/node startup time node reuse exists to save.
- VINTNER_TIMEOUT (a duration string, e.g. "30m") bounds how long any
single tool invocation is allowed to run, for the case something
wedges that isn't MSBuild-specific. Every exec.Command site in
internal/wrapper now goes through a shared newToolCommand
constructor that, when the timeout is set, kills the *whole*
process group (not just the immediate `wine` process - a wedged
child surviving under it is exactly the scenario this needs to
reach) via a context deadline, and reports a clear "timed out after
Xm" message (exit 124, matching the timeout(1) convention) instead
of a bare "signal: killed". Unset by default - every real build
observed stays unbounded, matching Windows' own behavior.
Verified end-to-end, not just at the unit level: a real `sleep 30`
through the `cmd` native wrapper with VINTNER_TIMEOUT=1s was killed
within the deadline and reported the timeout clearly (exit 124); a
real `cl` invocation with the same 1s timeout finished normally
(0.26s) without being mistaken for a hang.
vintner only ever downloads one compiler generation, but real-world
.vcxproj files are pinned to whichever PlatformToolset they were last
saved under - v142 (VS2019) for anything not actively maintained is
extremely common. MSBuild checks toolset "installed-ness" (MSB8020) by
testing whether MSBuild/Microsoft/VC/v<schema>/Platforms/<arch>/
PlatformToolsets/<toolset>/ exists on disk - a plain file lookup our
downloaded MSBuild package only satisfies for the exact generation it
shipped. `install` now symlinks every historical numeric PlatformToolset
name (v90 through v143) onto whichever real toolset directory is
actually present, so any of them resolves transparently; Toolset.props/
.targets don't hardcode a version number, so aliasing is correct, not
just a workaround.
Three more MSBuild property/environment issues came with it, all found
building a real years-old project against the one modern toolchain
vintner installs:
- VCInstallDir_<N>/VCToolsInstallDir_<N> needed a third numbering
source (PlatformToolset short names from Microsoft.VCToolsVersion.
v<N>.default.props) alongside the existing MSBuild schema-version and
known-toolset lists, so the env-var-driven half of toolset resolution
covers the same names the on-disk alias does.
- VCToolsVersion must be a real version string: left unset, it falls
back to a literal placeholder that then hits an unconditional
version-string comparison elsewhere in Microsoft.CppBuild.targets
(MSB4184). Setting it to the real installed version in turn requires
CheckMSVCComponents=false, since CheckVCToolsetVersion (MSB8052)
otherwise rejects an aliased PlatformToolset whenever its numeric
generation doesn't match VCToolsVersion's - exactly the case aliasing
creates on purpose. Everything else CheckMSVCComponents gates is
diagnostic-only (MFC/ATL/Spectre presence warnings), so disabling it
costs nothing else.
- WindowsTargetPlatformVersion needed to become an explicit /p: global
property on the msbuild command line, not just an env var: legacy
.vcxproj files commonly hardcode this in a PropertyGroup, and an
explicit project 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. Only injected when the caller
hasn't already pinned it themselves.
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.