diff --git a/.github/workflows/prebuild.yml b/.github/workflows/prebuild.yml index 6f4e45b7..c2ef73c1 100644 --- a/.github/workflows/prebuild.yml +++ b/.github/workflows/prebuild.yml @@ -71,6 +71,8 @@ jobs: arch: ${{ matrix.msvcArch }} - name: Build prebuild + env: + RUSTFLAGS: ${{ runner.os == 'Windows' && '-C target-feature=+crt-static' || '' }} run: npx napi build --platform --release --target "${{ matrix.rustTarget }}" --js false working-directory: packages/native diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b298842d..f09cd186 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -332,6 +332,8 @@ jobs: arch: ${{ matrix.msvcArch }} - name: Build prebuild + env: + RUSTFLAGS: ${{ runner.os == 'Windows' && '-C target-feature=+crt-static' || '' }} run: npx napi build --platform --release --target "${{ matrix.rustTarget }}" --js false working-directory: packages/native diff --git a/CHANGELOG.md b/CHANGELOG.md index 58106703..9c9e6827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ The format is based on Keep a Changelog and the project follows Semantic Version ### Fixed +- **native/windows**: Windows prebuilt native binaries now link the MSVC runtime + statically, so packaged apps can run on clean Windows installs without a + separate Visual C++ Redistributable install. - **core/renderer**: Per-side box borders now render standalone edges. A box with only `borderLeft` (or any vertical-only side combination) previously drew nothing when shorter than 2 rows; corner rows are now only required when a horizontal edge is present. Markdown blockquotes use this to render a GitHub-style dim left bar instead of a rounded box. ## [0.1.0-beta.3] - 2026-06-11 diff --git a/docs/backend/native.md b/docs/backend/native.md index 6bbd151e..276ae007 100644 --- a/docs/backend/native.md +++ b/docs/backend/native.md @@ -37,6 +37,8 @@ combinations: When you install `@rezi-ui/native`, the correct prebuilt binary is selected automatically based on `process.platform` and `process.arch`. No postinstall scripts are required -- the binary is included directly in the published package. +Windows prebuilds are linked with the static MSVC C runtime, so they do not +require users to install the Visual C++ Redistributable separately. Prebuilt binaries are produced by the `prebuild.yml` GitHub Actions workflow, which cross-compiles on CI for all target triples. diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index 41f77c2f..0dae5bac 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -62,6 +62,9 @@ No install-time source build runs for unsupported targets. If a matching prebuilt binary is not available, build from a repository checkout with `npm run build:native`. +Windows prebuilt binaries do not require a separate Visual C++ Redistributable +install. + ## Package Overview | Package | Description | Required | diff --git a/docs/packages/native.md b/docs/packages/native.md index 92ae6049..dcf0a406 100644 --- a/docs/packages/native.md +++ b/docs/packages/native.md @@ -5,6 +5,7 @@ Native addon: - Rust + `napi-rs` binding - vendored Zireael engine sources - prebuilt `.node` binaries for supported targets +- static MSVC runtime linking for Windows prebuilds ## Install diff --git a/packages/native/README.md b/packages/native/README.md index 2699e7b1..7820bf28 100644 --- a/packages/native/README.md +++ b/packages/native/README.md @@ -29,6 +29,8 @@ npm run check:native-vendor - Engine placement is controlled by `@rezi-ui/node` `executionMode` (`auto` | `worker` | `inline`). - `executionMode: "auto"` selects inline when `fpsCap <= 30`, worker otherwise. - All buffers across the boundary are caller-owned; binary formats are validated strictly. +- Windows builds link the MSVC C runtime statically so packaged apps do not need + a separate Visual C++ Redistributable install. - Native compilation reads `packages/native/vendor/zireael` (not `vendor/zireael`). - `packages/native/vendor/VENDOR_COMMIT.txt` must match the `vendor/zireael` gitlink commit. diff --git a/packages/native/build.rs b/packages/native/build.rs index c436496d..d122b6c1 100644 --- a/packages/native/build.rs +++ b/packages/native/build.rs @@ -14,6 +14,13 @@ fn main() { build.include(&src_dir); build.warnings(false); + let target_features = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or_default(); + if env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc") + && target_features.split(',').any(|feature| feature == "crt-static") + { + build.static_crt(true); + } + // The engine assumes a C99-or-newer compiler; C11 is required for atomics on MSVC. if build.get_compiler().is_like_msvc() { build.flag_if_supported("/std:c11"); diff --git a/packages/native/scripts/build-native.mjs b/packages/native/scripts/build-native.mjs index b1042437..a135fc0f 100644 --- a/packages/native/scripts/build-native.mjs +++ b/packages/native/scripts/build-native.mjs @@ -3,6 +3,8 @@ import { copyFileSync, existsSync, readdirSync, rmSync, writeFileSync } from "no import { tmpdir } from "node:os"; import { delimiter, join } from "node:path"; +const WINDOWS_STATIC_CRT_RUSTFLAGS = "-C target-feature=+crt-static"; + function splitPathList(value, delim) { return value .split(delim) @@ -338,6 +340,25 @@ function withRustToolchainOnPath(env) { return next; } +function isWindowsMsvcTarget(targetTriple) { + return typeof targetTriple === "string" && targetTriple.endsWith("-pc-windows-msvc"); +} + +function withWindowsStaticCrtEnv(env, hostTargetTriple) { + if (process.platform !== "win32" || !isWindowsMsvcTarget(hostTargetTriple)) return env; + + const current = typeof env.RUSTFLAGS === "string" ? env.RUSTFLAGS.trim() : ""; + if (/(^|\s)target-feature=\+crt-static(\s|$)/.test(current)) return env; + if (/(^|\s)-Ctarget-feature=\+crt-static(\s|$)/.test(current)) return env; + + const next = { ...env }; + next.RUSTFLAGS = + current.length > 0 + ? `${current} ${WINDOWS_STATIC_CRT_RUSTFLAGS}` + : WINDOWS_STATIC_CRT_RUSTFLAGS; + return next; +} + function buildWithCargoDirectly(env, host) { const cargoExe = getCargoExePath(env) ?? "cargo"; try { @@ -405,7 +426,10 @@ try { } const scriptEnv = ensureCargoOnPath( - withWindowsSdkEnv(withMsvcDevEnv(withRustToolchainOnPath(process.env), host), host), + withWindowsStaticCrtEnv( + withWindowsSdkEnv(withMsvcDevEnv(withRustToolchainOnPath(process.env), host), host), + host, + ), ); // @napi-rs/cli parses Cargo.toml by running `cargo metadata` through cmd.exe on Windows. diff --git a/scripts/__tests__/native-windows-static-crt.test.mjs b/scripts/__tests__/native-windows-static-crt.test.mjs new file mode 100644 index 00000000..7babc1bd --- /dev/null +++ b/scripts/__tests__/native-windows-static-crt.test.mjs @@ -0,0 +1,64 @@ +import { strict as assert } from "node:assert"; +import { readFileSync } from "node:fs"; +import { describe, test } from "node:test"; +import { fileURLToPath } from "node:url"; + +const BUILD_RS_PATH = fileURLToPath(new URL("../../packages/native/build.rs", import.meta.url)); +const BUILD_NATIVE_SCRIPT_PATH = fileURLToPath( + new URL("../../packages/native/scripts/build-native.mjs", import.meta.url), +); +const PREBUILD_WORKFLOW_PATH = fileURLToPath( + new URL("../../.github/workflows/prebuild.yml", import.meta.url), +); +const RELEASE_WORKFLOW_PATH = fileURLToPath( + new URL("../../.github/workflows/release.yml", import.meta.url), +); + +const STATIC_CRT_RUSTFLAGS = "-C target-feature=+crt-static"; + +function assertWindowsPrebuildUsesStaticCrt(workflow, label) { + assert.match( + workflow, + new RegExp( + [ + "- name: Build prebuild", + "[\\s\\S]*?env:", + `[\\s\\S]*?RUSTFLAGS: \\$\\{\\{ runner\\.os == 'Windows' && '${STATIC_CRT_RUSTFLAGS.replace("+", "\\+")}' \\|\\| '' \\}\\}`, + '[\\s\\S]*?run: npx napi build --platform --release --target "\\$\\{\\{ matrix\\.rustTarget \\}\\}" --js false', + ].join("\\n?"), + "u", + ), + `${label} Windows prebuilds must pass Rust +crt-static`, + ); +} + +describe("native Windows CRT linking", () => { + test("local Windows native builds use static MSVC CRT", () => { + const buildRs = readFileSync(BUILD_RS_PATH, "utf8"); + const buildScript = readFileSync(BUILD_NATIVE_SCRIPT_PATH, "utf8"); + + assert.match(buildRs, /CARGO_CFG_TARGET_ENV"\)\.as_deref\(\) == Ok\("msvc"\)/); + assert.match( + buildRs, + /target_features\.split\(','\)\.any\(\|feature\| feature == "crt-static"\)/, + ); + assert.match(buildRs, /build\.static_crt\(true\)/); + assert.match( + buildScript, + /const WINDOWS_STATIC_CRT_RUSTFLAGS = "-C target-feature=\+crt-static";/, + ); + assert.match(buildScript, /function isWindowsMsvcTarget\(targetTriple\)/); + assert.match(buildScript, /function withWindowsStaticCrtEnv\(env, hostTargetTriple\)/); + }); + + test("published Windows native prebuilds use static MSVC CRT", () => { + assertWindowsPrebuildUsesStaticCrt( + readFileSync(PREBUILD_WORKFLOW_PATH, "utf8"), + "manual prebuild workflow", + ); + assertWindowsPrebuildUsesStaticCrt( + readFileSync(RELEASE_WORKFLOW_PATH, "utf8"), + "release workflow", + ); + }); +});