Skip to content

Commit 5dee439

Browse files
committed
fix: statically link Windows native CRT
1 parent 671a8a2 commit 5dee439

10 files changed

Lines changed: 111 additions & 1 deletion

File tree

.github/workflows/prebuild.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ jobs:
7171
arch: ${{ matrix.msvcArch }}
7272

7373
- name: Build prebuild
74+
env:
75+
RUSTFLAGS: ${{ runner.os == 'Windows' && '-C target-feature=+crt-static' || '' }}
7476
run: npx napi build --platform --release --target "${{ matrix.rustTarget }}" --js false
7577
working-directory: packages/native
7678

.github/workflows/release.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@ jobs:
332332
arch: ${{ matrix.msvcArch }}
333333

334334
- name: Build prebuild
335+
env:
336+
RUSTFLAGS: ${{ runner.os == 'Windows' && '-C target-feature=+crt-static' || '' }}
335337
run: npx napi build --platform --release --target "${{ matrix.rustTarget }}" --js false
336338
working-directory: packages/native
337339

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ The format is based on Keep a Changelog and the project follows Semantic Version
1414

1515
### Fixed
1616

17+
- **native/windows**: Windows prebuilt native binaries now link the MSVC runtime
18+
statically, so packaged apps can run on clean Windows installs without a
19+
separate Visual C++ Redistributable install.
1720
- **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.
1821

1922
## [0.1.0-beta.3] - 2026-06-11

docs/backend/native.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ combinations:
3737
When you install `@rezi-ui/native`, the correct prebuilt binary is selected
3838
automatically based on `process.platform` and `process.arch`. No postinstall
3939
scripts are required -- the binary is included directly in the published package.
40+
Windows prebuilds are linked with the static MSVC C runtime, so they do not
41+
require users to install the Visual C++ Redistributable separately.
4042

4143
Prebuilt binaries are produced by the `prebuild.yml` GitHub Actions workflow,
4244
which cross-compiles on CI for all target triples.

docs/getting-started/install.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ No install-time source build runs for unsupported targets. If a matching
6262
prebuilt binary is not available, build from a repository checkout with
6363
`npm run build:native`.
6464

65+
Windows prebuilt binaries do not require a separate Visual C++ Redistributable
66+
install.
67+
6568
## Package Overview
6669

6770
| Package | Description | Required |

docs/packages/native.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Native addon:
55
- Rust + `napi-rs` binding
66
- vendored Zireael engine sources
77
- prebuilt `.node` binaries for supported targets
8+
- static MSVC runtime linking for Windows prebuilds
89

910
## Install
1011

packages/native/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ npm run check:native-vendor
2929
- Engine placement is controlled by `@rezi-ui/node` `executionMode` (`auto` | `worker` | `inline`).
3030
- `executionMode: "auto"` selects inline when `fpsCap <= 30`, worker otherwise.
3131
- All buffers across the boundary are caller-owned; binary formats are validated strictly.
32+
- Windows builds link the MSVC C runtime statically so packaged apps do not need
33+
a separate Visual C++ Redistributable install.
3234
- Native compilation reads `packages/native/vendor/zireael` (not `vendor/zireael`).
3335
- `packages/native/vendor/VENDOR_COMMIT.txt` must match the `vendor/zireael` gitlink commit.
3436

