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. 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);