Build old (pre-v145) PlatformToolset-pinned .vcxproj files

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.
This commit is contained in:
Cheviiot
2026-07-25 15:41:09 +10:00
parent 4795c7c105
commit 25f751e874
7 changed files with 448 additions and 22 deletions
+92
View File
@@ -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
}