# MSVC & Windows metadata

> Resolve, set up and cache Visual Studio, MSVC and the Windows SDK without vswhere or vcvarsall, cross-compile for Windows from Linux and macOS, and generate Rust or bun:ffi bindings from .winmd metadata

Bun resolves the native Windows toolchain the same way it ships `bun uv` for Python: in the process, with no helper executable. `bun msvc` finds every Visual Studio and Build Tools instance without `vswhere.exe`. It resolves the MSVC toolsets, the Windows SDKs, the Universal CRT, the .NET Framework SDK, LLVM and the developer scripts. It computes what `vcvarsall.bat` would set without running it, caches that environment, and installs or repairs what is missing. `bun winmd` links [windows-bindgen](https://github.com/microsoft/windows-rs) and generates bindings from Windows metadata.

On Linux and macOS, `bun msvc` sets up a [cross-compilation sysroot](#cross-compilation-from-linux-and-macos) instead. `bun winmd` is Windows-only. The discovery code comes from [find-msvc-tools](https://github.com/rust-lang/cc-rs) (MIT/Apache-2.0) and lives in `vendor/find-msvc-tools`; windows-bindgen (MIT/Apache-2.0) lives in `vendor/windows-rs`.

---

## `bun msvc`

```sh terminal icon="terminal"
bun msvc                         # doctor: instances, toolsets, cl/link/lib, the SDK, the UCRT, the MSI cache
bun msvc list                    # every instance with its toolsets, every Windows SDK and .NET Framework SDK
bun msvc info --toolset 14.44    # everything as JSON, for the 14.44 toolset
bun msvc which cl clang-cl       # C:\...\VC\Tools\MSVC\14.44.35207\bin\Hostx64\x64\cl.exe
bun msvc env --format pwsh       # the vcvarsall environment
bun msvc exec -- cl /nologo main.c
bun msvc sync                    # caches env.json, env.cmd, env.ps1, env.sh until the toolchain changes
bun msvc setup --dry-run         # what the Visual Studio Installer (or winget) would run
bun msvc msi                     # orphaned MSI products that make the installer fail (1714, 1612)
```

| Command                    | Output                                                                                                                   |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `doctor` (default)         | One line per component. The exit code is `1` when `cl`, `link`, `lib`, `rc`, the SDK or the UCRT is missing              |
| `info`                     | JSON: `error`, `instances`, `sdks`, `netfxSdks`, `instance`, `msvc`, `sdk`, `ucrt`, `netfx`, `llvm`, `scripts`, `tools`, `env`, `paths` |
| `list [--json]`            | Instances (sources, toolsets with their aliases and host/target pairs, LLVM), Windows SDKs, .NET Framework SDKs          |
| `env [--format <f>]`       | `json` (default, also `--json`), `pwsh`, `cmd`, `sh` (MSYS2/Git Bash, `PATH` in `/c/...` form), `github`                |
| `which <tool>...`          | Absolute path of `cl`, `link`, `lib`, `dumpbin`, `ml64`, `rc`, `midl`, `mt`, `signtool`, `clang-cl`, `cmake`, `ninja`, ... |
| `exec [--] <tool> [args]`  | Runs the tool with the computed environment and exits with its exit code                                                 |
| `sync [--check] [--force]` | Writes the environment cache and prints its directory. `--check` exits with `1` when the cache is missing or stale       |
| `setup`                    | Installs or completes the C++ tools, the Windows SDK and the requested toolset                                           |
| `msi [--fix]`              | Lists the Visual Studio and Windows SDK MSI products whose cached package is gone, and removes their registration        |

### Resolution

`bun msvc` merges instances by path from five sources:

- the Setup Configuration COM API
- the installer records, `%ProgramData%\Microsoft\VisualStudio\Packages\_Instances\*\state.json`
- the `SOFTWARE\Microsoft\VisualStudio\SxS\VS7` registry key
- `VSINSTALLDIR` and `VCToolsInstallDir`
- the `Microsoft Visual Studio\<year>\<product>` folders of both Program Files folders

This covers Visual Studio 2017 to 2026 (18.x) in every edition (Community, Professional, Enterprise, Build Tools, Preview), wherever it is installed. `bun msvc` never runs `vswhere.exe` or a `.bat` file, so `NoDefaultCurrentDirectoryInExePath` and a broken `VsDevCmd.bat` have no effect on it.

| Option           | Selects                                                                                                         |
| ---------------- | --------------------------------------------------------------------------------------------------------------- |
| `--arch <arch>`  | The target: `x64`, `x86`, `arm64`, `arm64ec` or `arm`. `x86_64`, `amd64` and `aarch64` work too. Default: the host |
| `--host <arch>`  | The host architecture of the tools. Default: native, then the x64 tools under emulation, then x86              |
| `--instance <q>` | An instance by id, path, product (`BuildTools`), year (`2026`) or version prefix (`17.14`). Default: the newest  |
| `--toolset <v>`  | An MSVC toolset, exact (`14.44.35207`), by prefix (`14.44`) or by alias (`v143`, `14.44.17.14`). Also `--vcvars-ver` |
| `--sdk <v>`      | A Windows SDK by version or prefix (`10.0.26100`). Default: the newest complete one                              |
| `--spectre`      | The Spectre-mitigated libraries (`-vcvars_spectre_libs=spectre`)                                                |

Without `--toolset`, `VCToolsVersion` decides, then the instance's `Microsoft.VCToolsVersion.default.txt`, as in `vcvarsall`. Without `--sdk`, `WindowsSDKVersion` decides, then the newest SDK that has `Windows.h` and the `um` libraries.

`env` prepends to `PATH`, `INCLUDE`, `EXTERNAL_INCLUDE`, `LIB` and `LIBPATH` in the order `vcvarsall.bat` uses. That covers the .NET Framework SDK, MSBuild, Roslyn and the `Common7` folders. CMake and Ninja are appended to `PATH`. `env` also sets the variables `vcvarsall.bat` sets:

- `VSINSTALLDIR`, `VCINSTALLDIR`, `VCToolsInstallDir`, `VCToolsVersion`, `VCToolsRedistDir`, `VisualStudioVersion` and `VS180COMNTOOLS` (named after the instance's version)
- `VSCMD_*`, `DevEnvDir` and `Platform`
- `Framework*` and `NETFXSDKDir`
- `WindowsSdk*`, `WindowsLibPath`, `UniversalCRTSdkDir` and `UCRTVersion`

In GitHub Actions, `--format github` appends to `$GITHUB_ENV`:

```yaml
- run: bun msvc env --arch x64 --format github >> $env:GITHUB_ENV
```

### `sync`

`bun msvc sync` writes the environment to `%LOCALAPPDATA%\bun\msvc\<key>`. `--cache-dir` changes the root. The key is the target followed by the options given: `x64`, `x64-toolset-14.44`, `arm64-host-x64-sdk-10.0.26100`. The directory holds:

- `env.json`: the variables it sets (`set`), the lists it prepends and appends (`prepend`, `append`), and a `fingerprint`.
- `env.cmd` for `call` in cmd.exe, `env.ps1` for `.` in PowerShell, and `env.sh` for `.` in Git Bash or MSYS2.

The fingerprint records when these last changed: the installer records, the instance's `VC\Tools\MSVC` and `VC\Auxiliary\Build` folders, and the SDK's `Include` and `Lib` folders. A later `sync` rewrites the cache only when one of them changed, or with `--force`.

`scripts/build.ts` reads this cache to build Bun on Windows. When the bun running it has no `msvc` command, it runs the same code with `cargo run --bin bun-msvc` from `vendor/find-msvc-tools`.

### `setup`

```sh terminal icon="terminal"
bun msvc setup                                 # the C++ tools for the host, and a Windows SDK if none is complete
bun msvc setup --toolset 14.44 --arch arm64    # adds Microsoft.VisualStudio.Component.VC.14.44.17.14.ARM64
bun msvc setup --sdk 10.0.22621                # adds Microsoft.VisualStudio.Component.Windows11SDK.22621
bun msvc setup --add Microsoft.VisualStudio.Component.VC.Llvm.Clang
bun msvc setup --repair                        # setup.exe repair --installPath ... --quiet --norestart --force
```

When an instance exists, `setup` runs the Visual Studio Installer (`setup.exe modify --add ... --quiet --norestart`) for the components that instance lacks. It runs `resume` when the instance's last install did not finish. `--update` updates the instance to the newest release of its channel.

When no instance exists, `setup` installs Build Tools with `winget`. `--product community`, `professional` or `enterprise` installs the IDE instead.

The installer asks for elevation through UAC. Exit codes `3010` and `1641` mean the install succeeded and Windows must restart. `--dry-run` prints the commands without running them. After a successful run, `setup` rewrites the `sync` cache.

### Orphaned MSI products

When `C:\Windows\Installer` no longer holds the cached `.msi` of a product that the registry still lists, Windows Installer can neither repair nor remove that product. The Visual Studio Installer then fails with error 1714 or 1612.

`bun msvc msi` lists those products in the Visual C++, Windows SDK, .NET and Visual Studio families. `doctor` and `setup` warn about them.

`bun msvc msi --fix` removes their registration, so the installers install them again from scratch. `setup --fix-msi-orphans` does the same before it runs the installer. Every key it deletes is first exported with `reg.exe export` to `%LOCALAPPDATA%\bun\msvc\msi-backup\<time>`. `--backup-dir` changes that folder. When the process is not elevated, it asks for elevation.

A binary named or linked as `msvc.exe` behaves like `bun msvc`. So does `bun-msvc.exe`, built from `vendor/find-msvc-tools`.

---

## Cross-compilation from Linux and macOS

On Linux and macOS, `bun msvc` downloads the MSVC CRT and the Windows SDK. It then exports the environment that cargo, rustc and the `cc` crate need to build for `x86_64-pc-windows-msvc`, `aarch64-pc-windows-msvc` and `i686-pc-windows-msvc`. Code is compiled with `clang-cl`, archived with `llvm-lib` and linked with `lld-link`. On Windows, the same commands are under `bun msvc cross`.

```sh terminal icon="terminal"
bun msvc setup --accept-license          # MSVC CRT + Windows SDK for x64 and arm64
eval "$(bun msvc env)"                   # CC_*, CXX_*, AR_*, CFLAGS_*, CARGO_TARGET_*_LINKER, CARGO_TARGET_*_RUSTFLAGS
rustup target add x86_64-pc-windows-msvc
cargo build --target x86_64-pc-windows-msvc
```

`setup` reads the Visual Studio release channel (`https://aka.ms/vs/17/release/channel`) and its installer manifest, the same files the Visual Studio Installer reads. It picks the newest CRT and SDK unless `--toolset` or `--sdk` names another. It downloads these packages and checks each one against the SHA-256 in the manifest:

- the CRT headers, and the `Desktop` and `Store` CRT libraries of each architecture (VSIX archives)
- the Windows SDK installers that hold headers and import libraries, and the cabinets they reference (MSI and CAB)

Only the headers and the libraries of the requested architectures are extracted. Debug symbols, tools, sources and redistributables are skipped. The layout matches a Visual Studio install, so `clang-cl /winsysroot` also accepts it:

```txt
~/.cache/bun/msvc/sysroot/14.44.35220-10.0.26100/
├── VC/Tools/MSVC/14.44.35207/{include,lib/x64,lib/arm64}
├── Windows Kits/10/Include/10.0.26100.0/{ucrt,um,shared,winrt,cppwinrt}
├── Windows Kits/10/Lib/10.0.26100.0/{um,ucrt}/{x64,arm64}
├── crt -> VC/Tools/MSVC/14.44.35207
├── sdk -> Windows Kits/10
└── sysroot.json, env.sh, env.json
```

Windows file names are case-insensitive, but Linux file names are not. So `setup` adds symbolic links:

- a lower-case name for every file, plus `name.lib` and `NAME.lib` for every library (`kernel32.lib` and `KERNEL32.lib` point to `kernel32.Lib`)
- every `#include` spelling found in the headers that differs from the file on disk only in case (`WinError.h`, `GL/gl.h`)

The `crt` and `sdk` links keep spaces out of the flags, because cargo and `cc` split `CFLAGS` and `RUSTFLAGS` on spaces.

| Command                   | Output                                                                                                            |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `doctor` (default)        | The sysroot, `clang-cl`, `lld-link`, `llvm-lib`, the optional LLVM tools, and the installed rustup targets        |
| `setup [--dry-run]`       | Downloads and lays out the sysroot, then prints its directory. `--dry-run` lists the downloads instead            |
| `sync [--check]`          | Sets up the sysroot when it is missing and rewrites `env.sh` and `env.json`. `--check` exits with `1` instead     |
| `env [--shell <f>]`       | `sh` (default), `json`, `pwsh`, `cmd` or `github`. `--format` is an alias                                         |
| `which <tool>...`         | Path of `clang-cl`, `lld-link`, `llvm-lib`, `llvm-rc`, `llvm-mt` or `llvm-dlltool`                                |
| `list [--json]`, `info`   | The installed sysroots; everything as JSON                                                                        |

| Option                    | Meaning                                                                                                           |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `--arch <arch>`           | `x64`, `arm64` or `x86`, repeated or comma-separated. Default: `x64,arm64`                                        |
| `--toolset <v>`           | A CRT version or prefix: `14.44`, `14.44.17.14` or `14.44.35220`                                                  |
| `--sdk <v>`               | A Windows SDK version or prefix: `10.0.26100`                                                                     |
| `--spectre`               | Also the Spectre-mitigated CRT libraries                                                                          |
| `--manifest <url\|path>`  | Another channel or installer manifest, such as a local mirror. Relative payload URLs resolve against it           |
| `--cache-dir <dir>`       | Default: `$XDG_CACHE_HOME/bun/msvc` (`~/.cache/bun/msvc`), or `%LOCALAPPDATA%\bun\msvc\cross` on Windows          |
| `--accept-license`        | Required by `setup` and `sync`, as is `BUN_MSVC_ACCEPT_LICENSE=1`. Microsoft licenses the CRT and the SDK         |

For each architecture of the sysroot, `env` sets these variables, where `T` is the Rust target triple:

| Variable                                         | Value                                                                                   |
| ------------------------------------------------ | --------------------------------------------------------------------------------------- |
| `CC_T`, `CXX_T` (`CC_x86_64_pc_windows_msvc`)    | `clang-cl`                                                                              |
| `AR_T`                                           | `llvm-lib`                                                                              |
| `CFLAGS_T`, `CXXFLAGS_T`                         | `--target=T`, and `/imsvc` for the CRT and the SDK `ucrt`, `um`, `shared`, `winrt`, `cppwinrt` |
| `CARGO_TARGET_T_LINKER`                          | `lld-link`                                                                              |
| `CARGO_TARGET_T_RUSTFLAGS`                       | `-Lnative=` for the CRT, SDK `um` and SDK `ucrt` libraries                              |

It also sets `BUN_MSVC_SYSROOT`, `BUN_MSVC_VERSION` and `BUN_MSVC_SDK_VERSION`. The LLVM tools are looked up on `PATH`, then in the newest `/usr/lib/llvm-<n>/bin` (Debian, Ubuntu) or `/usr/lib/llvm<n>/bin` (Alpine), then as `<tool>-<n>` on `PATH`. Install them with `apt install clang lld llvm` or `apk add clang lld llvm`.

Downloads go through Bun's HTTP client, so `HTTPS_PROXY` and `NODE_TLS_REJECT_UNAUTHORIZED` apply. They are kept in `<cache>/downloads`, so a later `setup` for another architecture downloads only what it lacks. The `bun-msvc` binary of `vendor/find-msvc-tools` has the same commands, but it only downloads from paths, `file://` URLs and `http://` URLs.

---

## `toolchain()` in `bun:windows`

```ts
import windows from "bun:windows";

const tc = windows.toolchain({ arch: "x64", toolset: "14.44" });
tc.instance?.version; // "18.10.12217.157"
tc.msvc?.version; // "14.44.35207"
tc.sdk?.version; // "10.0.26100.0"
tc.sdk?.windowsWinmd; // "C:\\Program Files (x86)\\Windows Kits\\10\\UnionMetadata\\10.0.26100.0\\Windows.winmd"

Bun.spawnSync([tc.tools.cl!, "/nologo", "main.c"], { env: { ...process.env, ...tc.env } });
```

The options are those of `bun msvc info`: `arch`, `toolset`, `sdk` and `instance`. The result has the same shape. Missing parts are `null`, `error` says why the toolchain could not be resolved, and the object is frozen. An unknown `arch` throws.

---

## `bun winmd`

```sh terminal icon="terminal"
bun winmd --out src/bindings.rs --filter GetTickCount MessageBoxW --sys --flat   # Rust (windows-sys style)
bun winmd --out kernel32.ts --filter GetCurrentProcessId GetTickCount            # bun:ffi module
bun winmd --out user32.json --in win32 --filter Windows.Win32.UI.WindowsAndMessaging --dll user32.dll
bun winmd inputs                                                                 # where the metadata comes from
```

`--lang rust|ts|json` follows the extension of `--out`: `.ts`, `.mts` or `.js` give `ts`, `.json` gives `json`, and any other extension gives `rust`. The filter grammar is the one windows-bindgen uses. It accepts namespaces, types, functions, `Type::Member`, and `!` to exclude.

| Input                  | Metadata                                                                                                    |
| ---------------------- | ----------------------------------------------------------------------------------------------------------- |
| none / `--in default`  | `Windows.Win32.winmd` (flattened `Windows.Win32` namespace) and `Windows.winmd` from windows-rs, embedded   |
| `--in win32`           | The `Microsoft.Windows.SDK.Win32Metadata` NuGet package: full namespaces, enums, `SetLastError`             |
| `--in sdk`             | `UnionMetadata\<version>\Windows.winmd` of the installed Windows SDK (WinRT)                                |
| `--in <file or dir>`   | Any `.winmd` file, or a directory of them                                                                    |

Rust-only options are passed to windows-bindgen: `--sys`, `--extern`, `--flat`, `--package`, `--minimal`, `--derive`, `--implement`, `--compose`, `--dead-code` and `--rustfmt`. The output is formatted when `rustfmt` is on `PATH`.

For `ts` and `json` output, `--arch x64|arm64|x86` selects the struct layouts and the pointer width. The default is the host. `--dll <name>` keeps only the functions imported from those DLLs.

The generated TypeScript module exports `signatures`, `symbols` (in `FFIType` form, per DLL), `structs` (size, alignment and field offsets), `enums`, `constants` (64-bit values are `bigint`), `wideAliases` (`MessageBox` maps to `MessageBoxW`) and `open(dll)`:

```ts
import { open } from "./kernel32.ts";

const kernel32 = open("kernel32.dll");
kernel32.symbols.GetCurrentProcessId() === process.pid; // true
```

A binary named or linked as `winmd.exe` behaves like `bun winmd`.