packages/native/build.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ fn main() {
1414
build.include(&src_dir);
1515
build.warnings(false);
1616

17+
let target_features = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or_default();
18+
if env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc")
19+
&& target_features.split(',').any(|feature| feature == "crt-static")
20+
{
21+
build.static_crt(true);
22+
}
23+
1724
// The engine assumes a C99-or-newer compiler; C11 is required for atomics on MSVC.
1825
if build.get_compiler().is_like_msvc() {
1926
build.flag_if_supported("/std:c11");

packages/native/scripts/build-native.mjs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { copyFileSync, existsSync, readdirSync, rmSync, writeFileSync } from "no
33
import { tmpdir } from "node:os";
44
import { delimiter, join } from "node:path";
55

6+
const WINDOWS_STATIC_CRT_RUSTFLAGS = "-C target-feature=+crt-static";
7+
68
function splitPathList(value, delim) {
79
return value
810
.split(delim)
@@ -338,6 +340,25 @@ function withRustToolchainOnPath(env) {
338340
return next;
339341
}
340342

343+
function isWindowsMsvcTarget(targetTriple) {
344+
return typeof targetTriple === "string" && targetTriple.endsWith("-pc-windows-msvc");
345+
}
346+
347+
function withWindowsStaticCrtEnv(env, hostTargetTriple) {
348+
if (process.platform !== "win32" || !isWindowsMsvcTarget(hostTargetTriple)) return env;
349+
350+
const current = typeof env.RUSTFLAGS === "string" ? env.RUSTFLAGS.trim() : "";
351+
if (/(^|\s)target-feature=\+crt-static(\s|$)/.test(current)) return env;
352+
if (/(^|\s)-Ctarget-feature=\+crt-static(\s|$)/.test(current)) return env;
353+
354+
const next = { ...env };
355+
next.RUSTFLAGS =
356+
current.length > 0
357+
? `${current} ${WINDOWS_STATIC_CRT_RUSTFLAGS}`
358+
: WINDOWS_STATIC_CRT_RUSTFLAGS;
359+
return next;
360+
}
361+
341362
function buildWithCargoDirectly(env, host) {
342363
const cargoExe = getCargoExePath(env) ?? "cargo";
343364
try {
@@ -405,7 +426,10 @@ try {
405426
}
406427

407428
const scriptEnv = ensureCargoOnPath(
408-
withWindowsSdkEnv(withMsvcDevEnv(withRustToolchainOnPath(process.env), host), host),
429+
withWindowsStaticCrtEnv(
430+
withWindowsSdkEnv(withMsvcDevEnv(withRustToolchainOnPath(process.env), host), host),
431+
host,
432+
),
409433
);
410434

411435
// @napi-rs/cli parses Cargo.toml by running `cargo metadata` through cmd.exe on Windows.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { strict as assert } from "node:assert";
2+
import { readFileSync } from "node:fs";
3+
import { describe, test } from "node:test";
4+
import { fileURLToPath } from "node:url";
5+
6+
const BUILD_RS_PATH = fileURLToPath(new URL("../../packages/native/build.rs", import.meta.url));
7+
const BUILD_NATIVE_SCRIPT_PATH = fileURLToPath(
8+
new URL("../../packages/native/scripts/build-native.mjs", import.meta.url),
9+
);
10+
const PREBUILD_WORKFLOW_PATH = fileURLToPath(
11+
new URL("../../.github/workflows/prebuild.yml", import.meta.url),
12+
);
13+
const RELEASE_WORKFLOW_PATH = fileURLToPath(
14+
new URL("../../.github/workflows/release.yml", import.meta.url),
15+
);
16+
17+
const STATIC_CRT_RUSTFLAGS = "-C target-feature=+crt-static";
18+
19+
function assertWindowsPrebuildUsesStaticCrt(workflow, label) {
20+
assert.match(
21+
workflow,
22+
new RegExp(
23+
[
24+
"- name: Build prebuild",
25+
"[\\s\\S]*?env:",
26+
`[\\s\\S]*?RUSTFLAGS: \\$\\{\\{ runner\\.os == 'Windows' && '${STATIC_CRT_RUSTFLAGS.replace("+", "\\+")}' \\|\\| '' \\}\\}`,
27+
'[\\s\\S]*?run: npx napi build --platform --release --target "\\$\\{\\{ matrix\\.rustTarget \\}\\}" --js false',
28+
].join("\\n?"),
29+
"u",
30+
),
31+
`${label} Windows prebuilds must pass Rust +crt-static`,
32+
);
33+
}
34+
35+
describe("native Windows CRT linking", () => {
36+
test("local Windows native builds use static MSVC CRT", () => {
37+
const buildRs = readFileSync(BUILD_RS_PATH, "utf8");
38+
const buildScript = readFileSync(BUILD_NATIVE_SCRIPT_PATH, "utf8");
39+
40+
assert.match(buildRs, /CARGO_CFG_TARGET_ENV"\)\.as_deref\(\) == Ok\("msvc"\)/);
41+
assert.match(
42+
buildRs,
43+
/target_features\.split\(','\)\.any\(\|feature\| feature == "crt-static"\)/,
44+
);
45+
assert.match(buildRs, /build\.static_crt\(true\)/);
46+
assert.match(
47+
buildScript,
48+
/const WINDOWS_STATIC_CRT_RUSTFLAGS = "-C target-feature=\+crt-static";/,
49+
);
50+
assert.match(buildScript, /function isWindowsMsvcTarget\(targetTriple\)/);
51+
assert.match(buildScript, /function withWindowsStaticCrtEnv\(env, hostTargetTriple\)/);
52+
});
53+
54+
test("published Windows native prebuilds use static MSVC CRT", () => {
55+
assertWindowsPrebuildUsesStaticCrt(
56+
readFileSync(PREBUILD_WORKFLOW_PATH, "utf8"),
57+
"manual prebuild workflow",
58+
);
59+
assertWindowsPrebuildUsesStaticCrt(
60+
readFileSync(RELEASE_WORKFLOW_PATH, "utf8"),
61+
"release workflow",
62+
);
63+
});
64+
});

0 commit comments

Comments
 (0)