2 Commits
Author SHA1 Message Date
Cheviiot 25f751e874 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.
2026-07-25 15:41:09 +10:00
Cheviiot 4795c7c105 Add DirectX SDK (D3DX9) download support
download --with-dxsdk fetches the DirectX SDK (June 2010) - the last
standalone release of D3DX9/10/11, XInput and XAudio2, dropped from
the Windows SDK entirely once D3DX was deprecated, but still needed
by plenty of legacy code. Like the WDK, it isn't part of the VS
installer manifest, so this is its own self-contained fetch+unpack
path: the installer is a self-extracting CAB, unpacked directly via
cabextract (already a prerequisite for the WinSDK .msi payloads) with
its -F filter restricting extraction to just Include/ and Lib/ -
about 21MB out of the installer's 1.2GB uncompressed payload.

Verified against the real installer: real d3dx9.h and d3dx9.lib
(x86 and x64) extracted correctly and linked into an actual legacy
game client build.
2026-07-25 15:40:41 +10:00
12 changed files with 708 additions and 34 deletions
+27 -8
View File
@@ -8,8 +8,9 @@ vintner cross-compiles with the real MSVC toolchain on Linux, using Wine.
One Go binary drops in as `cl`, `link`, `lib`, `rc`, `midl`, `mc`, `mt`,
`dumpbin`, `msbuild`, `nmake`, `ml`, `ml64`, `armasm`, `armasm64`, plus
`cmd`/`findstr` shims, so once installed you invoke the real Microsoft
tools exactly like on Windows. It handles full MSBuild projects, and with
`--with-wdk`, real KMDF/UMDF Windows drivers.
tools exactly like on Windows. It handles full MSBuild projects, with
`--with-wdk` real KMDF/UMDF Windows drivers, and with `--with-dxsdk` the
real D3DX9 headers/libs.
Inspired by [mstorsjo/msvc-wine](https://github.com/mstorsjo/msvc-wine)'s
approach: download the real MSVC/WinSDK, wrap the compiler under Wine.
@@ -21,6 +22,7 @@ approach: download the real MSVC/WinSDK, wrap the compiler under Wine.
- [Quick start](#quick-start)
- [Commands](#commands)
- [Building drivers (WDK)](#building-drivers-wdk)
- [Building against D3DX9 (DirectX SDK)](#building-against-d3dx9-directx-sdk)
- [Language](#language)
- [Shell completion](#shell-completion)
- [Using clang-cl/lld-link instead of Wine](#using-clang-cllld-link-instead-of-wine)
@@ -112,12 +114,13 @@ vintner completion bash|zsh print a shell
`download`'s main options: `--msvc-version`, `--sdk-version`,
`--architecture` (repeatable: `x86`/`x64`/`arm`/`arm64`/`host`),
`--host-arch`, `--only-host`, `--with-wdk` (see below), `--ignore`
(repeatable), `--only-download`, `--only-unpack`, `--keep-unpack`,
`--skip-patch`, `--cache`, `--language`, `--include-optional`,
`--skip-recommended`, `--major`, `--preview`, `--manifest`,
`--list-workloads`, `--list-components`, `--print-deps-tree`. Run
`vintner download -h` for the full list with descriptions.
`--host-arch`, `--only-host`, `--with-wdk` (see below), `--with-dxsdk`
(see below), `--ignore` (repeatable), `--only-download`, `--only-unpack`,
`--keep-unpack`, `--skip-patch`, `--cache`, `--language`,
`--include-optional`, `--skip-recommended`, `--major`, `--preview`,
`--manifest`, `--list-workloads`, `--list-components`,
`--print-deps-tree`. Run `vintner download -h` for the full list with
descriptions.
`--list-workloads`/`--list-components` print every workload/component id
and its human-readable title from the fetched manifest, then exit
@@ -143,6 +146,22 @@ work under Wine. Tested against a real sample driver from
Only x64 and arm64 targets have a WDK package upstream; there's no x86 or
arm one.
## Building against D3DX9 (DirectX SDK)
`--with-dxsdk` fetches the DirectX SDK (June 2010) — the last standalone
release of D3DX9/10/11, XInput and XAudio2, dropped from the Windows SDK
entirely once D3DX was deprecated. It unpacks the real headers and x86/x64
import libs (`d3dx9.h`/`d3dx9.lib` included) to `<dest>/DXSDK`.
```bash
vintner download --accept-license --with-dxsdk
```
Point your project's `IncludePath`/`LibraryPath` at
`<dest>/DXSDK/Include` and `<dest>/DXSDK/Lib/x86` or `<dest>/DXSDK/Lib/x64`.
Requires `cabextract` on `PATH` (the installer is a self-extracting CAB
archive).
## Language
CLI text (usage, progress lines, prompts) defaults to English. Set
+20
View File
@@ -35,6 +35,7 @@ func runDownload(args []string) int {
listComponents := fs.Bool("list-components", false, "list available components from the manifest and exit, without downloading anything")
printDepsTree := fs.Bool("print-deps-tree", false, "print the dependency tree of the selected packages and exit, without downloading anything")
withWDK := fs.Bool("with-wdk", false, "also fetch and install the Windows Driver Kit (headers, libs and MSBuild driver PlatformToolsets, for building KMDF/UMDF drivers)")
withDXSDK := fs.Bool("with-dxsdk", false, "also fetch and install the DirectX SDK (June 2010): real D3DX9/10/11, XInput and XAudio2 headers and import libs, dropped from the modern Windows SDK")
var archsFlag stringList
fs.Var(&archsFlag, "architecture", "target architecture to include (x86, x64, arm, arm64, host); repeatable")
var ignoreFlag stringList
@@ -219,6 +220,13 @@ func runDownload(args []string) int {
}
}
if *withDXSDK && !*onlyUnpack {
if err := downloadDXSDK(cache, destAbs); err != nil {
fmt.Fprintln(os.Stderr, "vintner download:", err)
return 1
}
}
fmt.Println(i18n.T("download.done", destAbs))
return 0
}
@@ -255,6 +263,18 @@ func downloadWDK(opts *download.Options, selected []*download.Package, cache, de
return nil
}
// downloadDXSDK fetches and unpacks the DirectX SDK (June 2010) into
// destAbs/DXSDK. See internal/download/dxsdk.go for why this is a separate
// download path from the rest of ExpandSelection/FetchPayloads/Unpack.
func downloadDXSDK(cache, destAbs string) error {
dxsdkDir, err := download.DownloadDXSDK(cache, destAbs)
if err != nil {
return err
}
fmt.Print(i18n.T("download.dxsdk_installed", dxsdkDir))
return nil
}
func contains(list []string, v string) bool {
for _, s := range list {
if s == v {
+94
View File
@@ -0,0 +1,94 @@
package download
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
// The DirectX SDK (June 2010) is the last standalone release of D3DX9 (and
// D3DX10/11, XInput, XAudio2, ...). Microsoft never carried D3DX forward
// into the Windows 10/11 SDK - it's deprecated in favor of D3DCompiler/
// WICTextureLoader/DirectXTex, but plenty of legacy code (this included)
// still links against the real d3dx9.h/d3dx9.lib. Like the WDK (see
// wdk.go), it isn't part of the VS installer manifest or any package feed
// vsman knows about, so this is a separate, self-contained download path
// outside ExpandSelection/FetchPayloads/UnpackSelectedPackages.
//
// The installer is a self-extracting PE with an appended CAB archive.
// cabextract (already a vintner prerequisite - see runMsiExtract's sibling
// extractWindowsSDKPackage) unpacks it directly, without needing Wine or a
// separate archive tool. Its -F/--filter flag (repeatable) restricts
// extraction to the Include and Lib subtrees actually needed for building -
// about 21MB out of the installer's 1.2GB uncompressed payload.
const dxsdkURL = "https://download.microsoft.com/download/A/E/7/AE743F1F-632B-4809-87A9-AA1BB3458E31/DXSDK_Jun10.exe"
// dxsdkSHA256 pins the exact installer build this code was written against
// (verified by fully extracting it with both cabextract and 7z and cross
// checking the file lists) - the June 2010 DirectX SDK is a frozen legacy
// artifact Microsoft is not going to rebuild. A var, not a const, so tests
// can point it at a small fake payload instead of the real 600MB installer.
var dxsdkSHA256 = "705271dc83bfee54d9b94e028426e288d5f070784b7446d164f48ecfbb2a02cb"
// DownloadDXSDK fetches (or reuses a cached copy of) the DirectX SDK (June
// 2010) installer into cacheDir, then unpacks its Include and Lib trees -
// headers and x86/x64 import libs for D3DX9/10/11, XInput, XAudio2, and the
// rest - into destDir/DXSDK. Returns that directory.
func DownloadDXSDK(cacheDir, destDir string) (string, error) {
if _, err := exec.LookPath("cabextract"); err != nil {
return "", fmt.Errorf("cabextract not found in PATH (install the cabextract package): %w", err)
}
cacheFile := filepath.Join(cacheDir, "DXSDK_Jun10.exe")
if !isFile(cacheFile) {
fmt.Println("Downloading DirectX SDK (June 2010)")
if err := httpDownloadFile(dxsdkURL, cacheFile); err != nil {
return "", fmt.Errorf("downloading DirectX SDK: %w", err)
}
} else {
fmt.Println("Using existing file", filepath.Base(cacheFile))
}
sum, err := sha256File(cacheFile)
if err != nil {
return "", err
}
if !equalFoldHex(sum, dxsdkSHA256) {
return "", fmt.Errorf("incorrect hash for downloaded file %s, aborting", filepath.Base(cacheFile))
}
scratch, err := os.MkdirTemp(destDir, "dxsdk-unpack-")
if err != nil {
return "", err
}
defer os.RemoveAll(scratch)
if err := runCabextract(cacheFile, scratch, "DXSDK/Include/*", "DXSDK/Lib/*"); err != nil {
return "", fmt.Errorf("extracting DirectX SDK: %w", err)
}
dxsdkDir := filepath.Join(destDir, "DXSDK")
if err := combineDirTrees(filepath.Join(scratch, "DXSDK"), dxsdkDir); err != nil {
return "", fmt.Errorf("moving DirectX SDK content into place: %w", err)
}
return dxsdkDir, nil
}
// runCabextract extracts srcFile into destDir, restricted to entries
// matching any of patterns (cabextract's -F, repeatable, glob-matched
// against the full in-archive path).
func runCabextract(srcFile, destDir string, patterns ...string) error {
if err := os.MkdirAll(destDir, 0o755); err != nil {
return err
}
args := []string{"-q", "-d", destDir}
for _, p := range patterns {
args = append(args, "-F", p)
}
args = append(args, srcFile)
cmd := exec.Command("cabextract", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
+111
View File
@@ -0,0 +1,111 @@
package download
import (
"os"
"path/filepath"
"runtime"
"testing"
)
// withFakeCabextract prepends a directory containing a fake "cabextract"
// script to PATH, so tests can exercise runCabextract/DownloadDXSDK without
// the real tool (or a real DXSDK installer) present. The fake script
// records the arguments it was invoked with to argsFile and creates an
// empty DXSDK/Include and DXSDK/Lib under whatever -d directory it was
// given, mimicking a successful (if empty) extraction.
func withFakeCabextract(t *testing.T, argsFile string) {
t.Helper()
if runtime.GOOS == "windows" {
t.Skip("fake cabextract script requires a POSIX shell")
}
bin := t.TempDir()
script := `#!/bin/sh
echo "$@" > "` + argsFile + `"
dest=""
prev=""
for a in "$@"; do
if [ "$prev" = "-d" ]; then
dest="$a"
fi
prev="$a"
done
mkdir -p "$dest/DXSDK/Include" "$dest/DXSDK/Lib"
`
path := filepath.Join(bin, "cabextract")
if err := os.WriteFile(path, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", bin+":"+os.Getenv("PATH"))
}
func TestRunCabextractPassesFilterArgs(t *testing.T) {
dest := t.TempDir()
argsFile := filepath.Join(t.TempDir(), "args")
withFakeCabextract(t, argsFile)
src := filepath.Join(t.TempDir(), "installer.exe")
writeFile(t, src, "fake-installer-bytes")
if err := runCabextract(src, dest, "DXSDK/Include/*", "DXSDK/Lib/*"); err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(argsFile)
if err != nil {
t.Fatalf("expected the fake cabextract to have run: %v", err)
}
want := "-q -d " + dest + " -F DXSDK/Include/* -F DXSDK/Lib/* " + src + "\n"
if string(got) != want {
t.Errorf("cabextract args = %q, want %q", got, want)
}
}
func TestDownloadDXSDKReusesCachedFileAndRejectsHashMismatch(t *testing.T) {
argsFile := filepath.Join(t.TempDir(), "args")
withFakeCabextract(t, argsFile)
cacheDir := t.TempDir()
destDir := t.TempDir()
writeFile(t, filepath.Join(cacheDir, "DXSDK_Jun10.exe"), "not the real installer")
_, err := DownloadDXSDK(cacheDir, destDir)
if err == nil {
t.Fatal("expected an error for a cached file that doesn't match dxsdkSHA256")
}
if _, statErr := os.Stat(argsFile); statErr == nil {
t.Error("cabextract should not have run before the hash was verified")
}
}
func TestDownloadDXSDKMovesExtractedContentIntoPlace(t *testing.T) {
argsFile := filepath.Join(t.TempDir(), "args")
withFakeCabextract(t, argsFile)
cacheDir := t.TempDir()
destDir := t.TempDir()
cacheFile := filepath.Join(cacheDir, "DXSDK_Jun10.exe")
writeFile(t, cacheFile, "fake-installer-bytes-for-hash-test")
// Patch the expected hash to match our fake cached file, since we can't
// (and shouldn't) fetch or embed the real 600MB installer in a test.
sum, err := sha256File(cacheFile)
if err != nil {
t.Fatal(err)
}
restore := dxsdkSHA256
dxsdkSHA256 = sum
defer func() { dxsdkSHA256 = restore }()
dxsdkDir, err := DownloadDXSDK(cacheDir, destDir)
if err != nil {
t.Fatal(err)
}
if dxsdkDir != filepath.Join(destDir, "DXSDK") {
t.Errorf("DownloadDXSDK returned %q, want %q", dxsdkDir, filepath.Join(destDir, "DXSDK"))
}
for _, sub := range []string{"Include", "Lib"} {
if !isDir(filepath.Join(dxsdkDir, sub)) {
t.Errorf("expected %s/%s to exist after extraction", dxsdkDir, sub)
}
}
}
+8 -4
View File
@@ -77,8 +77,8 @@ Usage:
vintner completion bash|zsh print a shell completion script
Run "vintner <command> --help" for that command's own options - download
has many, including --with-wdk, --list-workloads, --list-components and
--print-deps-tree.
has many, including --with-wdk, --with-dxsdk, --list-workloads,
--list-components and --print-deps-tree.
--dest/[dir] default to ~/.vintner if omitted.
Language: set VINTNER_LANG=ru (or LANG=ru_RU...) for Russian output.
@@ -99,8 +99,8 @@ Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
vintner completion bash|zsh вывести скрипт автодополнения для оболочки
Запустите «vintner <команда> --help» для параметров конкретной команды —
у download их много, включая --with-wdk, --list-workloads, --list-components
и --print-deps-tree.
у download их много, включая --with-wdk, --with-dxsdk, --list-workloads,
--list-components и --print-deps-tree.
--dest/[каталог] по умолчанию — ~/.vintner.
Язык: установите VINTNER_LANG=en (или LANG=en_US...) для вывода на английском.
@@ -161,6 +161,10 @@ Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
EN: "Installed WDK (%s) %s at %s\n",
RU: "WDK (%s) %s установлен в %s\n",
},
"download.dxsdk_installed": {
EN: "Installed DirectX SDK (June 2010) at %s\n",
RU: "DirectX SDK (июнь 2010) установлен в %s\n",
},
"download.workloads_header": {
EN: "Available Workloads (%d):\n",
RU: "Доступные рабочие нагрузки (Workload) (%d):\n",
+4
View File
@@ -62,6 +62,10 @@ func Install(dest, selfBinary string) error {
return err
}
if err := aliasPlatformToolsets(dest); err != nil {
return err
}
includeDir := filepath.Join(msvcDir, "include")
if err := Lowercase(includeDir, LowercaseOptions{Symlink: true}); err != nil {
return fmt.Errorf("lowercasing %s: %w", includeDir, err)
+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
}
+106
View File
@@ -0,0 +1,106 @@
package install
import (
"os"
"path/filepath"
"testing"
"github.com/Cheviiot/vintner/internal/wineenv"
)
func TestAliasPlatformToolsetsAliasesRealToolset(t *testing.T) {
dest := t.TempDir()
toolsetsDir := filepath.Join(dest, "MSBuild", "Microsoft", "VC", "v180", "Platforms", "x64", "PlatformToolsets")
realDir := filepath.Join(toolsetsDir, "v145")
if err := os.MkdirAll(realDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(realDir, "Toolset.props"), []byte("<Project/>"), 0o644); err != nil {
t.Fatal(err)
}
// A WDK toolset entry alongside it - must not be mistaken for the real
// numeric toolset or itself get aliased over.
if err := os.MkdirAll(filepath.Join(toolsetsDir, "WindowsKernelModeDriver10.0"), 0o755); err != nil {
t.Fatal(err)
}
if err := aliasPlatformToolsets(dest); err != nil {
t.Fatalf("aliasPlatformToolsets: %v", err)
}
for _, n := range wineenv.KnownPlatformToolsets {
alias := filepath.Join(toolsetsDir, "v"+n)
fi, err := os.Lstat(alias)
if err != nil {
t.Errorf("expected v%s alias to exist: %v", n, err)
continue
}
if fi.Mode()&os.ModeSymlink == 0 {
t.Errorf("v%s should be a symlink, got mode %v", n, fi.Mode())
continue
}
target, err := os.Readlink(alias)
if err != nil {
t.Fatal(err)
}
if target != "v145" {
t.Errorf("v%s symlink target = %q, want \"v145\"", n, target)
}
// Follow the alias and confirm it actually reaches the real content.
if !isFile(filepath.Join(toolsetsDir, "v"+n, "Toolset.props")) {
t.Errorf("v%s/Toolset.props not reachable through the alias", n)
}
}
if exists(filepath.Join(toolsetsDir, "WindowsKernelModeDriver10.0", "v"+wineenv.KnownPlatformToolsets[0])) {
t.Error("WDK toolset directory should not have been touched")
}
}
func TestAliasPlatformToolsetsDoesNotOverwriteExisting(t *testing.T) {
dest := t.TempDir()
toolsetsDir := filepath.Join(dest, "MSBuild", "Microsoft", "VC", "v180", "Platforms", "x64", "PlatformToolsets")
if err := os.MkdirAll(filepath.Join(toolsetsDir, "v145"), 0o755); err != nil {
t.Fatal(err)
}
// v142 already genuinely installed (e.g. a real VS install with several
// side-by-side toolsets) - must be left alone, not replaced with an alias.
real142 := filepath.Join(toolsetsDir, "v142")
if err := os.MkdirAll(real142, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(real142, "Toolset.props"), []byte("<!-- real v142 -->"), 0o644); err != nil {
t.Fatal(err)
}
if err := aliasPlatformToolsets(dest); err != nil {
t.Fatalf("aliasPlatformToolsets: %v", err)
}
fi, err := os.Lstat(real142)
if err != nil {
t.Fatal(err)
}
if fi.Mode()&os.ModeSymlink != 0 {
t.Error("pre-existing v142 directory should not have been replaced with a symlink")
}
}
func TestAliasPlatformToolsetsNoNumericToolset(t *testing.T) {
dest := t.TempDir()
// Only a WDK-style toolset present, nothing numeric to alias from.
toolsetsDir := filepath.Join(dest, "MSBuild", "Microsoft", "VC", "v180", "Platforms", "x64", "PlatformToolsets")
if err := os.MkdirAll(filepath.Join(toolsetsDir, "WindowsUserModeDriver10.0"), 0o755); err != nil {
t.Fatal(err)
}
if err := aliasPlatformToolsets(dest); err != nil {
t.Fatalf("aliasPlatformToolsets: %v", err)
}
for _, n := range wineenv.KnownPlatformToolsets {
if exists(filepath.Join(toolsetsDir, "v"+n)) {
t.Errorf("v%s should not have been created with no real numeric toolset present", n)
}
}
}
+15
View File
@@ -0,0 +1,15 @@
package wineenv
// KnownPlatformToolsets are every numeric PlatformToolset short name
// Microsoft.Cpp.Default.props has ever defined a
// _PlatformToolsetShortNameFor_v<N> entry for (VS2013 through the VS2022
// initial release; excludes the _xp/_wp80/_wp81 variants, which aren't
// purely numeric). vintner only ever installs one compiler generation, but
// real .vcxproj files in the wild are pinned to whichever generation they
// were last edited under - v142 (VS2019) for anything not yet retargeted is
// extremely common. Shared between internal/install (which symlinks these
// names onto the one real MSBuild PlatformToolsets directory) and
// internal/wrapper (which mirrors the same names onto VCInstallDir_<N>/
// VCToolsInstallDir_<N> for the older, environment-variable-driven toolset
// redirect chain) - see doc comments there for why both are needed.
var KnownPlatformToolsets = []string{"90", "100", "110", "120", "140", "141", "142", "143"}
+142 -18
View File
@@ -29,10 +29,31 @@ func msbuildEnv(cfg *wineenv.Config, paths *wineenv.Paths) map[string]string {
// tools onto the same UTC clock removes the mismatch.
"TZ": "UTC",
"DisableRegistryUse": "true",
"VCToolsVersion": cfg.MSVCVer,
"VsInstallRoot": paths.BaseWin + `\`,
"VSInstallDir": paths.BaseWin + `\`,
// VCToolsVersion must be a real version string, not left unset:
// Microsoft.Cpp.VCTools.props itself falls back to the literal
// placeholder "VCToolsVersion_is_not_defined" whenever it's empty,
// and that placeholder then reaches unconditional (not gated behind
// CheckMSVCComponents) version-string comparisons elsewhere in
// Microsoft.CppBuild.targets, e.g. the SegmentHeap manifest check's
// VersionGreaterThanOrEquals(), which throws MSB4184 on a
// non-version string.
//
// CheckMSVCComponents=false is what actually makes an aliased
// PlatformToolset safe to combine with that real version: without
// it, CheckVCToolsetVersion (Microsoft.CppBuild.targets, MSB8052)
// rejects the combination whenever VCToolsVersion's numeric
// generation doesn't match PlatformToolset's - exactly the legacy-
// project case toolsetSuffixes/aliasPlatformToolsets exist for (a
// v142 project against the one real, newer compiler actually
// installed). Everything else CheckMSVCComponents gates
// (Microsoft.CppBuild.targets ~495-535) is diagnostic-only - MFC/ATL/
// Spectre component presence warnings, none of it feeding into the
// actual compile/link - so disabling it costs nothing here.
"DisableRegistryUse": "true",
"CheckMSVCComponents": "false",
"VCToolsVersion": cfg.MSVCVer,
"VsInstallRoot": paths.BaseWin + `\`,
"VSInstallDir": paths.BaseWin + `\`,
"SDKReferenceDirectoryRoot": paths.BaseWin + `\`,
"SDKExtensionDirectoryRoot": paths.BaseWin + `\`,
@@ -58,20 +79,29 @@ func msbuildEnv(cfg *wineenv.Config, paths *wineenv.Paths) map[string]string {
"Platform": msbuildPlatform(cfg.Arch),
}
// Microsoft.Cpp.props resolves the compiler/toolset location through
// VCInstallDir_<N>/VCToolsInstallDir_<N>, where <N> is whatever numeric
// suffix the installed MSBuild toolset property sheets use (e.g.
// .../MSBuild/Microsoft/VC/v180 -> "180"). Populate every one actually
// present, so a project pinned to any of them resolves to the one real
// toolchain that's installed.
matches, _ := filepath.Glob(filepath.Join(paths.BaseUnix, "MSBuild", "Microsoft", "VC", "v*"))
for _, m := range matches {
sub := reToolsetDir.FindStringSubmatch(filepath.Base(m))
if sub == nil {
continue
}
env["VCInstallDir_"+sub[1]] = paths.MSVCBaseWin + `\`
env["VCToolsInstallDir_"+sub[1]] = paths.MSVCDirWin + `\`
// VCInstallDir_<N>/VCToolsInstallDir_<N> are consulted under two
// completely different numbering schemes, both needing the single real
// toolchain behind every <N> they might ask for:
//
// - Microsoft.Cpp.Default.props keys its early "is this toolset even
// installed" check (the one MSB8020 comes from) off <N> = the
// PlatformToolset suffix a .vcxproj actually declares (v142, v143,
// v145, ...) - the same short name Microsoft stamps on
// VC/Auxiliary/Build/Microsoft.VCToolsVersion.v<N>.default.props for
// the downloaded compiler.
// - Microsoft.CppBuild.targets (MSB8070) instead keys off <N> = the
// MSBuild targets-schema version whose Microsoft.Cpp.props ended up
// imported for this run (MSBuild/Microsoft/VC/v150|v160|v170|v180 -
// fixed, shipped identically with every MSBuild release, unrelated to
// which compiler is installed), to locate the specific toolset
// version subfolder.
//
// Populate every <N> from both sources, all pointing at the one real
// toolchain that's installed, so a project pinned to any PlatformToolset
// resolves at every stage MSBuild checks it.
for _, n := range toolsetSuffixes(paths.BaseUnix) {
env["VCInstallDir_"+n] = paths.MSVCBaseWin + `\`
env["VCToolsInstallDir_"+n] = paths.MSVCDirWin + `\`
}
if strings.HasSuffix(paths.MSBuildBinDir, "amd64") {
@@ -94,6 +124,100 @@ func msbuildEnv(cfg *wineenv.Config, paths *wineenv.Paths) map[string]string {
return env
}
// toolsetSuffixes collects every numeric <N> that either VCInstallDir_<N>
// lookup mechanism (see msbuildEnv) might be asked to resolve for this
// installation: PlatformToolset short names from
// vc/Auxiliary/Build/Microsoft.VCToolsVersion.v<N>.default.props, MSBuild
// targets-schema versions from MSBuild/Microsoft/VC/v<N>, and every
// historical PlatformToolset name (see wineenv.KnownPlatformToolsets) - a
// project pinned to any of them all resolves to the one real toolchain
// installed.
func toolsetSuffixes(baseUnix string) []string {
seen := map[string]bool{}
var suffixes []string
record := func(n string) {
if seen[n] {
return
}
seen[n] = true
suffixes = append(suffixes, n)
}
add := func(dir, prefix, suffix string) {
matches, _ := filepath.Glob(filepath.Join(dir, "*"))
for _, m := range matches {
name := filepath.Base(m)
if prefix != "" {
if !strings.HasPrefix(name, prefix) {
continue
}
name = strings.TrimPrefix(name, prefix)
}
name = strings.TrimSuffix(name, suffix)
sub := reToolsetDir.FindStringSubmatch(name)
if sub == nil {
continue
}
record(sub[1])
}
}
add(filepath.Join(baseUnix, "vc", "Auxiliary", "Build"), "Microsoft.VCToolsVersion.", ".default.props")
add(filepath.Join(baseUnix, "MSBuild", "Microsoft", "VC"), "", "")
for _, n := range wineenv.KnownPlatformToolsets {
record(n)
}
return suffixes
}
// reGlobalProp matches an MSBuild global-property command-line switch
// ("/p:Name=...", "-property:Name=...", case-insensitive on both the
// -p/-property spelling and the property name) so msbuildGlobalArgs can tell
// whether the caller already pinned a given property themselves.
func reGlobalProp(name string) *regexp.Regexp {
return regexp.MustCompile(`(?i)^[-/](p|property):` + regexp.QuoteMeta(name) + `=`)
}
// msbuildGlobalArgs returns /p: switches to prepend to an MSBuild invocation,
// one per forced property not already present in args.
//
// WindowsTargetPlatformVersion is the one property that needs this rather
// than an env var: unlike VCInstallDir_<N> (an input to a props-file
// *lookup*, so any value msbuildEnv sets is visible no matter what a project
// pins its PlatformToolset to), WindowsTargetPlatformVersion is itself the
// value most legacy .vcxproj files hardcode directly in a PropertyGroup -
// and an explicit PropertyGroup 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, which is exactly what's
// needed here: vintner only ever installs one Windows SDK version, so - same
// reasoning as the PlatformToolset fallback above - any project should
// transparently build against that one installed version rather than fail
// outright over an exact version string it happened to be pinned to when
// last saved from a real Windows SDK selector dropdown.
func msbuildGlobalArgs(cfg *wineenv.Config, args []string) []string {
forced := map[string]string{
"WindowsTargetPlatformVersion": cfg.SDKVer,
}
var out []string
for name, value := range forced {
re := reGlobalProp(name)
alreadySet := false
for _, a := range args {
if re.MatchString(a) {
alreadySet = true
break
}
}
if !alreadySet {
out = append(out, "/p:"+name+"="+value)
}
}
return out
}
func msbuildPlatform(arch string) string {
switch arch {
case "x86":
+85 -2
View File
@@ -39,6 +39,14 @@ func TestMsbuildEnvBasics(t *testing.T) {
if env["DisableRegistryUse"] != "true" {
t.Errorf(`env["DisableRegistryUse"] = %q, want "true"`, env["DisableRegistryUse"])
}
if env["CheckMSVCComponents"] != "false" {
t.Errorf(`env["CheckMSVCComponents"] = %q, want "false" (else CheckVCToolsetVersion errors on an aliased PlatformToolset)`, env["CheckMSVCComponents"])
}
// VCToolsVersion must be a real version string (see msbuildEnv's doc
// comment on it: leaving it unset makes Microsoft.Cpp.VCTools.props
// substitute a placeholder that then breaks unconditional version
// comparisons elsewhere). CheckMSVCComponents=false is what keeps this
// safe to combine with an aliased PlatformToolset.
if env["VCToolsVersion"] != cfg.MSVCVer {
t.Errorf(`env["VCToolsVersion"] = %q, want %q`, env["VCToolsVersion"], cfg.MSVCVer)
}
@@ -61,7 +69,24 @@ 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"} {
// Real layout has two independent sources feeding VCInstallDir_<N>/
// VCToolsInstallDir_<N> (see toolsetSuffixes' doc comment for why both
// are needed): the PlatformToolset short names a downloaded compiler
// ships default-props for, and MSBuild's own fixed schema-version dirs.
buildDir := filepath.Join(base, "vc", "Auxiliary", "Build")
if err := os.MkdirAll(buildDir, 0o755); err != nil {
t.Fatal(err)
}
for _, name := range []string{
"Microsoft.VCToolsVersion.v145.default.props",
"Microsoft.VCToolsVersion.v143.default.props",
"Microsoft.VCToolsVersion.default.props", // no version suffix - must not match
} {
if err := os.WriteFile(filepath.Join(buildDir, name), nil, 0o644); err != nil {
t.Fatal(err)
}
}
for _, v := range []string{"v180", "not-a-version"} {
if err := os.MkdirAll(filepath.Join(base, "MSBuild", "Microsoft", "VC", v), 0o755); err != nil {
t.Fatal(err)
}
@@ -69,7 +94,7 @@ func TestMsbuildEnvDiscoversEveryToolsetVersion(t *testing.T) {
env := msbuildEnv(cfg, paths)
for _, n := range []string{"145", "180"} {
for _, n := range []string{"145", "143", "180"} {
if _, ok := env["VCInstallDir_"+n]; !ok {
t.Errorf("expected VCInstallDir_%s to be set", n)
}
@@ -82,6 +107,64 @@ func TestMsbuildEnvDiscoversEveryToolsetVersion(t *testing.T) {
}
}
func TestToolsetSuffixesDedupsOverlap(t *testing.T) {
base := t.TempDir()
buildDir := filepath.Join(base, "vc", "Auxiliary", "Build")
if err := os.MkdirAll(buildDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(buildDir, "Microsoft.VCToolsVersion.v180.default.props"), nil, 0o644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(base, "MSBuild", "Microsoft", "VC", "v180"), 0o755); err != nil {
t.Fatal(err)
}
got := toolsetSuffixes(base)
count := 0
for _, n := range got {
if n == "180" {
count++
}
}
if count != 1 {
t.Errorf("toolsetSuffixes() returned %q with %d entries for \"180\" (from both sources), want exactly 1", got, count)
}
}
func TestMsbuildGlobalArgsForcesWindowsTargetPlatformVersion(t *testing.T) {
cfg := &wineenv.Config{SDKVer: "10.0.26100.0"}
got := msbuildGlobalArgs(cfg, []string{"Foo.sln", "/p:Configuration=Release"})
want := "/p:WindowsTargetPlatformVersion=10.0.26100.0"
found := false
for _, a := range got {
if a == want {
found = true
}
}
if !found {
t.Errorf("msbuildGlobalArgs(...) = %v, want it to contain %q", got, want)
}
}
func TestMsbuildGlobalArgsRespectsExplicitOverride(t *testing.T) {
cfg := &wineenv.Config{SDKVer: "10.0.26100.0"}
for _, explicit := range []string{
"/p:WindowsTargetPlatformVersion=10.0.19041.0",
"-p:WindowsTargetPlatformVersion=10.0.19041.0",
"/property:WindowsTargetPlatformVersion=10.0.19041.0",
} {
got := msbuildGlobalArgs(cfg, []string{"Foo.sln", explicit})
for _, a := range got {
if reGlobalProp("WindowsTargetPlatformVersion").MatchString(a) {
t.Errorf("msbuildGlobalArgs with explicit %q also injected %q - should have left the caller's value alone", explicit, a)
}
}
}
}
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)
+4 -2
View File
@@ -83,8 +83,10 @@ func Run(tool string, args []string) int {
// MSBuild: skip all filtering/toolrelay (its output is meant to be
// read as-is), and add the extra environment MSBuild's own
// toolset/SDK-detection props need on top of the generic
// INCLUDE/LIB/WINEPATH.
cmd := exec.Command(wineBin, append([]string{toolExePath}, rewritten...)...)
// INCLUDE/LIB/WINEPATH, plus any global properties a project file
// itself could otherwise override (see msbuildGlobalArgs).
msArgs := append(msbuildGlobalArgs(cfg, rewritten), rewritten...)
cmd := exec.Command(wineBin, append([]string{toolExePath}, msArgs...)...)
env := buildEnv(paths)
for k, v := range msbuildEnv(cfg, paths) {
env = append(env, k+"="+v)