From 8551d7fe993aed6aa462d2b04036c3b1278bfd4e Mon Sep 17 00:00:00 2001 From: Cheviiot <153805936+Cheviiot@users.noreply.github.com> Date: Sat, 25 Jul 2026 03:24:44 +1000 Subject: [PATCH] Add Windows Driver Kit (WDK) support for building KMDF/UMDF drivers download --with-wdk fetches the WDK headers/libs/host-tools NuGet packages (nuget.org has no vsman-manifest entry for this content) and lays them out where the DriverKit.BuildTools PlatformToolset expects them. msbuildEnv wires WDKContentRoot/WDKBuildFolder through DisableRegistryUse the same way the SDK/toolset paths already are. Two WDK-package fixups were needed for a real driver to actually build under Wine: the bundled build-task assembly is versioned for an older VisualStudioVersion than ours, and the package ships no x86 host-tools directory at all (only x64/arm64), which StampInf hardcodes a path to. Also force TZ=UTC for msbuild invocations: StampInf stamps DriverVer using the local wall-clock date while Inf2Cat validates it against UTC "now", so any timezone east of UTC sees a "postdated DriverVer" failure for most of the day. Verified against a real KMDF sample driver (microsoft/Windows-driver- samples' echo_2): compiles, links, INF stamps and passes Inf2Cat's signability check with SignMode=off. --- cmd/msvc-go-wine/download.go | 50 +++++++ internal/download/select.go | 12 +- internal/download/wdk.go | 264 +++++++++++++++++++++++++++++++++ internal/wrapper/msbuildenv.go | 24 ++- 4 files changed, 347 insertions(+), 3 deletions(-) create mode 100644 internal/download/wdk.go diff --git a/cmd/msvc-go-wine/download.go b/cmd/msvc-go-wine/download.go index d17570a..84f04c2 100644 --- a/cmd/msvc-go-wine/download.go +++ b/cmd/msvc-go-wine/download.go @@ -33,6 +33,7 @@ func runDownload(args []string) int { listWorkloads := fs.Bool("list-workloads", false, "list available workloads from the manifest and exit, without downloading anything") 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)") var archsFlag stringList fs.Var(&archsFlag, "architecture", "target architecture to include (x86, x64, arm, arm64, host); repeatable") var ignoreFlag stringList @@ -54,6 +55,7 @@ func runDownload(args []string) int { IncludeOptional: *includeOptional, SkipRecommended: *skipRecommended, Language: *language, + WithWDK: *withWDK, } manifestURL := *manifestFile @@ -198,10 +200,58 @@ func runDownload(args []string) int { } } + if opts.WithWDK && !*onlyUnpack { + if err := downloadWDK(opts, selected, cache, destAbs, *major); err != nil { + fmt.Fprintln(os.Stderr, "msvc-go-wine download:", err) + return 1 + } + } + fmt.Println("Done. Next: msvc-go-wine install", destAbs) return 0 } +// downloadWDK fetches the WDK NuGet package(s) matching opts.Architecture +// (only x64 and arm64 have one - there's no WDK package for x86/arm +// targets) into destAbs/wdk/, preferring a version matching the +// Windows SDK actually selected. See wdk.go for why this is a separate +// download path from the rest of ExpandSelection/FetchPayloads/Unpack. +func downloadWDK(opts *download.Options, selected []*download.Package, cache, destAbs string, major int) error { + sdkBuild := download.SDKBuildPrefix(selected) + vsVersion := fmt.Sprintf("%d.0", major) + var archs []string + for _, a := range []string{"x64", "arm64"} { + if contains(opts.Architecture, a) { + archs = append(archs, a) + } + } + if len(archs) == 0 { + fmt.Println("--with-wdk: no x64/arm64 target architecture selected, skipping (no WDK package exists for x86/arm)") + return nil + } + for _, arch := range archs { + version, err := download.FetchLatestWDKVersion(arch, sdkBuild) + if err != nil { + return err + } + wdkDir, err := download.DownloadWDK(arch, version, cache, destAbs, vsVersion) + if err != nil { + return err + } + fmt.Printf("Installed WDK (%s) %s at %s\n", arch, version, wdkDir) + } + return nil +} + +func contains(list []string, v string) bool { + for _, s := range list { + if s == v { + return true + } + } + return false +} + // printPackageList prints one line per package: its ID, and (when the // manifest carries one) its human-readable title in the requested language. func printPackageList(kind string, pkgs []*download.Package, language string) { diff --git a/internal/download/select.go b/internal/download/select.go index 0c218b1..32672b8 100644 --- a/internal/download/select.go +++ b/internal/download/select.go @@ -43,7 +43,15 @@ type Options struct { SkipRecommended bool Language string - WithWDKInstallers string + // WithWDK selects the PlatformToolset registration + // (Component.Microsoft.Windows.DriverKit.BuildTools) that lets MSBuild + // recognize WindowsKernelModeDriver10.0/WindowsUserModeDriver10.0 + // PlatformToolsets. It's not part of the manifest-driven default + // selection - unlike everything else it depends on, it's opt-in via + // --with-wdk, since the actual driver headers/libs come from a separate + // NuGet package download handled outside ExpandSelection entirely (see + // wdk.go and cmd/msvc-go-wine's runDownload). + WithWDK bool } func addIfWanted(opts *Options, flag TriState, pkg string) { @@ -242,7 +250,7 @@ func ResolveSelection(opts *Options, idx Index) error { addIfWanted(opts, opts.WithDevCmd, "Microsoft.VisualStudio.VC.vcvars") addIfWanted(opts, opts.WithDevCmd, "Microsoft.VisualStudio.PackageGroup.VsDevCmd") - if opts.WithWDKInstallers != "" { + if opts.WithWDK { opts.Package = append(opts.Package, "Component.Microsoft.Windows.DriverKit.BuildTools") } diff --git a/internal/download/wdk.go b/internal/download/wdk.go new file mode 100644 index 0000000..69b965d --- /dev/null +++ b/internal/download/wdk.go @@ -0,0 +1,264 @@ +package download + +import ( + "archive/zip" + "encoding/json" + "fmt" + "io/fs" + "net/url" + "os" + "path/filepath" + "regexp" + "strings" +) + +// The actual driver headers/libs (ntddk.h, wdf, km/um import libs) aren't +// part of the VS installer manifest at all - Component.Microsoft.Windows. +// DriverKit.BuildTools (see select.go) only registers the +// WindowsKernelModeDriver10.0/WindowsUserModeDriver10.0 PlatformToolsets in +// the MSBuild tree. The content those toolsets actually need ships as a +// separate per-architecture NuGet package, independently of the vsman +// pipeline: https://www.nuget.org/packages/Microsoft.Windows.WDK.x64 (and +// .ARM64). This file fetches and unpacks that package directly through the +// NuGet v3 flat-container API, without needing nuget.exe or a project +// restore. + +const nugetFlatContainer = "https://api.nuget.org/v3-flatcontainer" + +// WDKNuGetID returns the nuget.org package id providing WDK content for +// arch ("x86"/"x64" both use the x64 package - there's no 32-bit WDK +// package - "arm64" uses the ARM64 one). +func WDKNuGetID(arch string) string { + if arch == "arm64" { + return "Microsoft.Windows.WDK.ARM64" + } + return "Microsoft.Windows.WDK.x64" +} + +// nugetVersionIndex is the "versions" list nuget.org's flat-container index +// returns, oldest first. +type nugetVersionIndex struct { + Versions []string `json:"versions"` +} + +var reNuGetPrerelease = regexp.MustCompile(`-`) + +// FetchLatestWDKVersion returns the newest stable (non-prerelease) version +// of arch's WDK NuGet package, preferring one whose version starts with +// sdkBuild (e.g. "10.0.26100", to match an already-selected Windows SDK) if +// any such version exists; otherwise it returns the newest stable version +// overall. +func FetchLatestWDKVersion(arch, sdkBuild string) (string, error) { + id := strings.ToLower(WDKNuGetID(arch)) + data, err := httpGet(nugetFlatContainer + "/" + id + "/index.json") + if err != nil { + return "", fmt.Errorf("listing %s versions: %w", WDKNuGetID(arch), err) + } + var idx nugetVersionIndex + if err := json.Unmarshal(data, &idx); err != nil { + return "", fmt.Errorf("parsing %s version index: %w", WDKNuGetID(arch), err) + } + + var latestMatching, latestAny string + for _, v := range idx.Versions { + if reNuGetPrerelease.MatchString(v) { + continue + } + latestAny = v + if sdkBuild != "" && strings.HasPrefix(v, sdkBuild) { + latestMatching = v + } + } + if latestMatching != "" { + return latestMatching, nil + } + if latestAny != "" { + return latestAny, nil + } + return "", fmt.Errorf("no stable release of %s found", WDKNuGetID(arch)) +} + +// SDKBuildPrefix extracts the "10.0.26100"-style build prefix from a +// selected Win10SDK/Win11SDK package's version (e.g. "10.0.26100.1742" -> +// "10.0.26100"), used to pick a matching WDK NuGet version. Returns "" if +// selected contains no such package. +func SDKBuildPrefix(selected []*Package) string { + for _, p := range selected { + id := strings.ToLower(p.ID) + if strings.HasPrefix(id, "win10sdk") || strings.HasPrefix(id, "win11sdk") { + parts := strings.SplitN(p.Version, ".", 4) + if len(parts) >= 3 { + return strings.Join(parts[:3], ".") + } + } + } + return "" +} + +// DownloadWDK fetches (or reuses a cached copy of) arch's WDK NuGet package +// at version, then unpacks its "c" directory - headers, libs, and the +// per-SDK-build MSBuild props/targets the DriverKit.BuildTools PlatformToolset +// registration imports via $(WDKContentRoot) - into destDir/wdk//c. +// Kept as its own self-contained tree rather than merged into the regular +// SDK dirs, matching how $(WDKContentRoot) is meant to be used. +// +// vsVersion is the installed MSBuild's own $(VisualStudioVersion) (e.g. +// "18.0"): the package's WindowsDriver.common.targets loads its custom +// build tasks from an assembly named for that property +// (Microsoft.DriverKit.Build.Tasks.$(VisualStudioVersion).dll), but the +// package only ships one built for whatever (older) VS generation it +// targeted - see duplicateVersionedBuildTaskAssemblies. +func DownloadWDK(arch, version, cacheDir, destDir, vsVersion string) (string, error) { + id := WDKNuGetID(arch) + idLower := strings.ToLower(id) + nupkgURL := fmt.Sprintf("%s/%s/%s/%s.%s.nupkg", nugetFlatContainer, idLower, version, idLower, version) + + cacheFile := filepath.Join(cacheDir, fmt.Sprintf("%s-%s.nupkg", idLower, version)) + if !isFile(cacheFile) { + fmt.Printf("Downloading %s %s\n", id, version) + if err := httpDownloadFile(nupkgURL, cacheFile); err != nil { + return "", fmt.Errorf("downloading %s %s: %w", id, version, err) + } + } else { + fmt.Printf("Using existing file %s\n", filepath.Base(cacheFile)) + } + + wdkDir := filepath.Join(destDir, "wdk", arch) + cDir := filepath.Join(wdkDir, "c") + if err := extractNuGetPackageDir(cacheFile, "c/", cDir); err != nil { + return "", fmt.Errorf("unpacking %s %s: %w", id, version, err) + } + if err := duplicateVersionedBuildTaskAssemblies(cDir, vsVersion); err != nil { + return "", fmt.Errorf("adapting %s %s to VisualStudioVersion %s: %w", id, version, vsVersion, err) + } + if err := fillMissingHostTools(cDir); err != nil { + return "", fmt.Errorf("filling in missing x86 host tools for %s %s: %w", id, version, err) + } + return wdkDir, nil +} + +// fillMissingHostTools copies every file from each cDir/bin//x64 +// directory into its sibling .../x86 directory, adding whatever's missing +// without overwriting anything already there. The WDK NuGet package's x86 +// host-tool directory only ships a handful of genuinely x86-specific files +// (Inf2Cat.exe among them); most driver build tools (stampinf.exe included) +// only ship as x64 binaries, but WindowsDriver.Common.targets hardcodes an +// x86 tool path (WDKBinRoot_x86) with no fallback. This isn't "faking" an +// x86 binary - it's just running the real x64 PE binaries from a directory +// MSBuild happens to call "x86"; Wine doesn't care what the folder is +// named, only the PE header when it execs the file, and we're always on an +// x64 host/Wine setup here. +func fillMissingHostTools(cDir string) error { + binDir := filepath.Join(cDir, "bin") + entries, err := os.ReadDir(binDir) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + for _, e := range entries { + if !e.IsDir() { + continue + } + x64Dir := filepath.Join(binDir, e.Name(), "x64") + if !isDir(x64Dir) { + continue + } + x86Dir := filepath.Join(binDir, e.Name(), "x86") + if err := os.MkdirAll(x86Dir, 0o755); err != nil { + return err + } + files, err := os.ReadDir(x64Dir) + if err != nil { + return err + } + for _, f := range files { + if f.IsDir() { + continue + } + dst := filepath.Join(x86Dir, f.Name()) + if isFile(dst) { + continue + } + if err := copyFile(filepath.Join(x64Dir, f.Name()), dst, 0o755); err != nil { + return err + } + } + } + return nil +} + +var reVersionedDLL = regexp.MustCompile(`^(.*)\.\d+\.\d+\.dll$`) + +// duplicateVersionedBuildTaskAssemblies copies every "*..dll" file +// under cDir/build (the driver build task assemblies, e.g. +// Microsoft.DriverKit.Build.Tasks.17.0.dll) to a sibling +// "*..dll" when one doesn't already exist. The WDK NuGet +// package bundles these named for whatever VS generation it was built +// against; our installed MSBuild may be newer and looks them up by its own +// $(VisualStudioVersion). The assemblies aren't VS-version-specific in +// behavior - only the file name encodes an expected caller - so serving the +// same bytes under the new name is safe. +func duplicateVersionedBuildTaskAssemblies(cDir, targetVSVersion string) error { + buildDir := filepath.Join(cDir, "build") + if !isDir(buildDir) { + return nil + } + return filepath.WalkDir(buildDir, func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + m := reVersionedDLL.FindStringSubmatch(d.Name()) + if m == nil || strings.HasSuffix(d.Name(), "."+targetVSVersion+".dll") { + return nil + } + target := filepath.Join(filepath.Dir(path), m[1]+"."+targetVSVersion+".dll") + if isFile(target) { + return nil + } + return copyFile(path, target, 0o644) + }) +} + +// extractNuGetPackageDir extracts every entry of nupkgFile whose name starts +// with prefix into dest, stripping prefix from each resulting path. +func extractNuGetPackageDir(nupkgFile, prefix, dest string) error { + r, err := zip.OpenReader(nupkgFile) + if err != nil { + return err + } + defer r.Close() + + if err := os.MkdirAll(dest, 0o755); err != nil { + return err + } + for _, f := range r.File { + name, err := url.PathUnescape(f.Name) + if err != nil { + name = f.Name + } + name = strings.ReplaceAll(name, `\`, "/") + if !strings.HasPrefix(name, prefix) { + continue + } + rel := strings.TrimPrefix(name, prefix) + if rel == "" { + continue + } + target := filepath.Join(dest, rel) + if f.FileInfo().IsDir() { + if err := os.MkdirAll(target, 0o755); err != nil { + return err + } + continue + } + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return err + } + if err := extractZipEntry(f, target); err != nil { + return err + } + } + return nil +} diff --git a/internal/wrapper/msbuildenv.go b/internal/wrapper/msbuildenv.go index 5c7cd0d..08c88a4 100644 --- a/internal/wrapper/msbuildenv.go +++ b/internal/wrapper/msbuildenv.go @@ -1,6 +1,7 @@ package wrapper import ( + "os" "path/filepath" "regexp" "strings" @@ -19,12 +20,20 @@ var reToolsetDir = regexp.MustCompile(`^v(\d+)$`) // Windows Registry. func msbuildEnv(cfg *wineenv.Config, paths *wineenv.Paths) map[string]string { env := map[string]string{ + // WDK driver builds stamp the INF's DriverVer with StampInf (which + // uses the local wall-clock date) and then validate it with + // Inf2Cat (which checks against UTC "now"). For any timezone east + // of UTC, local-vs-UTC disagree on the calendar date for most of + // the day, so Inf2Cat rejects the just-stamped date as "postdated" + // (MSB6006, "DriverVer set to a date in the future"). Forcing both + // tools onto the same UTC clock removes the mismatch. + "TZ": "UTC", + "DisableRegistryUse": "true", "VCToolsVersion": cfg.MSVCVer, "VsInstallRoot": paths.BaseWin + `\`, "VSInstallDir": paths.BaseWin + `\`, - "MicrosoftKitRoot": paths.BaseWin + `\`, "SDKReferenceDirectoryRoot": paths.BaseWin + `\`, "SDKExtensionDirectoryRoot": paths.BaseWin + `\`, "MSBUILDSDKREFERENCEDIRECTORY": paths.BaseWin + `\`, @@ -69,6 +78,19 @@ func msbuildEnv(cfg *wineenv.Config, paths *wineenv.Paths) map[string]string { env["PreferredToolArchitecture"] = "x64" } + // The WindowsKernelModeDriver10.0/WindowsUserModeDriver10.0 + // PlatformToolsets (registered by `download --with-wdk`, see + // internal/download/wdk.go) resolve WDKContentRoot through the + // (nonexistent, under Wine) registry unless it's already set - same + // DisableRegistryUse workaround as WindowsSdkDir_10 above. WDKBuildFolder + // picks the per-SDK-build subtree (c/build//...) the NuGet + // package's content is organized under. + wdkContentRoot := filepath.Join(paths.BaseUnix, "wdk", cfg.Arch, "c") + if fi, err := os.Stat(wdkContentRoot); err == nil && fi.IsDir() { + env["WDKContentRoot"] = wineenv.ToWinPath(wdkContentRoot) + `\` + env["WDKBuildFolder"] = cfg.SDKVer + } + return env }