AphrodyBun GitHub

Runtime docs · Runtime · Interop & Tooling

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 and generates bindings from Windows metadata.

On Linux and macOS, bun msvc sets up a cross-compilation sysroot instead. bun winmd is Windows-only. The discovery code comes from find-msvc-tools (MIT/Apache-2.0) and lives in vendor/find-msvc-tools; windows-bindgen (MIT/Apache-2.0) lives in vendor/windows-rs.


bun msvc

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)
CommandOutput
doctor (default)One line per component. The exit code is 1 when cl, link, lib, rc, the SDK or the UCRT is missing
infoJSON: 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
setupInstalls 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.

OptionSelects
--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
--spectreThe 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:

- 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

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.

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:

~/.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.

CommandOutput
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], infoThe installed sysroots; everything as JSON
OptionMeaning
--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
--spectreAlso 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-licenseRequired 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:

VariableValue
CC_T, CXX_T (CC_x86_64_pc_windows_msvc)clang-cl
AR_Tllvm-lib
CFLAGS_T, CXXFLAGS_T--target=T, and /imsvc for the CRT and the SDK ucrt, um, shared, winrt, cppwinrt
CARGO_TARGET_T_LINKERlld-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

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

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.

InputMetadata
none / --in defaultWindows.Win32.winmd (flattened Windows.Win32 namespace) and Windows.winmd from windows-rs, embedded
--in win32The Microsoft.Windows.SDK.Win32Metadata NuGet package: full namespaces, enums, SetLastError
--in sdkUnionMetadata\<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):

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.