Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/release-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,25 @@ jobs:
with:
shared-key: "release-${{ matrix.target }}"

# #4856 — evict workspace-crate fingerprints from the restored cache.
# rust-cache's own workspace cleanup misses cross-target dirs, so a
# restored target/ can make cargo treat perry-runtime & co. as
# up-to-date and silently reuse a staticlib built from older sources
# (v0.5.1150 shipped Apple cross runtimes missing
# perry_macos_bundle_chdir that way). Deleting the .fingerprint
# entries forces every workspace crate to rebuild from the checkout
# while keeping the expensive external-dependency precompiles cached.
# Note `cargo clean -p` is NOT equivalent: it only covers the host
# layout, not target/<triple>/ dirs (verified on cargo 1.8x).
- name: Evict workspace crates from restored cache (#4856)
shell: bash
run: |
set -euo pipefail
while IFS= read -r pkg; do
rm -rf target/release/.fingerprint/"$pkg"-* \
target/*/release/.fingerprint/"$pkg"-*
done < <(cargo metadata --no-deps --format-version 1 | jq -r '.packages[].name')

# Closes #394: Homebrew bottle was missing libperry_ui_ios.a so
# `perry compile foo.ts -o app --target ios[-simulator]` failed at link
# with `perry/ui imported but libperry_ui_ios.a not found`. Both macOS
Expand Down Expand Up @@ -388,6 +407,23 @@ jobs:
cargo build --release --target aarch64-linux-android -p perry-runtime
cargo build --release --target aarch64-linux-android -p perry-stdlib

# #4856 — defense in depth behind the cache eviction above: assert
# every runtime archive this leg built (host triple + any Apple/
# Android cross triples) defines the sentinel #[no_mangle] symbols.
# A stale cached archive fails the release here instead of breaking
# consumer links with `undefined symbol: _perry_macos_bundle_chdir`.
- name: Verify runtime archives are fresh (#4856)
shell: bash
run: |
set -euo pipefail
libs=$(find target -path "*/release/libperry_runtime.a" -o -path "*/release/perry_runtime.lib")
if [ -z "$libs" ]; then
echo "::error::no built runtime archives found under target/ — nothing to verify"
exit 1
fi
# shellcheck disable=SC2086
./scripts/check_runtime_symbols.sh $libs

- name: Package release archive (Unix)
if: runner.os != 'Windows'
run: |
Expand Down Expand Up @@ -725,6 +761,22 @@ jobs:
with:
shared-key: "release-cross-${{ matrix.target }}"

# #4856 — evict workspace-crate fingerprints from the restored cache
# so perry-runtime/stdlib/UI always rebuild from the checkout. The
# v0.5.1150 Apple cross bundles shipped a stale pre-#4833
# libperry_runtime.a (missing perry_macos_bundle_chdir) because the
# restored cache let cargo skip the rebuild. Mirrors the eviction
# step in `build:` — see the comment there for why `cargo clean -p`
# doesn't work (host layout only, misses target/<triple>/).
- name: Evict workspace crates from restored cache (#4856)
shell: bash
run: |
set -euo pipefail
while IFS= read -r pkg; do
rm -rf target/release/.fingerprint/"$pkg"-* \
target/*/release/.fingerprint/"$pkg"-*
done < <(cargo metadata --no-deps --format-version 1 | jq -r '.packages[].name')

# --- Per-runner toolchain wiring --------------------------------------
# Each of these blocks installs what the cross target needs that's NOT
# already on the runner image. Apple runners get Xcode for free; Linux
Expand Down Expand Up @@ -803,6 +855,18 @@ jobs:
--target ${{ matrix.target }} \
-p perry-runtime -p perry-stdlib -p ${{ matrix.ui_crate }}

# #4856 — defense in depth behind the cache eviction above: a stale
# cached runtime archive fails the release here instead of breaking
# every consumer executable link on the build workers.
- name: Verify runtime archive is fresh (#4856)
shell: bash
run: |
set -euo pipefail
lib="target/${{ matrix.target }}/release/libperry_runtime.a"
# Windows MSVC emits perry_runtime.lib (no `lib` prefix).
[ -f "$lib" ] || lib="target/${{ matrix.target }}/release/perry_runtime.lib"
./scripts/check_runtime_symbols.sh "$lib"

# --- Package + manifest ----------------------------------------------
# Manifest schema (per issue #1083):
# { "perry_version": "<x.y.z>", "target_triple": "<triple>",
Expand Down
12 changes: 10 additions & 2 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ use crate::types::{DOUBLE, I32, I8, PTR, VOID};

use super::helpers::{
emit_namespace_populator, enable_module_init_shadow_frame, init_static_fields_early,
init_static_fields_late, register_module_globals_as_gc_roots, write_barriers_enabled,
init_static_fields_late, is_macos_triple, register_module_globals_as_gc_roots,
write_barriers_enabled,
};
use super::opts::CrossModuleCtx;

Expand Down Expand Up @@ -145,7 +146,14 @@ pub(super) fn compile_module_entry(
// launch starts at CWD=`/`. chdir there before any user code or
// native engine init so relative asset paths (`assets/...`) resolve.
// No-op on non-macOS and on non-bundle binaries (see the runtime fn).
blk.call_void("perry_macos_bundle_chdir", &[]);
// Emitted only for macOS triples (#4856): the runtime fn is a no-op
// everywhere else anyway, and referencing it from every `main` made
// iOS/tvOS links depend on non-macOS runtime archives carrying a
// macOS-only symbol — a stale cross runtime then failed the link
// with `undefined symbol: _perry_macos_bundle_chdir`.
if is_macos_triple(&cross_module.target_triple) {
blk.call_void("perry_macos_bundle_chdir", &[]);
}
if let Some((const_name, byte_len)) = app_group_init.as_ref() {
let suite_ptr = format!("@{}", const_name);
let len_str = byte_len.to_string();
Expand Down
11 changes: 11 additions & 0 deletions crates/perry-codegen/src/codegen/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,17 @@ pub fn resolve_target_triple(name: &str) -> Option<String> {
}
}

/// True for macOS triples only (`*-apple-macosx*` LLVM-style, or
/// `*-apple-darwin*` rustc-style when a raw triple is passed through).
/// Deliberately false for every other Apple platform (`apple-ios`,
/// `apple-tvos`, `apple-xros`, `apple-watchos`): the `.app` CWD fix in
/// `perry_macos_bundle_chdir` is macOS-only, and emitting the call on
/// non-macOS targets makes their links depend on the runtime archive
/// carrying a macOS-only symbol (#4856).
pub(super) fn is_macos_triple(triple: &str) -> bool {
triple.contains("apple-macosx") || triple.contains("apple-darwin")
}

pub(super) fn emit_buffer_alias_metadata(llmod: &mut LlModule, count: u32) {
if count == 0 {
return;
Expand Down
135 changes: 135 additions & 0 deletions crates/perry-codegen/tests/macos_bundle_chdir_gate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//! #4856 — `perry_macos_bundle_chdir` must only be emitted into `main` for
//! macOS targets. The runtime fn is a documented no-op everywhere else, and
//! referencing it unconditionally made every iOS/tvOS executable link depend
//! on the Apple cross runtime archive carrying a macOS-only symbol — a stale
//! cross runtime then failed the link with
//! `undefined symbol: _perry_macos_bundle_chdir`.

use perry_codegen::{compile_module, AppMetadata, CompileOptions};
use perry_hir::{Module, ModuleInitKind};

fn entry_opts(target: Option<&str>) -> CompileOptions {
CompileOptions {
target: target.map(str::to_string),
is_entry_module: true,
non_entry_module_prefixes: Vec::new(),
import_function_prefixes: std::collections::HashMap::new(),
import_function_origin_names: std::collections::HashMap::new(),
import_function_v8_specifiers: std::collections::HashMap::new(),
import_function_node_submodule: std::collections::HashMap::new(),
namespace_node_submodules: std::collections::HashMap::new(),
namespace_v8_specifiers: std::collections::HashMap::new(),
namespace_member_prefixes: std::collections::HashMap::new(),
emit_ir_only: true,
verify_native_regions: false,
disable_buffer_fast_path: false,
namespace_imports: Vec::new(),
namespace_reexport_named_imports: std::collections::HashSet::new(),
imported_classes: Vec::new(),
imported_enums: Vec::new(),
imported_async_funcs: std::collections::HashSet::new(),
type_aliases: std::collections::HashMap::new(),
imported_func_param_counts: std::collections::HashMap::new(),
imported_func_has_rest: std::collections::HashSet::new(),
imported_func_synthetic_arguments: std::collections::HashSet::new(),
imported_func_return_types: std::collections::HashMap::new(),
imported_vars: std::collections::HashSet::new(),
output_type: "executable".to_string(),
needs_stdlib: false,
needs_ui: false,
needs_geisterhand: false,
geisterhand_port: 7676,
enabled_features: Vec::new(),
native_module_init_names: Vec::new(),
js_module_specifiers: Vec::new(),
bundled_extensions: Vec::new(),
native_library_functions: Vec::new(),
i18n_table: None,
fast_math: false,
fp_contract_mode: perry_codegen::FpContractMode::Off,
app_metadata: AppMetadata::default(),
namespace_entries: Vec::new(),
dynamic_import_path_to_prefix: std::collections::HashMap::new(),
deferred_module_prefixes: std::collections::HashSet::new(),
module_init_deps: Vec::new(),
is_dynamic_import_target: false,
}
}

fn empty_entry_module() -> Module {
Module {
name: "chdir_gate.ts".to_string(),
imports: Vec::new(),
exports: Vec::new(),
classes: Vec::new(),
interfaces: Vec::new(),
type_aliases: Vec::new(),
enums: Vec::new(),
globals: Vec::new(),
functions: Vec::new(),
init: Vec::new(),
exported_native_instances: Vec::new(),
exported_func_return_native_instances: Vec::new(),
exported_objects: Vec::new(),
exported_functions: Vec::new(),
widgets: Vec::new(),
uses_fetch: false,
uses_webassembly: false,
extern_funcs: Vec::new(),
init_was_unrolled: false,
has_top_level_await: false,
init_kind: ModuleInitKind::Eager,
async_step_closures: std::collections::HashSet::new(),
closure_display_names: std::collections::HashMap::new(),
closure_source_text: std::collections::HashMap::new(),
async_generator_funcs: std::collections::HashSet::new(),
gen_param_prologue_len: std::collections::HashMap::new(),
}
}

fn ir_for_target(target: &str) -> String {
String::from_utf8(compile_module(&empty_entry_module(), entry_opts(Some(target))).unwrap())
.unwrap()
}

const CHDIR_CALL: &str = "call void @perry_macos_bundle_chdir()";

#[test]
fn macos_targets_emit_bundle_chdir_in_main() {
for triple in [
"arm64-apple-macosx15.0.0",
"x86_64-apple-macosx15.0.0",
"aarch64-apple-darwin",
] {
let ir = ir_for_target(triple);
assert!(
ir.contains(CHDIR_CALL),
"expected {} in main for {}",
CHDIR_CALL,
triple
);
}
}

#[test]
fn non_macos_targets_do_not_reference_bundle_chdir() {
for triple in [
"aarch64-apple-ios",
"arm64-apple-ios17.0-simulator",
"aarch64-apple-tvos",
"arm64-apple-tvos17.0-simulator",
"arm64-apple-xros1.0",
"arm64_32-apple-watchos",
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-musl",
"aarch64-unknown-linux-android",
"x86_64-pc-windows-msvc",
] {
let ir = ir_for_target(triple);
assert!(
!ir.contains(CHDIR_CALL),
"{} must not call perry_macos_bundle_chdir from main (#4856)",
triple
);
}
}
82 changes: 82 additions & 0 deletions scripts/check_runtime_symbols.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# Post-build freshness guard for runtime static libraries (#4856).
#
# A Swatinem/rust-cache restore can leave `target/` fingerprints that make
# cargo treat workspace crates as up-to-date and silently reuse a
# `libperry_runtime.a` built from older sources — v0.5.1150 shipped Apple
# cross runtimes missing `perry_macos_bundle_chdir` exactly that way, which
# broke every iOS/tvOS executable link. This script asserts that a built
# runtime archive defines the sentinel `#[no_mangle]` symbols below, so a
# stale archive fails the release instead of shipping.
#
# Usage: check_runtime_symbols.sh <libperry_runtime.a | perry_runtime.lib>...
#
# When codegen starts referencing a new unconditional runtime symbol from
# every program's `main` prelude (see perry-codegen/src/codegen/entry.rs),
# add it to SENTINELS so a stale runtime missing it is caught here, not on
# a build worker at link time.

set -euo pipefail

if [ "$#" -lt 1 ]; then
echo "usage: $0 <runtime-archive>..." >&2
exit 2
fi

# Each sentinel must be defined unconditionally in perry-runtime — i.e. a
# `#[no_mangle] pub extern "C" fn` with no `#[cfg]` on the item or its
# module (a cfg-gated *body* is fine; the symbol still exists everywhere).
SENTINELS=(
js_gc_init
perry_macos_bundle_chdir # added by #4833; absence = pre-#4833 stale archive
)

# Tool preference: rustup's llvm-tools nm (matches rustc's LLVM, reads the
# thin-LTO bitcode members) → PATH llvm-nm → system nm. The fallbacks may
# fail to parse bitcode members, but `--print-armap` below only needs the
# archive symbol index (ranlib map), which every archiver writes as plain
# data — readable regardless of member object format.
NM=""
if command -v rustc >/dev/null 2>&1; then
sysroot=$(rustc --print sysroot 2>/dev/null || true)
host=$(rustc -vV 2>/dev/null | sed -n 's/^host: //p')
if [ -n "$sysroot" ] && [ -n "$host" ] && [ -x "$sysroot/lib/rustlib/$host/bin/llvm-nm" ]; then
NM="$sysroot/lib/rustlib/$host/bin/llvm-nm"
fi
fi
if [ -z "$NM" ] && command -v llvm-nm >/dev/null 2>&1; then
NM=llvm-nm
fi
if [ -z "$NM" ] && command -v nm >/dev/null 2>&1; then
NM=nm
fi
if [ -z "$NM" ]; then
echo "::warning::check_runtime_symbols: no llvm-nm/nm available — skipping symbol guard" >&2
exit 0
fi

status=0
for lib in "$@"; do
if [ ! -f "$lib" ]; then
echo "::error::check_runtime_symbols: $lib does not exist" >&2
status=1
continue
fi
# `--print-armap` emits the archive symbol index ("sym in member.o") in
# addition to per-member listings; unreadable members only lose the
# latter. Tokenize, strip the Mach-O leading underscore, exact-match —
# no substring false positives (`foo_js_gc_init` ≠ `js_gc_init`).
tokens=$("$NM" --print-armap "$lib" 2>/dev/null | tr -d '\r' | tr ' \t' '\n\n' | sed 's/^_//' | sort -u || true)
missing=0
for sym in "${SENTINELS[@]}"; do
if ! grep -qx "$sym" <<<"$tokens"; then
echo "::error::$lib does not define runtime symbol '$sym' — stale cached build artifact? (#4856)" >&2
missing=1
status=1
fi
done
if [ "$missing" -eq 0 ]; then
echo "ok: $lib defines all ${#SENTINELS[@]} sentinel symbols"
fi
done
exit "$status"