diff --git a/README.md b/README.md index ab90067..b6d37b7 100644 --- a/README.md +++ b/README.md @@ -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 `/DXSDK`. + +```bash +vintner download --accept-license --with-dxsdk +``` + +Point your project's `IncludePath`/`LibraryPath` at +`/DXSDK/Include` and `/DXSDK/Lib/x86` or `/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 diff --git a/cmd/vintner/download.go b/cmd/vintner/download.go index 0985d10..d25649b 100644 --- a/cmd/vintner/download.go +++ b/cmd/vintner/download.go @@ -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 { diff --git a/internal/download/dxsdk.go b/internal/download/dxsdk.go new file mode 100644 index 0000000..4e7a85f --- /dev/null +++ b/internal/download/dxsdk.go @@ -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() +} diff --git a/internal/download/dxsdk_test.go b/internal/download/dxsdk_test.go new file mode 100644 index 0000000..c10af43 --- /dev/null +++ b/internal/download/dxsdk_test.go @@ -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) + } + } +} diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go index 48dd277..37544d2 100644 --- a/internal/i18n/i18n.go +++ b/internal/i18n/i18n.go @@ -77,8 +77,8 @@ Usage: vintner completion bash|zsh print a shell completion script Run "vintner --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 /bin/ 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 /bin/ 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",