From 000a27524f156eb70a9638ba12c6294abe987547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 16 Sep 2026 18:01:03 +0200 Subject: [PATCH 1/3] fix(windows): unbreak the MSVC compiler link and the WinUI widget backend Two Windows-only breaks on main (CI run 35103968637). windows-arm64-build, 7 x LNK2019 on js_lru_cache_{new,get,set,has,delete, clear,peek}: perry-runtime's lru_subclass module declares the cache ABI as extern "C" and leaves it to whichever provider the PROGRAM links. A Rust binary that links perry-runtime without one still carries the references, and the Windows legs build two such binaries -- the perry compiler and the crate's own --lib test harness. Elsewhere that is invisible because ld64 -dead_strip / ld --gc-sections drop the thunks (and their references) before the linker reports; link.exe resolves before /OPT:REF, so the same inputs are hard unresolved externals there. A Cargo feature cannot express "this link has no provider": the job builds -p perry -p perry-runtime-static -p perry-stdlib-static in ONE invocation, so perry-stdlib's perry-runtime/stdlib is unified onto the copy of perry-runtime that the compiler links, and anything gated on it (stdlib_stubs, an external-*-symbols flag) is compiled out in exactly the failing configuration. Use MSVC's weak default instead: an #[cfg(all(windows, target_env = "msvc"))] module emits one /ALTERNATENAME:js_lru_cache_=perry_lru_cache_absent_ directive per symbol through .drectve, with no-op fallbacks that report via stub_diag. link.exe substitutes an alternate only for a symbol still undefined after every input is read, so a link that does carry perry_stdlib.lib or the ext archive binds the real implementation -- unlike an unconditional definition, which would duplicate or silently shadow it. windows-build, E0425 cannot find function `reorder_child` in module `widgets`: perry-ui-windows-winui #[path]-includes perry-ui-windows' ffi/mod.rs, so widget_layout_extras.rs resolves widgets:: against winui's own widgets.rs, which had add_child_at / remove_child / clear_children but no reorder_child. Add it in that module's shape -- delegate to the Win32 backend when Fluent is inactive, otherwise reorder the node's child list under with_node_mut, with the Win32 implementation's guards. perry_ui_widget_reorder_child is a live UI dispatch-table entry that every other backend implements, so cfg'ing the caller out would be a regression rather than a fix. --- changelog.d/10384-windows-build-breaks.md | 60 +++++++++ crates/perry-runtime/src/lru_subclass.rs | 125 +++++++++++++++++++ crates/perry-ui-windows-winui/src/widgets.rs | 29 +++++ 3 files changed, 214 insertions(+) create mode 100644 changelog.d/10384-windows-build-breaks.md diff --git a/changelog.d/10384-windows-build-breaks.md b/changelog.d/10384-windows-build-breaks.md new file mode 100644 index 0000000000..6f2a2114a8 --- /dev/null +++ b/changelog.d/10384-windows-build-breaks.md @@ -0,0 +1,60 @@ +### Fixed + +- **Windows: `perry.exe` failed to link with 7 × LNK2019 on the `js_lru_cache_*` + ABI.** `perry-runtime/src/lru_subclass.rs` (the `class X extends LRUCache` + support added with the LRUCache subclass-init) declares `js_lru_cache_new`, + `_get`, `_set`, `_has`, `_delete`, `_clear` and `_peek` as `extern "C"`, and + leaves them to whichever cache provider the *program* links — + `perry-ext-lru-cache` or perry-stdlib's `bundled-lru-cache`. A Rust binary + that links perry-runtime without either still carries the references, and CI + builds two such binaries on every Windows leg: the `perry` compiler + (`cargo build -p perry …`) and the crate's own `--lib` test harness. + + That is invisible everywhere else because those linkers dead-strip *before* + they report. `ld64 -dead_strip` drops the thunks out of a binary that never + calls them and the references go with them — verified locally: the same + `cargo build --profile perry-dev -p perry -p perry-runtime-static -p + perry-stdlib-static` that fails on `windows-arm64-build` succeeds on macOS, + and the linked `target/perry-dev/perry` carries no `js_lru_cache_subclass_init` + symbol at all while the rlib it links still shows all seven as `U`. + `link.exe` resolves symbols before `/OPT:REF`, so on MSVC the same inputs are + seven hard unresolved externals. + + A Cargo feature cannot express "this link has no provider": the Windows job + builds `-p perry -p perry-runtime-static -p perry-stdlib-static` in **one** + invocation, so perry-stdlib's `perry-runtime/stdlib` feature is unified onto + the copy of perry-runtime that `perry` links even though perry-stdlib is not + in that binary's link (checked against `cargo build --unit-graph`: the single + `perry-runtime` rlib unit has `stdlib` enabled). Anything gated on `stdlib` — + `crate::stdlib_stubs`, an `external-*-symbols` flag — is compiled out in + exactly the configuration that fails. + + Fixed with MSVC's spelling of a weak default: an `#[cfg(all(windows, + target_env = "msvc"))]` module in `lru_subclass.rs` emits one + `/ALTERNATENAME:js_lru_cache_=perry_lru_cache_absent_` linker + directive per symbol via `.drectve`, alongside no-op fallbacks that report + through `stub_diag::perry_stub_warn`. `link.exe` substitutes an alternate + only for a symbol still undefined after every input has been read, so a + program that does link `perry_stdlib.lib` or the ext archive binds the real + implementation and never reaches these — unlike an unconditional definition, + which would either duplicate (LNK2005) or silently shadow the real cache. + The fallbacks share a codegen unit with the thunks whose references they + answer. `js_lru_cache_new` answering `0` is already the module's "no cache" + path: subclass-init returns `this` with no method installed, so a `.get()` + throws `is not a function` at the call site, the same failure the module + already chooses for `forEach`/`dispose`/`fetch`. + +- **Windows: `perry-ui-windows-winui` failed to compile — + ``cannot find function `reorder_child` in module `widgets` ``.** + `perry-ui-windows-winui` `#[path]`-includes perry-ui-windows' + `src/ffi/mod.rs`, so `ffi/widget_layout_extras.rs`'s + `perry_ui_widget_reorder_child` resolves `widgets::` against winui's **own** + `src/widgets.rs` — which had `add_child_at`, `remove_child` and + `clear_children` but no `reorder_child`. Added it in the shape every other + entry in that module uses: delegate to `perry_ui_windows::widgets` when the + Fluent backend is inactive, otherwise reorder the node's own child list under + `with_node_mut`, with the same out-of-range / no-op guards as the Win32 + implementation. `perry_ui_widget_reorder_child` is a live entry in the + UI dispatch table and every other backend (macOS, GTK4, iOS, tvOS, visionOS, + Android, watchOS, Win32) implements it, so cfg'ing the caller out on Windows + would have been a regression, not a fix. diff --git a/crates/perry-runtime/src/lru_subclass.rs b/crates/perry-runtime/src/lru_subclass.rs index 58cb3934f7..7454627d7e 100644 --- a/crates/perry-runtime/src/lru_subclass.rs +++ b/crates/perry-runtime/src/lru_subclass.rs @@ -210,3 +210,128 @@ pub extern "C" fn js_lru_cache_subclass_init(this: f64, opts: f64) -> f64 { install_methods_on_existing_object(obj, this, &methods, &[]); this } + +/// Link-time default for the `js_lru_cache_*` ABI on MSVC, and nowhere else. +/// +/// The extern block above is satisfied by whichever cache provider the PROGRAM +/// links. A Rust binary that links perry-runtime without one still carries the +/// references, and two of them are built in CI on every Windows leg: the +/// `perry` compiler itself (`cargo build -p perry …`) and this crate's own +/// `--lib` test harness (`cargo test --lib -p perry-runtime`). Neither wants an +/// LRU cache; neither links a provider. +/// +/// On every other target that is harmless, because the linker dead-strips +/// before it reports: `ld64 -dead_strip` / `ld --gc-sections` drop the thunks +/// above out of a binary that never calls them, and the references go with +/// them. Verified on macOS — the linked `target/perry-dev/perry` contains no +/// `js_lru_cache_subclass_init` symbol at all, and the build succeeds while the +/// rlib it links still shows all seven as `U`. `link.exe` resolves symbols +/// BEFORE `/OPT:REF`, so the same inputs are 7 × LNK2019 there. +/// +/// A Cargo feature cannot express "this link has no provider". The Windows job +/// builds `-p perry -p perry-runtime-static -p perry-stdlib-static` in ONE +/// invocation, so perry-stdlib's `perry-runtime/stdlib` feature is unified onto +/// the copy of perry-runtime that the `perry` binary links — even though +/// perry-stdlib is not in that binary's link. Anything gated on `stdlib` +/// (`crate::stdlib_stubs`, an `external-*-symbols` flag) is therefore compiled +/// out in exactly the configuration that fails. +/// +/// `/ALTERNATENAME` is MSVC's spelling of a weak default: link.exe substitutes +/// the alternate only for a symbol still undefined after every input has been +/// read. A program that does link `perry_stdlib.lib` or the ext archive binds +/// the real implementation and never reaches these — so this cannot shadow a +/// provider the way an unconditional definition would. They live in this module +/// so that they share a codegen unit with the thunks whose references they +/// answer. +/// +/// `js_lru_cache_new` answering 0 is already the "no cache" path: the +/// subclass-init returns `this` without installing any method, so a `.get()` on +/// it throws `is not a function` at the call site — the same failure this +/// module deliberately chooses for `forEach`/`dispose`/`fetch`. +#[cfg(all(windows, target_env = "msvc"))] +mod msvc_absent_provider { + use crate::stub_diag::perry_stub_warn; + + const REASON: &str = + "no lru-cache provider (perry-ext-lru-cache / perry-stdlib bundled-lru-cache) \ + is linked into this binary"; + + /// Emit one `/ALTERNATENAME:=` linker directive. + macro_rules! alternatename { + ($stat:ident, $bytes:literal) => { + #[used] + #[link_section = ".drectve"] + static $stat: [u8; $bytes.len()] = *$bytes; + }; + } + + alternatename!( + D_NEW, + b" /ALTERNATENAME:js_lru_cache_new=perry_lru_cache_absent_new" + ); + alternatename!( + D_GET, + b" /ALTERNATENAME:js_lru_cache_get=perry_lru_cache_absent_get" + ); + alternatename!( + D_SET, + b" /ALTERNATENAME:js_lru_cache_set=perry_lru_cache_absent_set" + ); + alternatename!( + D_HAS, + b" /ALTERNATENAME:js_lru_cache_has=perry_lru_cache_absent_has" + ); + alternatename!( + D_DELETE, + b" /ALTERNATENAME:js_lru_cache_delete=perry_lru_cache_absent_delete" + ); + alternatename!( + D_CLEAR, + b" /ALTERNATENAME:js_lru_cache_clear=perry_lru_cache_absent_clear" + ); + alternatename!( + D_PEEK, + b" /ALTERNATENAME:js_lru_cache_peek=perry_lru_cache_absent_peek" + ); + + #[no_mangle] + pub extern "C" fn perry_lru_cache_absent_new(_options: f64) -> i64 { + perry_stub_warn("js_lru_cache_new", REASON, None); + 0 + } + + #[no_mangle] + pub extern "C" fn perry_lru_cache_absent_get(_handle: i64, _key: f64) -> f64 { + perry_stub_warn("js_lru_cache_get", REASON, None); + super::undefined_value() + } + + #[no_mangle] + pub extern "C" fn perry_lru_cache_absent_set(handle: i64, _key: f64, _value: f64) -> i64 { + perry_stub_warn("js_lru_cache_set", REASON, None); + handle + } + + #[no_mangle] + pub extern "C" fn perry_lru_cache_absent_has(_handle: i64, _key: f64) -> f64 { + perry_stub_warn("js_lru_cache_has", REASON, None); + super::bool_value(false) + } + + #[no_mangle] + pub extern "C" fn perry_lru_cache_absent_delete(_handle: i64, _key: f64) -> f64 { + perry_stub_warn("js_lru_cache_delete", REASON, None); + super::bool_value(false) + } + + #[no_mangle] + pub extern "C" fn perry_lru_cache_absent_clear(_handle: i64) { + perry_stub_warn("js_lru_cache_clear", REASON, None); + } + + #[no_mangle] + pub extern "C" fn perry_lru_cache_absent_peek(_handle: i64, _key: f64) -> f64 { + perry_stub_warn("js_lru_cache_peek", REASON, None); + super::undefined_value() + } +} diff --git a/crates/perry-ui-windows-winui/src/widgets.rs b/crates/perry-ui-windows-winui/src/widgets.rs index a12669f6ed..965345f584 100644 --- a/crates/perry-ui-windows-winui/src/widgets.rs +++ b/crates/perry-ui-windows-winui/src/widgets.rs @@ -610,6 +610,35 @@ pub fn add_child_at(parent: i64, child: i64, index: i64) { }); } +/// Move an existing child without changing its native window or layout +/// metadata. The Win32 backend owns the widget list whenever Fluent +/// rendering is off, so this mirrors `perry_ui_windows::widgets::reorder_child` +/// exactly — including its out-of-range / no-op guards. Needed here because +/// `ffi/widget_layout_extras.rs` is `#[path]`-shared with perry-ui-windows and +/// resolves `widgets::` against THIS module. +pub fn reorder_child(parent: i64, from_index: i64, to_index: i64) { + // Win32 rejects a non-positive parent outright, and so must this: the + // Fluent arm reaches its node through `handle.saturating_sub(1)`, which + // would turn handle 0 into node 0 and reorder the wrong subtree. + if parent <= 0 { + return; + } + if !is_fluent() { + perry_ui_windows::widgets::reorder_child(parent, from_index, to_index); + return; + } + with_node_mut(parent, |node| { + let from = from_index as usize; + let to = to_index as usize; + let len = node.common.children.len(); + if from >= len || to >= len || from == to { + return; + } + let child = node.common.children.remove(from); + node.common.children.insert(to, child); + }); +} + pub fn remove_child(parent: i64, child: i64) { if !is_fluent() { perry_ui_windows::widgets::remove_child(parent, child); From 347fa5d17391d2f16c77a8b6ed7cf9ef294e11c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 16 Sep 2026 18:04:28 +0200 Subject: [PATCH 2/3] docs: drop the guessed changelog filename --- changelog.d/10384-windows-build-breaks.md | 60 ----------------------- 1 file changed, 60 deletions(-) delete mode 100644 changelog.d/10384-windows-build-breaks.md diff --git a/changelog.d/10384-windows-build-breaks.md b/changelog.d/10384-windows-build-breaks.md deleted file mode 100644 index 6f2a2114a8..0000000000 --- a/changelog.d/10384-windows-build-breaks.md +++ /dev/null @@ -1,60 +0,0 @@ -### Fixed - -- **Windows: `perry.exe` failed to link with 7 × LNK2019 on the `js_lru_cache_*` - ABI.** `perry-runtime/src/lru_subclass.rs` (the `class X extends LRUCache` - support added with the LRUCache subclass-init) declares `js_lru_cache_new`, - `_get`, `_set`, `_has`, `_delete`, `_clear` and `_peek` as `extern "C"`, and - leaves them to whichever cache provider the *program* links — - `perry-ext-lru-cache` or perry-stdlib's `bundled-lru-cache`. A Rust binary - that links perry-runtime without either still carries the references, and CI - builds two such binaries on every Windows leg: the `perry` compiler - (`cargo build -p perry …`) and the crate's own `--lib` test harness. - - That is invisible everywhere else because those linkers dead-strip *before* - they report. `ld64 -dead_strip` drops the thunks out of a binary that never - calls them and the references go with them — verified locally: the same - `cargo build --profile perry-dev -p perry -p perry-runtime-static -p - perry-stdlib-static` that fails on `windows-arm64-build` succeeds on macOS, - and the linked `target/perry-dev/perry` carries no `js_lru_cache_subclass_init` - symbol at all while the rlib it links still shows all seven as `U`. - `link.exe` resolves symbols before `/OPT:REF`, so on MSVC the same inputs are - seven hard unresolved externals. - - A Cargo feature cannot express "this link has no provider": the Windows job - builds `-p perry -p perry-runtime-static -p perry-stdlib-static` in **one** - invocation, so perry-stdlib's `perry-runtime/stdlib` feature is unified onto - the copy of perry-runtime that `perry` links even though perry-stdlib is not - in that binary's link (checked against `cargo build --unit-graph`: the single - `perry-runtime` rlib unit has `stdlib` enabled). Anything gated on `stdlib` — - `crate::stdlib_stubs`, an `external-*-symbols` flag — is compiled out in - exactly the configuration that fails. - - Fixed with MSVC's spelling of a weak default: an `#[cfg(all(windows, - target_env = "msvc"))]` module in `lru_subclass.rs` emits one - `/ALTERNATENAME:js_lru_cache_=perry_lru_cache_absent_` linker - directive per symbol via `.drectve`, alongside no-op fallbacks that report - through `stub_diag::perry_stub_warn`. `link.exe` substitutes an alternate - only for a symbol still undefined after every input has been read, so a - program that does link `perry_stdlib.lib` or the ext archive binds the real - implementation and never reaches these — unlike an unconditional definition, - which would either duplicate (LNK2005) or silently shadow the real cache. - The fallbacks share a codegen unit with the thunks whose references they - answer. `js_lru_cache_new` answering `0` is already the module's "no cache" - path: subclass-init returns `this` with no method installed, so a `.get()` - throws `is not a function` at the call site, the same failure the module - already chooses for `forEach`/`dispose`/`fetch`. - -- **Windows: `perry-ui-windows-winui` failed to compile — - ``cannot find function `reorder_child` in module `widgets` ``.** - `perry-ui-windows-winui` `#[path]`-includes perry-ui-windows' - `src/ffi/mod.rs`, so `ffi/widget_layout_extras.rs`'s - `perry_ui_widget_reorder_child` resolves `widgets::` against winui's **own** - `src/widgets.rs` — which had `add_child_at`, `remove_child` and - `clear_children` but no `reorder_child`. Added it in the shape every other - entry in that module uses: delegate to `perry_ui_windows::widgets` when the - Fluent backend is inactive, otherwise reorder the node's own child list under - `with_node_mut`, with the same out-of-range / no-op guards as the Win32 - implementation. `perry_ui_widget_reorder_child` is a live entry in the - UI dispatch table and every other backend (macOS, GTK4, iOS, tvOS, visionOS, - Android, watchOS, Win32) implements it, so cfg'ing the caller out on Windows - would have been a regression, not a fix. From 0bf76991b9320f6386443d4a41d5101e6a4c40d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 16 Sep 2026 18:04:49 +0200 Subject: [PATCH 3/3] docs: changelog fragment for #10384 --- changelog.d/10384-windows-build-breaks.md | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 changelog.d/10384-windows-build-breaks.md diff --git a/changelog.d/10384-windows-build-breaks.md b/changelog.d/10384-windows-build-breaks.md new file mode 100644 index 0000000000..1614f9ffc6 --- /dev/null +++ b/changelog.d/10384-windows-build-breaks.md @@ -0,0 +1,32 @@ +Unbreak both Windows CI legs on `main`. + +`windows-arm64-build` failed with `LNK1120: 7 unresolved externals` — the whole +`js_lru_cache_*` ABI — when linking **the `perry` compiler itself**. +`perry-runtime/src/lru_subclass.rs` declares that ABI `extern "C"` and leaves it +to whichever cache provider the program links; `perry` links neither provider, so +its link carries seven undefined references. Every other target hides this +because its linker dead-strips before it reports (the same command succeeds on +macOS while the rlib still shows all seven as `U`), whereas `link.exe` resolves +symbols before `/OPT:REF`. + +A Cargo feature cannot express "this link has no provider": the Windows job +builds `-p perry -p perry-runtime-static -p perry-stdlib-static` in one +invocation, so perry-stdlib unifies `perry-runtime/stdlib` onto the copy of +perry-runtime that `perry` links, and anything gated on `stdlib` — including the +existing `stdlib_stubs` mechanism — is compiled out in exactly the failing +configuration. Fixed with `/ALTERNATENAME` directives in `.drectve` behind +`cfg(all(windows, target_env = "msvc"))` plus no-op fallbacks reporting through +`perry_stub_warn`; `link.exe` substitutes an alternate only for a symbol still +undefined after all inputs are read, so a real provider always wins. + +`windows-build` failed with `error[E0425]: cannot find function reorder_child in +module widgets`, in `perry-ui-windows-winui`: it `#[path]`-includes +perry-ui-windows' `ffi/mod.rs`, so `widgets::` resolves against winui's own +module, which never gained `reorder_child`. Added in the module's established +shape — delegate to the Win32 implementation when Fluent is inactive, otherwise +reorder the node's children under `with_node_mut`. + +Neither Windows job runs in the PR tier (`ci_plan.py`: sweep and full only), so +the fix was validated by emitting the COFF object for +`x86_64-pc-windows-msvc` and confirming the `.drectve` contents and symbol +classes directly; no Windows link was performed.