From 8169c1a8bf4665d2e7d748d16ce61cb8e5dd1473 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Tue, 13 Oct 2026 07:35:01 -0700 Subject: [PATCH 1/5] fix: honor Pico SDK flags and archive optional Teensy libraries --- .github/workflows/check-ubuntu.yml | 2 + Cargo.lock | 42 ++-- Cargo.toml | 2 +- .../src/rp2040/orchestrator.rs | 228 +++++++++++++++++- .../src/teensy/orchestrator.rs | 9 +- .../fbuild-build-engine/src/pipeline/mod.rs | 4 +- .../src/pipeline/sequential.rs | 185 +++++++++++++- pyproject.toml | 2 +- 8 files changed, 433 insertions(+), 41 deletions(-) diff --git a/.github/workflows/check-ubuntu.yml b/.github/workflows/check-ubuntu.yml index 1e1e0a36..bce1964b 100644 --- a/.github/workflows/check-ubuntu.yml +++ b/.github/workflows/check-ubuntu.yml @@ -103,6 +103,8 @@ jobs: - name: Test id: tests run: uv run --no-project python ci/test_cache_status.py -- soldr cargo test --workspace + - name: Test optional SDK library linkage with native GCC LTO + run: soldr cargo test -p fbuild-build-engine --lib optional_framework_archive_preserves_only_referenced_initializers -- --ignored python-facade-tests: # FastLED/fbuild#1485 §5.2/§6: the embedded-CPython AT-P integration diff --git a/Cargo.lock b/Cargo.lock index 238cdd01..56c36b60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -988,7 +988,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fbuild-bench-fastled-examples" -version = "2.5.40" +version = "2.5.41" dependencies = [ "fbuild-core", "fbuild-library-select", @@ -1002,7 +1002,7 @@ dependencies = [ [[package]] name = "fbuild-build" -version = "2.5.40" +version = "2.5.41" dependencies = [ "async-trait", "blake3", @@ -1039,7 +1039,7 @@ dependencies = [ [[package]] name = "fbuild-build-arm" -version = "2.5.40" +version = "2.5.41" dependencies = [ "async-trait", "blake3", @@ -1073,7 +1073,7 @@ dependencies = [ [[package]] name = "fbuild-build-engine" -version = "2.5.40" +version = "2.5.41" dependencies = [ "async-trait", "blake3", @@ -1107,7 +1107,7 @@ dependencies = [ [[package]] name = "fbuild-build-esp" -version = "2.5.40" +version = "2.5.41" dependencies = [ "async-trait", "blake3", @@ -1142,7 +1142,7 @@ dependencies = [ [[package]] name = "fbuild-build-mcu" -version = "2.5.40" +version = "2.5.41" dependencies = [ "async-trait", "blake3", @@ -1176,7 +1176,7 @@ dependencies = [ [[package]] name = "fbuild-cli" -version = "2.5.40" +version = "2.5.41" dependencies = [ "blake3", "clap", @@ -1208,7 +1208,7 @@ dependencies = [ [[package]] name = "fbuild-config" -version = "2.5.40" +version = "2.5.41" dependencies = [ "fbuild-core", "fbuild-paths", @@ -1223,7 +1223,7 @@ dependencies = [ [[package]] name = "fbuild-core" -version = "2.5.40" +version = "2.5.41" dependencies = [ "async-trait", "fs2", @@ -1252,7 +1252,7 @@ dependencies = [ [[package]] name = "fbuild-daemon" -version = "2.5.40" +version = "2.5.41" dependencies = [ "async-trait", "axum 0.7.9", @@ -1291,7 +1291,7 @@ dependencies = [ [[package]] name = "fbuild-deploy" -version = "2.5.40" +version = "2.5.41" dependencies = [ "async-trait", "espflash", @@ -1319,7 +1319,7 @@ dependencies = [ [[package]] name = "fbuild-header-scan" -version = "2.5.40" +version = "2.5.41" dependencies = [ "criterion", "fbuild-paths", @@ -1330,7 +1330,7 @@ dependencies = [ [[package]] name = "fbuild-library" -version = "2.5.40" +version = "2.5.41" dependencies = [ "async-trait", "axum 0.7.9", @@ -1362,7 +1362,7 @@ dependencies = [ [[package]] name = "fbuild-library-select" -version = "2.5.40" +version = "2.5.41" dependencies = [ "bincode", "blake3", @@ -1383,7 +1383,7 @@ dependencies = [ [[package]] name = "fbuild-packages" -version = "2.5.40" +version = "2.5.41" dependencies = [ "async-trait", "axum 0.7.9", @@ -1415,7 +1415,7 @@ dependencies = [ [[package]] name = "fbuild-packages-fetch" -version = "2.5.40" +version = "2.5.41" dependencies = [ "async-trait", "axum 0.7.9", @@ -1444,7 +1444,7 @@ dependencies = [ [[package]] name = "fbuild-paths" -version = "2.5.40" +version = "2.5.41" dependencies = [ "blake3", "fbuild-core", @@ -1456,7 +1456,7 @@ dependencies = [ [[package]] name = "fbuild-python" -version = "2.5.40" +version = "2.5.41" dependencies = [ "base64", "fbuild-core", @@ -1480,7 +1480,7 @@ dependencies = [ [[package]] name = "fbuild-serial" -version = "2.5.40" +version = "2.5.41" dependencies = [ "async-trait", "base64", @@ -1503,7 +1503,7 @@ dependencies = [ [[package]] name = "fbuild-test-support" -version = "2.5.40" +version = "2.5.41" dependencies = [ "fbuild-config", "fbuild-core", @@ -1523,7 +1523,7 @@ dependencies = [ [[package]] name = "fbuild-toolchain" -version = "2.5.40" +version = "2.5.41" dependencies = [ "async-trait", "axum 0.7.9", diff --git a/Cargo.toml b/Cargo.toml index 66d15837..3c3c270a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,7 @@ targets = [ ] [workspace.package] -version = "2.5.40" +version = "2.5.41" edition = "2021" rust-version = "1.95.0" license = "AGPL-3.0-only" diff --git a/crates/fbuild-build-arm/src/rp2040/orchestrator.rs b/crates/fbuild-build-arm/src/rp2040/orchestrator.rs index 9ca6df5f..61d00f95 100644 --- a/crates/fbuild-build-arm/src/rp2040/orchestrator.rs +++ b/crates/fbuild-build-arm/src/rp2040/orchestrator.rs @@ -41,6 +41,8 @@ pub struct Rp2040Orchestrator; #[derive(Debug, Serialize)] struct Rp2040FingerprintMetadata { version: u32, + // Invalidate firmware produced before SDK platform.txt defaults were applied. + platform_define_policy: &'static str, env_name: String, profile: String, board_name: String, @@ -201,6 +203,7 @@ impl BuildOrchestrator for Rp2040Orchestrator { let metadata_hash = stable_hash_with_build_config( &Rp2040FingerprintMetadata { version: BUILD_FINGERPRINT_VERSION, + platform_define_policy: "platform-sdfatdefaults-v1", env_name: params.env_name.clone(), profile: profile_label(params.profile).to_string(), board_name: ctx.board.name.clone(), @@ -294,7 +297,7 @@ impl BuildOrchestrator for Rp2040Orchestrator { } add_rp_manifest_defines(&framework_dir, &ctx.board.mcu, &mut defines); apply_rp_board_props(&board_props, &framework_dir, &mut defines); - apply_rp_platform_net_defines(&framework_dir, &board_props, &mut defines)?; + apply_rp_platform_defines(&framework_dir, &board_props, &mut defines)?; // Arduino-Pico ships framework libraries (WiFi, SPI, Wire, ...) under // `libraries/`. Mirror PlatformIO's LDF so their headers are visible @@ -728,22 +731,33 @@ fn apply_rp_board_props( } } -/// Apply Arduino-Pico's network compiler definitions after expanding values -/// selected by the board's menu properties. These flags are declared by the -/// framework rather than owned by an individual board, so they must be read -/// from its platform metadata instead of being duplicated here. -fn apply_rp_platform_net_defines( +/// Apply Arduino-Pico's framework compiler definitions, with selected board +/// properties overriding platform defaults. SdFat's public-copy setting lives +/// in platform.txt, not boards.txt, and is required to compile bundled SDFS. +fn apply_rp_platform_defines( framework_dir: &Path, board_props: &Option>, defines: &mut HashMap, ) -> Result<()> { let platform_txt = framework_dir.join("platform.txt"); let content = std::fs::read_to_string(platform_txt)?; - let Some(flags) = content.lines().find_map(|line| { - line.trim() - .strip_prefix("compiler.netdefines=") - .map(str::trim) - }) else { + let property = |name: &str| { + content.lines().find_map(|line| { + let (key, value) = line.trim().split_once('=')?; + (key.trim() == name).then(|| value.trim()) + }) + }; + let sdfat_flags = board_props + .as_ref() + .and_then(|props| props.get("sdfatdefines")) + .map(String::as_str) + .or_else(|| property("build.sdfatdefines")); + if let Some(flags) = sdfat_flags { + let expanded = flags.replace("{runtime.platform.path}", &framework_dir.to_string_lossy()); + apply_define_flags(&fbuild_core::shell_split::split(&expanded), defines); + } + + let Some(flags) = property("compiler.netdefines") else { return Ok(()); }; @@ -1038,6 +1052,75 @@ mod tests { use super::*; use std::collections::HashMap; + #[test] + fn platform_default_policy_invalidates_legacy_firmware_cache() { + use crate::build_fingerprint::{ + PersistedBuildFingerprint, fast_path_check, hash_watch_set_stamps, save_json, + stable_hash_json, + }; + + let metadata = Rp2040FingerprintMetadata { + version: BUILD_FINGERPRINT_VERSION, + platform_define_policy: "platform-sdfatdefaults-v1", + env_name: "pico".into(), + profile: "release".into(), + board_name: "Pico".into(), + board_mcu: "rp2040".into(), + board_define: "ARDUINO_RASPBERRY_PI_PICO".into(), + board_core: "rp2040".into(), + board_variant: "rpipico".into(), + board_f_cpu: "133000000L".into(), + board_extra_flags: None, + board_upload_protocol: None, + board_menu_overrides: BTreeMap::new(), + platform: "raspberrypi".into(), + max_flash: None, + max_ram: None, + eh_frame_policy: "strip", + }; + let current = serde_json::to_value(&metadata).unwrap(); + let mut legacy = current.clone(); + // Successful builds before the SDK-default fix did not encode this policy. + legacy + .as_object_mut() + .unwrap() + .remove("platform_define_policy"); + let current_hash = stable_hash_json(¤t).unwrap(); + let legacy_hash = stable_hash_json(&legacy).unwrap(); + let tmp = tempfile::tempdir().unwrap(); + let source_dir = tmp.path().join("src"); + std::fs::create_dir(&source_dir).unwrap(); + std::fs::write(source_dir.join("main.cpp"), "void setup() {}\n").unwrap(); + let firmware = tmp.path().join("firmware.elf"); + std::fs::write(&firmware, "previous successful firmware").unwrap(); + let mut contract = FastPathContract::new(tmp.path()); + contract.add_watch_root("project", &source_dir); + contract.add_required_artifacts([firmware]); + let mut persisted = PersistedBuildFingerprint { + version: BUILD_FINGERPRINT_VERSION, + metadata_hash: legacy_hash, + file_set_hash: Some(hash_watch_set_stamps(contract.watches()).unwrap()), + size_info: None, + }; + save_json(contract.fingerprint_path(), &persisted).unwrap(); + let inputs = FastPathCheckInputs { + metadata_hash: ¤t_hash, + extra_artifact_ok: None, + watch_set_cache: None, + compiler_cache: None, + }; + assert!( + fast_path_check(&contract, &inputs).unwrap().is_none(), + "firmware built without SDK platform defaults must be rebuilt" + ); + persisted.metadata_hash = current_hash.clone(); + save_json(contract.fingerprint_path(), &persisted).unwrap(); + assert!( + fast_path_check(&contract, &inputs).unwrap().is_some(), + "unchanged firmware built with the current defaults remains reusable" + ); + } + #[test] fn rp2040_consumes_resolved_framework_toolchain_and_picotool() { let tmp = tempfile::tempdir().unwrap(); @@ -1186,6 +1269,127 @@ mod tests { ); } + #[test] + fn test_rp_platform_sdfat_defaults_without_network_flags() { + let tmp = tempfile::TempDir::new().unwrap(); + std::fs::write( + tmp.path().join("platform.txt"), + "build.sdfatdefines=-DFILE_COPY_CONSTRUCTOR_SELECT=FILE_COPY_CONSTRUCTOR_PUBLIC -DUSE_UTF8_LONG_NAMES=1 -DDISABLE_FS_H_WARNING=1\n", + ) + .unwrap(); + let mut defines = HashMap::new(); + apply_rp_platform_defines(tmp.path(), &None, &mut defines).unwrap(); + assert_eq!( + defines + .get("FILE_COPY_CONSTRUCTOR_SELECT") + .map(String::as_str), + Some("FILE_COPY_CONSTRUCTOR_PUBLIC") + ); + assert_eq!( + defines.get("USE_UTF8_LONG_NAMES").map(String::as_str), + Some("1") + ); + assert_eq!( + defines.get("DISABLE_FS_H_WARNING").map(String::as_str), + Some("1") + ); + } + + #[tokio::test] + #[ignore = "requires Arduino-Pico SDK and GCC in FBUILD_TEST_PICO_FRAMEWORK and FBUILD_TEST_PICO_CXX"] + async fn test_rp_platform_sdfat_compiles_real_sdk_sdfs() { + let framework = fbuild_core::path::NormalizedPath::from( + std::env::var("FBUILD_TEST_PICO_FRAMEWORK").unwrap(), + ) + .into_path_buf(); + let cxx = + fbuild_core::path::NormalizedPath::from(std::env::var("FBUILD_TEST_PICO_CXX").unwrap()) + .into_path_buf(); + for board_id in ["rpipico", "rpipico2"] { + let board = + fbuild_config::BoardConfig::from_board_id(board_id, &HashMap::new()).unwrap(); + let props = crate::arduino_props::load_board_props_with_default_menus( + &framework.join("boards.txt"), + board_id, + ); + let mcu = super::super::mcu_config::get_rp2040_config_for_mcu(&board.mcu).unwrap(); + let mut defines = board.get_defines(); + defines.extend(mcu.defines_map()); + add_rp_manifest_defines(&framework, &board.mcu, &mut defines); + apply_rp_board_props(&props, &framework, &mut defines); + apply_rp_platform_defines(&framework, &props, &mut defines).unwrap(); + let mut includes = vec![ + framework.join("cores/rp2040"), + framework.join("variants").join(&board.variant), + framework.join("include"), + ]; + add_rp_manifest_includes(&framework, &board.mcu, &mut includes); + add_rp_family_includes(&framework.join("include"), &board.mcu, &mut includes); + add_rp_board_includes(&props, &framework, &mut includes); + for library in ["SDFS", "SdFat", "SPI"] { + includes.push(framework.join("libraries").join(library).join("src")); + } + let compiler = ArmCompiler::new( + cxx.clone(), + cxx.clone(), + &board.mcu, + &board.f_cpu, + defines, + includes, + mcu, + fbuild_core::BuildProfile::Quick, + false, + ); + let mut command = vec![cxx.to_string_lossy().into_owned()]; + command.extend( + compiler + .cpp_flags() + .into_iter() + .filter(|flag| flag != "-MMD"), + ); + command.push("-fsyntax-only".into()); + command.push( + framework + .join("libraries/SDFS/src/SDFS.cpp") + .to_string_lossy() + .into_owned(), + ); + let args: Vec<&str> = command.iter().map(String::as_str).collect(); + let output = fbuild_core::subprocess::run_command( + &args, + None, + None, + Some(std::time::Duration::from_secs(60)), + ) + .await + .unwrap(); + assert!(output.success(), "{board_id}: {}", output.stderr); + } + } + + #[test] + fn test_rp_platform_sdfat_board_override_replaces_default_flags() { + let tmp = tempfile::TempDir::new().unwrap(); + std::fs::write( + tmp.path().join("platform.txt"), + "build.sdfatdefines=-DFILE_COPY_CONSTRUCTOR_SELECT=FILE_COPY_CONSTRUCTOR_PUBLIC -DUSE_UTF8_LONG_NAMES=1\n", + ) + .unwrap(); + let board_props = Some(HashMap::from([( + "sdfatdefines".to_string(), + "-DFILE_COPY_CONSTRUCTOR_SELECT=FILE_COPY_CONSTRUCTOR_PRIVATE".to_string(), + )])); + let mut defines = HashMap::new(); + apply_rp_platform_defines(tmp.path(), &board_props, &mut defines).unwrap(); + assert_eq!( + defines + .get("FILE_COPY_CONSTRUCTOR_SELECT") + .map(String::as_str), + Some("FILE_COPY_CONSTRUCTOR_PRIVATE") + ); + assert!(!defines.contains_key("USE_UTF8_LONG_NAMES")); + } + #[test] fn test_apply_rp_platform_net_defines_expands_board_wifi_flags() { let tmp = tempfile::TempDir::new().unwrap(); @@ -1200,7 +1404,7 @@ mod tests { )])); let mut defines = HashMap::new(); - apply_rp_platform_net_defines(tmp.path(), &board_props, &mut defines).unwrap(); + apply_rp_platform_defines(tmp.path(), &board_props, &mut defines).unwrap(); assert_eq!(defines.get("LWIP_IPV6").map(String::as_str), Some("0")); assert_eq!(defines.get("LWIP_IPV4").map(String::as_str), Some("1")); diff --git a/crates/fbuild-build-arm/src/teensy/orchestrator.rs b/crates/fbuild-build-arm/src/teensy/orchestrator.rs index ef95d9f4..4ed4dde0 100644 --- a/crates/fbuild-build-arm/src/teensy/orchestrator.rs +++ b/crates/fbuild-build-arm/src/teensy/orchestrator.rs @@ -53,6 +53,7 @@ struct TeensyFingerprintMetadata { max_flash: Option, max_ram: Option, eh_frame_policy: &'static str, + framework_library_linkage: &'static str, } fn profile_label(profile: fbuild_core::BuildProfile) -> &'static str { @@ -187,6 +188,7 @@ impl BuildOrchestrator for TeensyOrchestrator { crate::eh_frame_policy::EhFramePolicy::Strip => "strip", crate::eh_frame_policy::EhFramePolicy::Preserve => "preserve", }, + framework_library_linkage: "archive", }, &ctx, )?; @@ -290,7 +292,9 @@ impl BuildOrchestrator for TeensyOrchestrator { "Teensy framework library sources added: {}", framework_library_sources.len() ); - sources.core_sources.extend(framework_library_sources); + sources + .core_sources + .extend(framework_library_sources.iter().cloned()); } tracing::info!( @@ -413,7 +417,7 @@ impl BuildOrchestrator for TeensyOrchestrator { }; // 9. Run shared sequential build pipeline - let build_result = pipeline::run_sequential_build_with_libs( + let build_result = pipeline::run_sequential_build_with_optional_framework_libs( &compiler, &linker, ctx, @@ -422,6 +426,7 @@ impl BuildOrchestrator for TeensyOrchestrator { &[], lib_deps_plan, Some(&lib_env), + &framework_library_sources, TargetArchitecture::Arm, "Teensy", start, diff --git a/crates/fbuild-build-engine/src/pipeline/mod.rs b/crates/fbuild-build-engine/src/pipeline/mod.rs index a9ca78dc..3714f358 100644 --- a/crates/fbuild-build-engine/src/pipeline/mod.rs +++ b/crates/fbuild-build-engine/src/pipeline/mod.rs @@ -27,4 +27,6 @@ pub use library::{ }; pub use link::{assemble_build_result, handle_link_result}; pub use project_discovery::{discover_project_includes, is_platform_project, is_project_a_library}; -pub use sequential::run_sequential_build_with_libs; +pub use sequential::{ + run_sequential_build_with_libs, run_sequential_build_with_optional_framework_libs, +}; diff --git a/crates/fbuild-build-engine/src/pipeline/sequential.rs b/crates/fbuild-build-engine/src/pipeline/sequential.rs index 810871c0..ba2ca1bb 100644 --- a/crates/fbuild-build-engine/src/pipeline/sequential.rs +++ b/crates/fbuild-build-engine/src/pipeline/sequential.rs @@ -32,12 +32,47 @@ use super::link::{assemble_build_result, handle_link_result}; /// with the rest of the build. See [`compile_project_as_library`] and /// ISSUES.md Issue 1. #[allow(clippy::too_many_arguments)] +pub async fn run_sequential_build_with_libs( + compiler: &(dyn Compiler + Send + Sync), + linker: &(dyn crate::linker::Linker + Send + Sync), + ctx: BuildContext, + params: &BuildParams, + sources: &SourceCollection, + extra_link_inputs: &[PathBuf], + lib_deps: super::library::LibDeps, + lib_env: Option<&LibraryBuildEnv<'_>>, + arch: TargetArchitecture, + platform_label: &str, + start: Instant, +) -> Result { + run_sequential_build_with_optional_framework_libs( + compiler, + linker, + ctx, + params, + sources, + extra_link_inputs, + lib_deps, + lib_env, + &[], + arch, + platform_label, + start, + ) + .await +} + +/// Compile all sources normally, but archive optional framework library objects +/// so unreferenced library constructors cannot become firmware startup roots. +/// `optional_framework_sources` must be a subset of the core/variant sources. +/// The library environment supplies the platform's LTO-aware archiver. +#[allow(clippy::too_many_arguments)] #[expect( clippy::cognitive_complexity, clippy::too_many_lines, reason = "baseline, zackees/ci.yml#229" )] -pub async fn run_sequential_build_with_libs( +pub async fn run_sequential_build_with_optional_framework_libs( compiler: &(dyn Compiler + Send + Sync), linker: &(dyn crate::linker::Linker + Send + Sync), mut ctx: BuildContext, @@ -46,6 +81,7 @@ pub async fn run_sequential_build_with_libs( extra_link_inputs: &[PathBuf], lib_deps: super::library::LibDeps, lib_env: Option<&LibraryBuildEnv<'_>>, + optional_framework_sources: &[PathBuf], arch: TargetArchitecture, platform_label: &str, start: Instant, @@ -362,10 +398,26 @@ pub async fn run_sequential_build_with_libs( // GCC accepts .a in the same positional slot as .o files. core_objects.push(archive); } + if !optional_framework_sources.is_empty() { + let env = lib_env.ok_or_else(|| { + fbuild_core::FbuildError::BuildFailed( + "optional framework libraries require an LTO-aware library build environment" + .into(), + ) + })?; + core_objects = archive_optional_framework_objects( + core_objects, + optional_framework_sources, + &ctx.core_build_dir, + &ctx.build_dir, + env.ar_path, + ) + .await?; + } let link_result = { // FastLED/fbuild#1465: `link` covers `Linker::link_all` end to end - // (link + objcopy + size). No separate archive step exists: core - // objects are passed to the linker directly for LTO compatibility. + // (link + objcopy + size). Mandatory core objects remain direct; + // the optional framework archive has already been prepared above. let _g = perf.phase("link"); crate::linker::Linker::link_all( linker, @@ -436,6 +488,42 @@ pub async fn run_sequential_build_with_libs( )) } +/// Preserve mandatory core startup objects, and place optional SDK objects in +/// one archive after every consumer. GNU archive extraction resolves dependencies +/// among its members without imposing the scanner's library discovery order. +async fn archive_optional_framework_objects( + inputs: Vec, + optional_sources: &[PathBuf], + core_build_dir: &Path, + build_dir: &Path, + ar_path: &Path, +) -> Result> { + let optional: std::collections::HashSet = optional_sources + .iter() + .map(|source| crate::compiler::CompilerBase::object_path(source, core_build_dir)) + .collect(); + let (objects, mut inputs): (Vec<_>, Vec<_>) = inputs + .into_iter() + .partition(|input| optional.contains(input)); + if objects.len() != optional.len() { + return Err(fbuild_core::FbuildError::BuildFailed( + "optional framework library object missing from compiled link inputs".into(), + )); + } + if !objects.is_empty() { + let archive = build_dir.join("libFrameworkLibraries.a"); + crate::linker::LinkerBase::archive_if_stale( + ar_path, + &objects, + &archive, + "framework library archiver", + ) + .await?; + inputs.push(archive); + } + Ok(inputs) +} + /// Await `future`, returning its value with the wall time it took. async fn timed( future: impl std::future::Future>, @@ -461,3 +549,94 @@ fn local_lib_dir_names(project_dir: &Path) -> std::collections::HashSet } names } + +#[cfg(test)] +mod optional_framework_tests { + use super::*; + use crate::compiler::CompilerBase; + use fbuild_core::subprocess::run_command; + + async fn run(args: &[String]) -> String { + let args: Vec<&str> = args.iter().map(String::as_str).collect(); + let result = run_command(&args, None, None, Some(std::time::Duration::from_secs(30))) + .await + .unwrap(); + assert!(result.success(), "{args:?}: {}", result.stderr); + result.stdout + } + + async fn gcc_archiver(cc: &str) -> PathBuf { + // Compiler wrappers need not expose gcc-ar on PATH. Ask GCC for its + // actual frontend, then use the companion archiver in that toolchain. + let frontend = run(&[cc.into(), "-print-prog-name=cc1".into()]).await; + let frontend = Path::new(frontend.trim()); + if frontend.is_absolute() { + let name = fbuild_core::platform::executable::native_name("gcc-ar"); + for ancestor in frontend.ancestors().skip(1) { + let candidate = ancestor.join("bin").join(&name); + if candidate.is_file() { + return candidate; + } + } + } + let reported = run(&[cc.into(), "-print-prog-name=gcc-ar".into()]).await; + PathBuf::from(reported.trim()) + } + + #[tokio::test] + #[ignore = "requires native GCC and matching gcc-ar; explicitly run in Check Ubuntu CI"] + async fn optional_framework_archive_preserves_only_referenced_initializers() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + let cc = std::env::var("FBUILD_TEST_CC").unwrap_or_else(|_| "gcc".into()); + let ar = gcc_archiver(&cc).await; + let core = root.join("core.c"); + let optional = root.join("optional.c"); + let dependency = root.join("dependency.c"); + std::fs::write( + &core, + "int state; __attribute__((constructor)) void core_init(void) { state |= 1; }", + ) + .unwrap(); + std::fs::write(&optional, "extern int state; extern int dependency(void); __attribute__((constructor)) void optional_init(void) { state |= 2; } int optional(void) { return dependency(); }").unwrap(); + std::fs::write(&dependency, "int dependency(void) { return 7; }").unwrap(); + let sources = [&core, &optional, &dependency]; + let mut objects = Vec::new(); + for source in sources { + let object = CompilerBase::object_path(source, root); + run(&[ + cc.clone(), + "-flto".into(), + "-c".into(), + source.display().to_string(), + "-o".into(), + object.display().to_string(), + ]) + .await; + objects.push(object); + } + let inputs = + archive_optional_framework_objects(objects, &[optional, dependency], root, root, &ar) + .await + .unwrap(); + for (name, body) in [ + ( + "unused", + "extern int state; int main(void) { return state != 1; }", + ), + ( + "used", + "extern int state; extern int optional(void); int main(void) { return state != 3 || optional() != 7; }", + ), + ] { + let main = root.join(format!("{name}.c")); + let exe = root.join(format!("{name}{}", std::env::consts::EXE_SUFFIX)); + std::fs::write(&main, body).unwrap(); + let mut args = vec![cc.clone(), "-flto".into(), main.display().to_string()]; + args.extend(inputs.iter().map(|path| path.display().to_string())); + args.extend(["-o".into(), exe.display().to_string()]); + run(&args).await; + run(&[exe.display().to_string()]).await; + } + } +} diff --git a/pyproject.toml b/pyproject.toml index 5cd87979..b4f5e5f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fbuild" -version = "2.5.40" +version = "2.5.41" description = "PlatformIO-compatible embedded build tool (Rust implementation)" readme = "README.md" license = "AGPL-3.0-only" From 345d49dccca3d485347c95d01bbb190f25f7e039 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Tue, 13 Oct 2026 07:40:20 -0700 Subject: [PATCH 2/5] fix: apply optional SDK archive extraction to Pico builds --- .../src/rp2040/orchestrator.rs | 30 +++++++++++++------ local-gate.toml | 2 +- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/crates/fbuild-build-arm/src/rp2040/orchestrator.rs b/crates/fbuild-build-arm/src/rp2040/orchestrator.rs index 61d00f95..15e82a0b 100644 --- a/crates/fbuild-build-arm/src/rp2040/orchestrator.rs +++ b/crates/fbuild-build-arm/src/rp2040/orchestrator.rs @@ -43,6 +43,7 @@ struct Rp2040FingerprintMetadata { version: u32, // Invalidate firmware produced before SDK platform.txt defaults were applied. platform_define_policy: &'static str, + framework_library_linkage: &'static str, env_name: String, profile: String, board_name: String, @@ -204,6 +205,7 @@ impl BuildOrchestrator for Rp2040Orchestrator { &Rp2040FingerprintMetadata { version: BUILD_FINGERPRINT_VERSION, platform_define_policy: "platform-sdfatdefaults-v1", + framework_library_linkage: "archive", env_name: params.env_name.clone(), profile: profile_label(params.profile).to_string(), board_name: ctx.board.name.clone(), @@ -355,7 +357,9 @@ impl BuildOrchestrator for Rp2040Orchestrator { "RP2040 framework library sources added: {}", framework_library_sources.len() ); - sources.core_sources.extend(framework_library_sources); + sources + .core_sources + .extend(framework_library_sources.iter().cloned()); } // Use the resolved core_dir/variant_dir instead of board.get_include_paths(): // RP2040 board metadata reports `core = earlephilhower`, while the actual @@ -524,7 +528,7 @@ impl BuildOrchestrator for Rp2040Orchestrator { // 9. Run shared sequential build pipeline let board_mcu = ctx.board.mcu.clone(); - let mut build_result = pipeline::run_sequential_build_with_libs( + let mut build_result = pipeline::run_sequential_build_with_optional_framework_libs( &compiler, &linker, ctx, @@ -533,6 +537,7 @@ impl BuildOrchestrator for Rp2040Orchestrator { &support_link_inputs, lib_deps_plan, Some(&lib_env), + &framework_library_sources, TargetArchitecture::Arm, "RP2040", start, @@ -1054,6 +1059,15 @@ mod tests { #[test] fn platform_default_policy_invalidates_legacy_firmware_cache() { + assert_policy_invalidates_legacy_firmware_cache("platform_define_policy"); + } + + #[test] + fn framework_archive_policy_invalidates_legacy_firmware_cache() { + assert_policy_invalidates_legacy_firmware_cache("framework_library_linkage"); + } + + fn assert_policy_invalidates_legacy_firmware_cache(policy: &str) { use crate::build_fingerprint::{ PersistedBuildFingerprint, fast_path_check, hash_watch_set_stamps, save_json, stable_hash_json, @@ -1062,6 +1076,7 @@ mod tests { let metadata = Rp2040FingerprintMetadata { version: BUILD_FINGERPRINT_VERSION, platform_define_policy: "platform-sdfatdefaults-v1", + framework_library_linkage: "archive", env_name: "pico".into(), profile: "release".into(), board_name: "Pico".into(), @@ -1080,11 +1095,8 @@ mod tests { }; let current = serde_json::to_value(&metadata).unwrap(); let mut legacy = current.clone(); - // Successful builds before the SDK-default fix did not encode this policy. - legacy - .as_object_mut() - .unwrap() - .remove("platform_define_policy"); + // Successful builds before the policy change did not encode this field. + legacy.as_object_mut().unwrap().remove(policy); let current_hash = stable_hash_json(¤t).unwrap(); let legacy_hash = stable_hash_json(&legacy).unwrap(); let tmp = tempfile::tempdir().unwrap(); @@ -1111,13 +1123,13 @@ mod tests { }; assert!( fast_path_check(&contract, &inputs).unwrap().is_none(), - "firmware built without SDK platform defaults must be rebuilt" + "firmware built without current {policy} must be rebuilt" ); persisted.metadata_hash = current_hash.clone(); save_json(contract.fingerprint_path(), &persisted).unwrap(); assert!( fast_path_check(&contract, &inputs).unwrap().is_some(), - "unchanged firmware built with the current defaults remains reusable" + "unchanged firmware built with current {policy} remains reusable" ); } diff --git a/local-gate.toml b/local-gate.toml index 6ff84794..57bbd846 100644 --- a/local-gate.toml +++ b/local-gate.toml @@ -18,7 +18,7 @@ mode = "minimal" [[gate.replay.jobs]] source-job = "check-ubuntu.yml:check" key = "linux/Check Ubuntu/Check (ubuntu-latest)" -steps = ["Checkout source", "Verify candidate SHA", "Set up uv", "Select workspace test interpreter", "Setup soldr", "Clippy", "Check tracked Python complexity ratchet", "Lint subprocess spawns", "Guard production USB VID/PID catalogue literals", "Test"] +steps = ["Checkout source", "Verify candidate SHA", "Set up uv", "Select workspace test interpreter", "Setup soldr", "Clippy", "Check tracked Python complexity ratchet", "Lint subprocess spawns", "Guard production USB VID/PID catalogue literals", "Test", "Test optional SDK library linkage with native GCC LTO"] lanes = ["linux-minimal"] [[gate.replay.jobs]] From f27d10de366b43007c9a0c121c04c64ca6b452f5 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Tue, 13 Oct 2026 07:55:15 -0700 Subject: [PATCH 3/5] fix: preserve library extraction and provision Windows source esptool Local-Gate: v1 tree=ca323e0a2653fd885a7a06dc0d9c9ddc1c7dfacf secs=949 lanes=linux-minimal:run,dylint:run Ci-Attestation: {"at":1791904377,"gate":"general/all/ubuntu-ci-guards","host":"linux-x86_64","key":"c5e6bc9b31f5592c750167fed494255bd18c58f4a28914e0930b77a4a9afbb44","lane":"linux-minimal","parents":["345d49dccca3d485347c95d01bbb190f25f7e039"],"secs":597,"stamp":"9b7b25e775998ba482f6cc4e0144b2e3","tree":"ca323e0a2653fd885a7a06dc0d9c9ddc1c7dfacf","v":1,"via":"run"} Ci-Attestation: {"at":1791904377,"gate":"rust/x86_64-unknown-linux-gnu/workspace-clippy","host":"linux-x86_64","key":"c5e6bc9b31f5592c750167fed494255bd18c58f4a28914e0930b77a4a9afbb44","lane":"linux-minimal","parents":["345d49dccca3d485347c95d01bbb190f25f7e039"],"secs":597,"stamp":"773730e19235ae1a08fa26f97934ea5b","tree":"ca323e0a2653fd885a7a06dc0d9c9ddc1c7dfacf","v":1,"via":"run"} Ci-Attestation: {"at":1791904377,"gate":"rust/x86_64-unknown-linux-gnu/workspace-test","host":"linux-x86_64","key":"c5e6bc9b31f5592c750167fed494255bd18c58f4a28914e0930b77a4a9afbb44","lane":"linux-minimal","parents":["345d49dccca3d485347c95d01bbb190f25f7e039"],"secs":597,"stamp":"cd6867ce2ab118fa5658beb45bea3288","tree":"ca323e0a2653fd885a7a06dc0d9c9ddc1c7dfacf","v":1,"via":"run"} Ci-Attestation: {"at":1791904377,"gate":"rust/x86_64-unknown-linux-gnu/python-facade-test","host":"linux-x86_64","key":"c5e6bc9b31f5592c750167fed494255bd18c58f4a28914e0930b77a4a9afbb44","lane":"linux-minimal","parents":["345d49dccca3d485347c95d01bbb190f25f7e039"],"secs":597,"stamp":"356619d0cec622152bc1b226829444ab","tree":"ca323e0a2653fd885a7a06dc0d9c9ddc1c7dfacf","v":1,"via":"run"} Ci-Attestation: {"at":1791904377,"gate":"general/all/dylint-policy","host":"linux-x86_64","key":"e2ae50b5b3ac7d17c39446d89a31f66c78c5c1cd8ba773326e74306888b75214","lane":"dylint","parents":["345d49dccca3d485347c95d01bbb190f25f7e039"],"secs":352,"stamp":"dce19de802be4a77620276744c5c84ca","tree":"ca323e0a2653fd885a7a06dc0d9c9ddc1c7dfacf","v":1,"via":"run"} Ci-Attestation: {"at":1791904377,"gate":"rust/x86_64-unknown-linux-gnu/dylint-library-check","host":"linux-x86_64","key":"e2ae50b5b3ac7d17c39446d89a31f66c78c5c1cd8ba773326e74306888b75214","lane":"dylint","parents":["345d49dccca3d485347c95d01bbb190f25f7e039"],"secs":352,"stamp":"a3d3fecdd9671487ce634b773c8bd9cd","tree":"ca323e0a2653fd885a7a06dc0d9c9ddc1c7dfacf","v":1,"via":"run"} Ci-Attestation: {"at":1791904377,"gate":"rust/x86_64-unknown-linux-gnu/workspace-dylint","host":"linux-x86_64","key":"e2ae50b5b3ac7d17c39446d89a31f66c78c5c1cd8ba773326e74306888b75214","lane":"dylint","parents":["345d49dccca3d485347c95d01bbb190f25f7e039"],"secs":352,"stamp":"95d369d61a9b7b229b42b6a012718ece","tree":"ca323e0a2653fd885a7a06dc0d9c9ddc1c7dfacf","v":1,"via":"run"} --- .github/workflows/check-ubuntu.yml | 4 +- .github/workflows/check-windows.yml | 49 ++++++++++ ci/test_fractional_workflows.py | 25 +++++ .../src/rp2040/orchestrator.rs | 26 ++++-- .../src/teensy/orchestrator.rs | 4 +- .../src/teensy/teensy_linker.rs | 91 +++++++++++++++++++ .../src/pipeline/sequential.rs | 83 +++++++++++------ crates/fbuild-library/src/library/esptool.rs | 45 +++++++-- .../src/library/esptool_tests.rs | 59 +++++++++++- 9 files changed, 337 insertions(+), 49 deletions(-) diff --git a/.github/workflows/check-ubuntu.yml b/.github/workflows/check-ubuntu.yml index bce1964b..182115af 100644 --- a/.github/workflows/check-ubuntu.yml +++ b/.github/workflows/check-ubuntu.yml @@ -104,7 +104,9 @@ jobs: id: tests run: uv run --no-project python ci/test_cache_status.py -- soldr cargo test --workspace - name: Test optional SDK library linkage with native GCC LTO - run: soldr cargo test -p fbuild-build-engine --lib optional_framework_archive_preserves_only_referenced_initializers -- --ignored + run: | + soldr cargo test -p fbuild-build-engine --lib optional_framework_archive_preserves_only_referenced_initializers -- --ignored + soldr cargo test -p fbuild-build-arm --lib optional_library_rescans_earlier_dependency_archive -- --ignored python-facade-tests: # FastLED/fbuild#1485 §5.2/§6: the embedded-CPython AT-P integration diff --git a/.github/workflows/check-windows.yml b/.github/workflows/check-windows.yml index 5a2aff45..c4d7871b 100644 --- a/.github/workflows/check-windows.yml +++ b/.github/workflows/check-windows.yml @@ -118,3 +118,52 @@ jobs: throw "Warm check did not reuse the verified source launcher" } # The hosted runner discards RUNNER_TEMP after the job. + + # Pioarduino 55.03.35 pins esptool 5.1.0, whose release has no + # Windows standalone binary. Verify the real source-package route. + - name: Install and recheck ESP32-C6 source tools + id: modern-esptool + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $probe = Join-Path $env:RUNNER_TEMP ("fbuild-modern-esptool-" + [guid]::NewGuid()) + New-Item -ItemType Directory -Path $probe | Out-Null + $env:FBUILD_CACHE_DIR = Join-Path $probe "cache" + [Environment]::SetEnvironmentVariable("FBUILD_ESPTOOL_PATH", $null, "Process") + @" + [env:esp32c6-idf5] + platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.35/platform-espressif32.zip + board = esp32-c6-devkitc-1 + framework = arduino + "@ | Set-Content -LiteralPath (Join-Path $probe "platformio.ini") -Encoding utf8 + + $coldJson = & soldr cargo run -p fbuild-cli --bin fbuild -- install "$probe" -e esp32c6-idf5 --json + if ($LASTEXITCODE -ne 0) { throw "Cold ESP32-C6 install failed ($LASTEXITCODE)" } + $coldJson | Write-Output + $cold = ($coldJson -join "`n") | ConvertFrom-Json + $coldTools = @($cold.environments.packages | Where-Object name -eq "tool-esptoolpy") + if ($coldTools.Count -ne 1) { throw "Expected exactly one esptool package" } + $coldTool = $coldTools[0] + if ($coldTool.version -ne "5.1.0" -or $coldTool.status -ne "fetched" -or + $coldTool.url -ne "https://github.com/pioarduino/esptool/releases/download/v5.1.0/esptool.zip") { + throw "Expected a cold install of the pinned esptool 5.1.0 source archive" + } + if (-not (Test-Path -LiteralPath $coldTool.install_path -PathType Leaf)) { + throw "Installed esptool launcher is missing" + } + if ($coldTool.install_path -notmatch '[\\/]\.fbuild-tools[\\/]esptool[\\/]Scripts[\\/]esptool\.exe$') { + throw "Expected the real uv-managed Windows esptool launcher" + } + + $warmJson = & soldr cargo run -p fbuild-cli --bin fbuild -- install "$probe" -e esp32c6-idf5 --check --json + if ($LASTEXITCODE -ne 0) { throw "Warm ESP32-C6 check failed ($LASTEXITCODE)" } + $warmJson | Write-Output + $warm = ($warmJson -join "`n") | ConvertFrom-Json + $warmTools = @($warm.environments.packages | Where-Object name -eq "tool-esptoolpy") + if ($warmTools.Count -ne 1) { throw "Expected exactly one cached esptool package" } + $warmTool = $warmTools[0] + if ($warmTool.version -ne "5.1.0" -or $warmTool.status -ne "present" -or + $warmTool.url -ne $coldTool.url -or $warmTool.install_path -ne $coldTool.install_path) { + throw "Warm check did not reuse the verified source launcher" + } + # The hosted runner discards RUNNER_TEMP after the job. diff --git a/ci/test_fractional_workflows.py b/ci/test_fractional_workflows.py index f2f777cb..a80066e9 100644 --- a/ci/test_fractional_workflows.py +++ b/ci/test_fractional_workflows.py @@ -13,6 +13,31 @@ class FractionalWorkflowTests(unittest.TestCase): + def test_windows_checks_real_modern_esptool_source_install(self): + job = self.load("check-windows.yml")["jobs"]["check"] + smoke = next(step for step in job["steps"] if step.get("id") == "modern-esptool") + self.assertEqual("windows-latest", job["runs-on"]) + self.assertEqual("pwsh", smoke["shell"]) + self.assertNotIn("if", smoke) + self.assertFalse(smoke.get("continue-on-error", False)) + script = smoke["run"] + for contract in ( + "$env:RUNNER_TEMP", "[guid]::NewGuid()", "$env:FBUILD_CACHE_DIR", + 'SetEnvironmentVariable("FBUILD_ESPTOOL_PATH", $null, "Process")', + "https://github.com/pioarduino/platform-espressif32/releases/download/55.03.35/platform-espressif32.zip", + "board = esp32-c6-devkitc-1", "framework = arduino", + "soldr cargo run -p fbuild-cli --bin fbuild -- install", + "--check --json", "$LASTEXITCODE -ne 0", "ConvertFrom-Json", + 'name -eq "tool-esptoolpy"', 'version -ne "5.1.0"', + 'status -ne "fetched"', 'status -ne "present"', + "https://github.com/pioarduino/esptool/releases/download/v5.1.0/esptool.zip", + 'Test-Path -LiteralPath $coldTool.install_path -PathType Leaf', + '$warmTool.install_path -ne $coldTool.install_path', + ): + self.assertIn(contract, script) + self.assertEqual(2, script.count("soldr cargo run -p fbuild-cli --bin fbuild -- install")) + self.assertEqual(2, script.count("$LASTEXITCODE -ne 0")) + def test_macos_lipo_places_input_before_variadic_architectures(self): job = self.load("release-auto.yml")["jobs"]["macos-release-smoke"] script = next(step["run"] for step in job["steps"] if "lipo " in step.get("run", "")) diff --git a/crates/fbuild-build-arm/src/rp2040/orchestrator.rs b/crates/fbuild-build-arm/src/rp2040/orchestrator.rs index 15e82a0b..9bcead3e 100644 --- a/crates/fbuild-build-arm/src/rp2040/orchestrator.rs +++ b/crates/fbuild-build-arm/src/rp2040/orchestrator.rs @@ -205,7 +205,7 @@ impl BuildOrchestrator for Rp2040Orchestrator { &Rp2040FingerprintMetadata { version: BUILD_FINGERPRINT_VERSION, platform_define_policy: "platform-sdfatdefaults-v1", - framework_library_linkage: "archive", + framework_library_linkage: "local-and-framework-archive-v2", env_name: params.env_name.clone(), profile: profile_label(params.profile).to_string(), board_name: ctx.board.name.clone(), @@ -537,7 +537,7 @@ impl BuildOrchestrator for Rp2040Orchestrator { &support_link_inputs, lib_deps_plan, Some(&lib_env), - &framework_library_sources, + Some(&framework_library_sources), TargetArchitecture::Arm, "RP2040", start, @@ -1059,15 +1059,23 @@ mod tests { #[test] fn platform_default_policy_invalidates_legacy_firmware_cache() { - assert_policy_invalidates_legacy_firmware_cache("platform_define_policy"); + assert_policy_invalidates_legacy_firmware_cache("platform_define_policy", None); } #[test] fn framework_archive_policy_invalidates_legacy_firmware_cache() { - assert_policy_invalidates_legacy_firmware_cache("framework_library_linkage"); + assert_policy_invalidates_legacy_firmware_cache("framework_library_linkage", None); } - fn assert_policy_invalidates_legacy_firmware_cache(policy: &str) { + #[test] + fn local_archive_policy_invalidates_sdk_only_firmware_cache() { + assert_policy_invalidates_legacy_firmware_cache( + "framework_library_linkage", + Some("archive"), + ); + } + + fn assert_policy_invalidates_legacy_firmware_cache(policy: &str, previous_value: Option<&str>) { use crate::build_fingerprint::{ PersistedBuildFingerprint, fast_path_check, hash_watch_set_stamps, save_json, stable_hash_json, @@ -1076,7 +1084,7 @@ mod tests { let metadata = Rp2040FingerprintMetadata { version: BUILD_FINGERPRINT_VERSION, platform_define_policy: "platform-sdfatdefaults-v1", - framework_library_linkage: "archive", + framework_library_linkage: "local-and-framework-archive-v2", env_name: "pico".into(), profile: "release".into(), board_name: "Pico".into(), @@ -1097,6 +1105,12 @@ mod tests { let mut legacy = current.clone(); // Successful builds before the policy change did not encode this field. legacy.as_object_mut().unwrap().remove(policy); + if let Some(value) = previous_value { + legacy + .as_object_mut() + .unwrap() + .insert(policy.into(), value.into()); + } let current_hash = stable_hash_json(¤t).unwrap(); let legacy_hash = stable_hash_json(&legacy).unwrap(); let tmp = tempfile::tempdir().unwrap(); diff --git a/crates/fbuild-build-arm/src/teensy/orchestrator.rs b/crates/fbuild-build-arm/src/teensy/orchestrator.rs index 4ed4dde0..68887527 100644 --- a/crates/fbuild-build-arm/src/teensy/orchestrator.rs +++ b/crates/fbuild-build-arm/src/teensy/orchestrator.rs @@ -188,7 +188,7 @@ impl BuildOrchestrator for TeensyOrchestrator { crate::eh_frame_policy::EhFramePolicy::Strip => "strip", crate::eh_frame_policy::EhFramePolicy::Preserve => "preserve", }, - framework_library_linkage: "archive", + framework_library_linkage: "local-and-framework-archive-v2", }, &ctx, )?; @@ -426,7 +426,7 @@ impl BuildOrchestrator for TeensyOrchestrator { &[], lib_deps_plan, Some(&lib_env), - &framework_library_sources, + Some(&framework_library_sources), TargetArchitecture::Arm, "Teensy", start, diff --git a/crates/fbuild-build-arm/src/teensy/teensy_linker.rs b/crates/fbuild-build-arm/src/teensy/teensy_linker.rs index d890b853..6c92e18f 100644 --- a/crates/fbuild-build-arm/src/teensy/teensy_linker.rs +++ b/crates/fbuild-build-arm/src/teensy/teensy_linker.rs @@ -98,6 +98,9 @@ impl TeensyLinker { args.push(obj.to_string_lossy().to_string()); } + // Rescan ordinary archives: a local/SDK member can reference an earlier + // lib_deps/project archive. This does not retain unreferenced members. + args.push("-Wl,--start-group".into()); // Core objects passed directly (not archived) for LTO compatibility for archive in archives { args.push(archive.to_string_lossy().to_string()); @@ -115,6 +118,7 @@ impl TeensyLinker { args.push(format!("-l{}", lib)); } args.extend(extra.libs.iter().cloned()); + args.push("-Wl,--end-group".into()); args } @@ -266,6 +270,93 @@ mod tests { use super::*; use crate::teensy::mcu_config::get_teensy_config; + async fn run_native(args: &[String]) { + let borrowed: Vec<_> = args.iter().map(String::as_str).collect(); + let result = run_command( + &borrowed, + None, + None, + Some(std::time::Duration::from_secs(30)), + ) + .await + .unwrap(); + assert!(result.success(), "{args:?}: {}", result.stderr); + } + + #[tokio::test] + #[ignore = "requires native GCC and GNU ar; explicitly run in Check Ubuntu CI"] + async fn optional_library_rescans_earlier_dependency_archive() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + let fixtures = [ + ( + "main", + "int state; extern int optional(void); int main(void) { return state != 0 || optional() != 7; }", + ), + ( + "optional", + "extern int dependency(void); int optional(void) { return dependency(); }", + ), + ("dependency", "int dependency(void) { return 7; }"), + ( + "unused", + "extern int state; __attribute__((constructor)) void unused(void) { state = 1; }", + ), + ]; + let mut objects = Vec::new(); + for (name, body) in fixtures { + let source = root.join(format!("{name}.c")); + let object = root.join(format!("{name}.o")); + std::fs::write(&source, body).unwrap(); + run_native(&[ + "gcc".into(), + "-ffunction-sections".into(), + "-c".into(), + source.display().to_string(), + "-o".into(), + object.display().to_string(), + ]) + .await; + objects.push(object); + } + let earlier = root.join("libDeps.a"); + let optional = root.join("libOptional.a"); + crate::linker::LinkerBase::archive(Path::new("ar"), &objects[2..], &earlier, "ar") + .await + .unwrap(); + crate::linker::LinkerBase::archive(Path::new("ar"), &objects[1..2], &optional, "ar") + .await + .unwrap(); + // Use the actual Teensy command builder with only its target-specific + // flags removed so the host GNU linker can execute this fixture. + let mut config = get_teensy_config().unwrap(); + config.linker_flags = vec!["-Wl,--gc-sections".into()]; + config.linker_libs.clear(); + config.profiles.clear(); + let linker = TeensyLinker::new( + "gcc".into(), + "ar".into(), + PathBuf::new(), + PathBuf::new(), + LinkerScripts::new(), + config, + BuildProfile::Release, + None, + None, + None, + false, + ); + let executable = root.join("firmware"); + run_native(&linker.build_link_args( + &objects[..1], + &[earlier, optional], + &executable, + &LinkExtraArgs::default(), + )) + .await; + run_native(&[executable.display().to_string()]).await; + } + /// The link must not run in the daemon's inherited cwd: collect2 / /// lto-wrapper write scratch files relative to it, which put a stray /// 0-byte `-r` in the user's repo root on every clean build. diff --git a/crates/fbuild-build-engine/src/pipeline/sequential.rs b/crates/fbuild-build-engine/src/pipeline/sequential.rs index ba2ca1bb..c27bffca 100644 --- a/crates/fbuild-build-engine/src/pipeline/sequential.rs +++ b/crates/fbuild-build-engine/src/pipeline/sequential.rs @@ -54,7 +54,7 @@ pub async fn run_sequential_build_with_libs( extra_link_inputs, lib_deps, lib_env, - &[], + None, arch, platform_label, start, @@ -62,9 +62,10 @@ pub async fn run_sequential_build_with_libs( .await } -/// Compile all sources normally, but archive optional framework library objects -/// so unreferenced library constructors cannot become firmware startup roots. -/// `optional_framework_sources` must be a subset of the core/variant sources. +/// With `Some`, archive selected local and framework library objects together +/// so unused local code cannot extract SDK startup constructors. With `None`, +/// retain the legacy direct-object linkage. The framework source slice must be +/// a subset of the core/variant sources; `Some(&[])` still archives local libs. /// The library environment supplies the platform's LTO-aware archiver. #[allow(clippy::too_many_arguments)] #[expect( @@ -81,7 +82,7 @@ pub async fn run_sequential_build_with_optional_framework_libs( extra_link_inputs: &[PathBuf], lib_deps: super::library::LibDeps, lib_env: Option<&LibraryBuildEnv<'_>>, - optional_framework_sources: &[PathBuf], + optional_framework_sources: Option<&[PathBuf]>, arch: TargetArchitecture, platform_label: &str, start: Instant, @@ -290,7 +291,7 @@ pub async fn run_sequential_build_with_optional_framework_libs( &build_log_mutex, )); - // Compile local libraries (lib/* — loose objects, LTO-safe; per-lib parallel) + // Compile local libraries normally; opted-in platforms archive them below. let declared_lib_deps = ctx .config .get_lib_deps(¶ms.env_name) @@ -391,6 +392,13 @@ pub async fn run_sequential_build_with_optional_framework_libs( // Link crate::build_output::log_linking(&mut ctx.build_log, "Linking firmware.elf"); + let optional_objects: Option> = optional_framework_sources.map(|sources| { + sources + .iter() + .map(|source| crate::compiler::CompilerBase::object_path(source, &ctx.core_build_dir)) + .chain(library_objects.iter().cloned()) + .collect() + }); core_objects.extend(library_objects); core_objects.extend(extra_link_inputs.iter().cloned()); core_objects.extend(lib_deps_archives); @@ -398,17 +406,16 @@ pub async fn run_sequential_build_with_optional_framework_libs( // GCC accepts .a in the same positional slot as .o files. core_objects.push(archive); } - if !optional_framework_sources.is_empty() { + if let Some(optional_objects) = optional_objects.filter(|objects| !objects.is_empty()) { let env = lib_env.ok_or_else(|| { fbuild_core::FbuildError::BuildFailed( "optional framework libraries require an LTO-aware library build environment" .into(), ) })?; - core_objects = archive_optional_framework_objects( + core_objects = archive_optional_library_objects( core_objects, - optional_framework_sources, - &ctx.core_build_dir, + &optional_objects, &ctx.build_dir, env.ar_path, ) @@ -417,7 +424,7 @@ pub async fn run_sequential_build_with_optional_framework_libs( let link_result = { // FastLED/fbuild#1465: `link` covers `Linker::link_all` end to end // (link + objcopy + size). Mandatory core objects remain direct; - // the optional framework archive has already been prepared above. + // the optional library archive has already been prepared above. let _g = perf.phase("link"); crate::linker::Linker::link_all( linker, @@ -488,35 +495,31 @@ pub async fn run_sequential_build_with_optional_framework_libs( )) } -/// Preserve mandatory core startup objects, and place optional SDK objects in +/// Preserve mandatory core startup objects, and place local and SDK objects in /// one archive after every consumer. GNU archive extraction resolves dependencies /// among its members without imposing the scanner's library discovery order. -async fn archive_optional_framework_objects( +async fn archive_optional_library_objects( inputs: Vec, - optional_sources: &[PathBuf], - core_build_dir: &Path, + optional_objects: &[PathBuf], build_dir: &Path, ar_path: &Path, ) -> Result> { - let optional: std::collections::HashSet = optional_sources - .iter() - .map(|source| crate::compiler::CompilerBase::object_path(source, core_build_dir)) - .collect(); + let optional: std::collections::HashSet<&PathBuf> = optional_objects.iter().collect(); let (objects, mut inputs): (Vec<_>, Vec<_>) = inputs .into_iter() .partition(|input| optional.contains(input)); if objects.len() != optional.len() { return Err(fbuild_core::FbuildError::BuildFailed( - "optional framework library object missing from compiled link inputs".into(), + "optional library object missing from compiled link inputs".into(), )); } if !objects.is_empty() { - let archive = build_dir.join("libFrameworkLibraries.a"); + let archive = build_dir.join("libOptionalLibraries.a"); crate::linker::LinkerBase::archive_if_stale( ar_path, &objects, &archive, - "framework library archiver", + "optional library archiver", ) .await?; inputs.push(archive); @@ -593,14 +596,31 @@ mod optional_framework_tests { let core = root.join("core.c"); let optional = root.join("optional.c"); let dependency = root.join("dependency.c"); + let local_used = root.join("local_used.c"); + let local_sd = root.join("local_sd.c"); + let local_callback = root.join("local_callback.c"); std::fs::write( &core, "int state; __attribute__((constructor)) void core_init(void) { state |= 1; }", ) .unwrap(); - std::fs::write(&optional, "extern int state; extern int dependency(void); __attribute__((constructor)) void optional_init(void) { state |= 2; } int optional(void) { return dependency(); }").unwrap(); + std::fs::write(&optional, "extern int state; extern int dependency(void); extern int local_callback(void); __attribute__((constructor)) void optional_init(void) { state |= 2; } int optional(void) { return dependency() + local_callback() - 13; }").unwrap(); std::fs::write(&dependency, "int dependency(void) { return 7; }").unwrap(); - let sources = [&core, &optional, &dependency]; + std::fs::write(&local_used, "int local_value(void) { return 11; }").unwrap(); + std::fs::write( + &local_sd, + "extern int optional(void); int local_sd(void) { return optional(); }", + ) + .unwrap(); + std::fs::write(&local_callback, "int local_callback(void) { return 13; }").unwrap(); + let sources = [ + &core, + &optional, + &dependency, + &local_used, + &local_sd, + &local_callback, + ]; let mut objects = Vec::new(); for source in sources { let object = CompilerBase::object_path(source, root); @@ -615,18 +635,21 @@ mod optional_framework_tests { .await; objects.push(object); } - let inputs = - archive_optional_framework_objects(objects, &[optional, dependency], root, root, &ar) - .await - .unwrap(); + let optional_objects: Vec<_> = [optional, dependency, local_used, local_sd, local_callback] + .iter() + .map(|source| CompilerBase::object_path(source, root)) + .collect(); + let inputs = archive_optional_library_objects(objects, &optional_objects, root, &ar) + .await + .unwrap(); for (name, body) in [ ( "unused", - "extern int state; int main(void) { return state != 1; }", + "extern int state; extern int local_value(void); int main(void) { return state != 1 || local_value() != 11; }", ), ( "used", - "extern int state; extern int optional(void); int main(void) { return state != 3 || optional() != 7; }", + "extern int state; extern int local_value(void); extern int local_sd(void); int main(void) { return state != 3 || local_value() != 11 || local_sd() != 7; }", ), ] { let main = root.join(format!("{name}.c")); diff --git a/crates/fbuild-library/src/library/esptool.rs b/crates/fbuild-library/src/library/esptool.rs index 978bd135..0a5498dc 100644 --- a/crates/fbuild-library/src/library/esptool.rs +++ b/crates/fbuild-library/src/library/esptool.rs @@ -8,7 +8,7 @@ //! provisions esptool as a managed package instead, so no user `pip install` //! is required. //! -//! Modern metadata URLs use the **PyInstaller standalone binary** from +//! Modern metadata URLs on Unix use the **PyInstaller standalone binary** from //! [`tasmota/esptool`](https://github.com/tasmota/esptool). Older pioarduino //! releases instead pin a source-only `pioarduino/esptool` archive; those are //! installed at the exact URL in a private `uv tool` environment so their @@ -20,7 +20,9 @@ //! (`.../download/0.0.1/esptoolpy-v5.3.0.zip` → `5.3.0`) and //! version-in-release-tag (`.../download/v4.8.5/esptool.zip` → `4.8.5`). //! See `extract_esptool_version`. -//! 2. The host `(OS, ARCH)` maps to a tasmota platform tag +//! 2. Windows registry metadata selects the published pioarduino source archive +//! at the same version: some releases (including 5.1.0) have no Windows binary. +//! Other hosts map `(OS, ARCH)` to a tasmota platform tag //! (`linux-amd64`, `macos-arm64`, `windows-amd64`, …). An unsupported host //! yields an error, and the caller falls back to an `esptool` on PATH. //! 3. `https://github.com/tasmota/esptool/releases/download/v{version}/esptool-{platform}.zip` @@ -85,17 +87,42 @@ pub struct Esptool { impl Esptool { /// Create from the `platform.json`-derived `tool-esptoolpy` URL /// (`Esp32Platform::get_package_url("tool-esptoolpy")`). Only the version - /// embedded in the URL is used — from the filename, or from the release - /// tag when the filename is generic. + /// embedded in registry URLs is used — from the filename. Direct source + /// archive URLs are retained, with the version taken from the release tag. pub fn from_metadata_url(project_dir: &Path, metadata_url: &str) -> Self { + Self::from_metadata_url_for_host( + project_dir, + metadata_url, + fbuild_core::platform::host::is_windows(), + ) + } + + fn from_metadata_url_for_host(project_dir: &Path, metadata_url: &str, windows: bool) -> Self { + let version = extract_esptool_version(metadata_url); + let source_url = if metadata_url + .starts_with("https://github.com/pioarduino/esptool/releases/download/") + { + Some(metadata_url.to_string()) + } else if windows + && metadata_url.starts_with("https://github.com/pioarduino/registry/releases/download/") + && version != "unknown" + && metadata_url.ends_with(&format!("/esptoolpy-v{version}.zip")) + { + // This is the any-host source URL published in the registry's + // tools.json, not a renamed binary asset. Keep the SDK's version; + // ensure_source_installed verifies the resulting uv launcher. + Some(format!( + "https://github.com/pioarduino/esptool/releases/download/v{version}/esptool.zip" + )) + } else { + None + }; Self { project_dir: NormalizedPath::from(project_dir), - version: extract_esptool_version(metadata_url), - source_url: metadata_url - .starts_with("https://github.com/pioarduino/esptool/releases/download/") - .then(|| metadata_url.to_string()), + version: version.clone(), + source_url, source_checksum: None, - expected_runtime_version: Some(extract_esptool_version(metadata_url)), + expected_runtime_version: Some(version), } } diff --git a/crates/fbuild-library/src/library/esptool_tests.rs b/crates/fbuild-library/src/library/esptool_tests.rs index f42078c5..6270e58d 100644 --- a/crates/fbuild-library/src/library/esptool_tests.rs +++ b/crates/fbuild-library/src/library/esptool_tests.rs @@ -339,7 +339,12 @@ fn download_url_matches_the_url_ensure_installed_uses() { ); assert_eq!(esptool.version(), "5.3.0"); - if let Some(tag) = tasmota_platform_tag() { + if fbuild_core::platform::host::is_windows() { + assert_eq!( + esptool.download_url().unwrap(), + "https://github.com/pioarduino/esptool/releases/download/v5.3.0/esptool.zip" + ); + } else if let Some(tag) = tasmota_platform_tag() { assert_eq!( esptool.download_url().unwrap(), Esptool::release_url("5.3.0", tag) @@ -347,6 +352,58 @@ fn download_url_matches_the_url_ensure_installed_uses() { } } +#[test] +fn windows_registry_esptool_uses_published_source_archives() { + let tmp = tempfile::TempDir::new().unwrap(); + // The real 5.1.0 registry tools.json publishes this any-host source URL; + // its upstream release has no esptool-windows-amd64.zip asset. + for version in ["5.1.0", "5.3.0"] { + let metadata = format!( + "https://github.com/pioarduino/registry/releases/download/0.0.1/esptoolpy-v{version}.zip" + ); + let source = format!( + "https://github.com/pioarduino/esptool/releases/download/v{version}/esptool.zip" + ); + let windows = Esptool::from_metadata_url_for_host(tmp.path(), &metadata, true); + assert_eq!(windows.version(), version); + assert_eq!(windows.download_url().unwrap(), source); + assert_eq!(windows.source_url.as_deref(), Some(source.as_str())); + assert_eq!(windows.expected_runtime_version.as_deref(), Some(version)); + + let unix = Esptool::from_metadata_url_for_host(tmp.path(), &metadata, false); + assert!(unix.source_url.is_none()); + assert_eq!(unix.version(), version); + } +} + +#[test] +fn windows_source_selection_preserves_direct_and_platformio_archives() { + let tmp = tempfile::TempDir::new().unwrap(); + let source = "https://github.com/pioarduino/esptool/releases/download/v4.7.5/esptool.zip"; + for windows in [false, true] { + let esptool = Esptool::from_metadata_url_for_host(tmp.path(), source, windows); + assert_eq!(esptool.download_url().unwrap(), source); + assert_eq!(esptool.expected_runtime_version.as_deref(), Some("4.7.5")); + } + let payload = fbuild_core::platformio_package::ResolvedPayload { + owner: "platformio".into(), + kind: fbuild_core::platformio_package::PackageKind::Tool, + name: "tool-esptoolpy".into(), + version: "1.30300.0".into(), + system: "all".into(), + url: "https://dl.registry.platformio.org/download/platformio/tool/tool-esptoolpy/1.30300.0/tool-esptoolpy-1.30300.0.tar.gz".into(), + sha256: "published-registry-digest".into(), + }; + let legacy = Esptool::from_registry_payload(tmp.path(), &payload); + assert_eq!(legacy.download_url().unwrap(), payload.url); + assert_eq!(legacy.version(), "1.30300.0"); + assert_eq!( + legacy.source_checksum.as_deref(), + Some(payload.sha256.as_str()) + ); + assert!(legacy.expected_runtime_version.is_none()); +} + #[test] fn pinned_pioarduino_source_archive_is_not_replaced_by_tasmota() { let tmp = tempfile::TempDir::new().unwrap(); From b40617fe62c2bf5919e039399efb7ac0c10e38d3 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Tue, 13 Oct 2026 08:50:40 -0700 Subject: [PATCH 4/5] test: synchronize shared gate overlap without startup assumptions Local-Gate: v1 tree=29ce8632be0b5c19f9fe99873bbfc703c5a0af80 secs=674 lanes=linux-minimal:run,dylint:run Ci-Attestation: {"at":1791907323,"gate":"general/all/ubuntu-ci-guards","host":"linux-x86_64","key":"48a8a2f989b70037eb1de6e0b5066c678d412860021a8ea5a97dded828927341","lane":"linux-minimal","parents":["f27d10de366b43007c9a0c121c04c64ca6b452f5"],"secs":396,"stamp":"917eb5c9f531fc1390c91d1ea2e5845c","tree":"29ce8632be0b5c19f9fe99873bbfc703c5a0af80","v":1,"via":"run"} Ci-Attestation: {"at":1791907323,"gate":"rust/x86_64-unknown-linux-gnu/workspace-clippy","host":"linux-x86_64","key":"48a8a2f989b70037eb1de6e0b5066c678d412860021a8ea5a97dded828927341","lane":"linux-minimal","parents":["f27d10de366b43007c9a0c121c04c64ca6b452f5"],"secs":396,"stamp":"2f741d09a8d53c3556e2f285477ede26","tree":"29ce8632be0b5c19f9fe99873bbfc703c5a0af80","v":1,"via":"run"} Ci-Attestation: {"at":1791907323,"gate":"rust/x86_64-unknown-linux-gnu/workspace-test","host":"linux-x86_64","key":"48a8a2f989b70037eb1de6e0b5066c678d412860021a8ea5a97dded828927341","lane":"linux-minimal","parents":["f27d10de366b43007c9a0c121c04c64ca6b452f5"],"secs":396,"stamp":"d8da058197f7e727a6300d154e1822a3","tree":"29ce8632be0b5c19f9fe99873bbfc703c5a0af80","v":1,"via":"run"} Ci-Attestation: {"at":1791907323,"gate":"rust/x86_64-unknown-linux-gnu/python-facade-test","host":"linux-x86_64","key":"48a8a2f989b70037eb1de6e0b5066c678d412860021a8ea5a97dded828927341","lane":"linux-minimal","parents":["f27d10de366b43007c9a0c121c04c64ca6b452f5"],"secs":396,"stamp":"98d209477a7957effbc4241ce7b93697","tree":"29ce8632be0b5c19f9fe99873bbfc703c5a0af80","v":1,"via":"run"} Ci-Attestation: {"at":1791907323,"gate":"general/all/dylint-policy","host":"linux-x86_64","key":"25fdb571d5bdf03cda4dd88cc7efd4120458506aeece4a548b971531b5f019d3","lane":"dylint","parents":["f27d10de366b43007c9a0c121c04c64ca6b452f5"],"secs":277,"stamp":"e0ea01c9cb2ec9ebeda42d770877d640","tree":"29ce8632be0b5c19f9fe99873bbfc703c5a0af80","v":1,"via":"run"} Ci-Attestation: {"at":1791907323,"gate":"rust/x86_64-unknown-linux-gnu/dylint-library-check","host":"linux-x86_64","key":"25fdb571d5bdf03cda4dd88cc7efd4120458506aeece4a548b971531b5f019d3","lane":"dylint","parents":["f27d10de366b43007c9a0c121c04c64ca6b452f5"],"secs":277,"stamp":"0654e24041768ddfc536303af0b65d48","tree":"29ce8632be0b5c19f9fe99873bbfc703c5a0af80","v":1,"via":"run"} Ci-Attestation: {"at":1791907323,"gate":"rust/x86_64-unknown-linux-gnu/workspace-dylint","host":"linux-x86_64","key":"25fdb571d5bdf03cda4dd88cc7efd4120458506aeece4a548b971531b5f019d3","lane":"dylint","parents":["f27d10de366b43007c9a0c121c04c64ca6b452f5"],"secs":277,"stamp":"2757437bd60d153c844273313cbabf21","tree":"29ce8632be0b5c19f9fe99873bbfc703c5a0af80","v":1,"via":"run"} --- .../src/pipeline/library_shared_gate_tests.rs | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/crates/fbuild-build-engine/src/pipeline/library_shared_gate_tests.rs b/crates/fbuild-build-engine/src/pipeline/library_shared_gate_tests.rs index 718b4720..75b18fe6 100644 --- a/crates/fbuild-build-engine/src/pipeline/library_shared_gate_tests.rs +++ b/crates/fbuild-build-engine/src/pipeline/library_shared_gate_tests.rs @@ -19,12 +19,13 @@ use crate::parallel::compile_sources_parallel_shared; use std::path::Path; /// Fake in-process "core" compiler: records a (label, phase, ns) event to -/// the shared log for every compile, sleeping long enough that at least -/// one lib_deps archive compile is guaranteed to overlap it. +/// the shared log for every compile, retaining its gate permit until the +/// library phase finishes, regardless of subprocess startup latency. struct FakeCoreCompiler { gcc: NormalizedPath, log: Arc>>, - sleep_ms: u64, + started: Arc, + libraries_done: Arc, } fn now_ns() -> u128 { @@ -48,7 +49,13 @@ impl Compiler for FakeCoreCompiler { .lock() .unwrap() .push(("core".to_string(), "start".to_string(), now_ns())); - tokio::time::sleep(std::time::Duration::from_millis(self.sleep_ms)).await; + self.started.notify_one(); + tokio::time::timeout( + std::time::Duration::from_secs(30), + self.libraries_done.notified(), + ) + .await + .expect("library phase must finish while core retains one shared gate permit"); self.log .lock() .unwrap() @@ -227,14 +234,13 @@ async fn lib_deps_and_core_share_one_job_gate() { let core_log: Arc>> = Arc::new(std::sync::Mutex::new(Vec::new())); + let core_started = Arc::new(tokio::sync::Notify::new()); + let libraries_done = Arc::new(tokio::sync::Notify::new()); let core_compiler = FakeCoreCompiler { gcc: NormalizedPath::new(&gcc), log: core_log.clone(), - // Long enough that the two ~0.1s library compiles (which - // must serialize behind each other once the gate's second - // slot is claimed by this core compile) are guaranteed to - // land inside this compile's [start, end) window. - sleep_ms: 300, + started: core_started.clone(), + libraries_done: libraries_done.clone(), }; let core_source = project.join("core.cpp"); std::fs::write(&core_source, "int g();\n").unwrap(); @@ -253,7 +259,15 @@ async fn lib_deps_and_core_share_one_job_gate() { None, None, ), - lib_deps.compile(&gate), + async { + core_started.notified().await; + // Exercise startup slower than the old 300 ms core sleep. + // Correctness depends on completion signaling, not this delay. + tokio::time::sleep(std::time::Duration::from_millis(400)).await; + let result = lib_deps.compile(&gate).await; + libraries_done.notify_one(); + result + }, ); let core_result = core_result.unwrap(); @@ -317,7 +331,7 @@ async fn lib_deps_and_core_share_one_job_gate() { // The two lib compiles must not run concurrently with each // other AND the core compile at once (only one gate, size 2: - // core takes one slot for its whole 300ms, so the two lib + // core takes one slot until both libraries finish, so the two lib // compiles must themselves serialize behind the gate's other // slot). assert!( From a7bff04b0937414e76c08d0fe9f9cc44991e0f3b Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Tue, 13 Oct 2026 09:41:09 -0700 Subject: [PATCH 5/5] chore: synchronize editable package lock metadata Local-Gate: v1 tree=37c123f2c5340906e82e040746aae3b94ea4a606 secs=754 lanes=linux-minimal:run,dylint:run Ci-Attestation: {"at":1791910425,"gate":"general/all/ubuntu-ci-guards","host":"linux-x86_64","key":"df49b4973686fd1feac2ef9c797aa0167abe550208640b840ca0ab8b178528f0","lane":"linux-minimal","parents":["b40617fe62c2bf5919e039399efb7ac0c10e38d3"],"secs":467,"stamp":"34e55cbfe0fd768df5b4f38687ff4a56","tree":"37c123f2c5340906e82e040746aae3b94ea4a606","v":1,"via":"run"} Ci-Attestation: {"at":1791910425,"gate":"rust/x86_64-unknown-linux-gnu/workspace-clippy","host":"linux-x86_64","key":"df49b4973686fd1feac2ef9c797aa0167abe550208640b840ca0ab8b178528f0","lane":"linux-minimal","parents":["b40617fe62c2bf5919e039399efb7ac0c10e38d3"],"secs":467,"stamp":"0698e3d1f3ae07accf96b1db5afa7d14","tree":"37c123f2c5340906e82e040746aae3b94ea4a606","v":1,"via":"run"} Ci-Attestation: {"at":1791910425,"gate":"rust/x86_64-unknown-linux-gnu/workspace-test","host":"linux-x86_64","key":"df49b4973686fd1feac2ef9c797aa0167abe550208640b840ca0ab8b178528f0","lane":"linux-minimal","parents":["b40617fe62c2bf5919e039399efb7ac0c10e38d3"],"secs":467,"stamp":"28154df894c6d4ff81484c57872db53f","tree":"37c123f2c5340906e82e040746aae3b94ea4a606","v":1,"via":"run"} Ci-Attestation: {"at":1791910425,"gate":"rust/x86_64-unknown-linux-gnu/python-facade-test","host":"linux-x86_64","key":"df49b4973686fd1feac2ef9c797aa0167abe550208640b840ca0ab8b178528f0","lane":"linux-minimal","parents":["b40617fe62c2bf5919e039399efb7ac0c10e38d3"],"secs":467,"stamp":"0d70f84aabcff11abc15e6e41656c2de","tree":"37c123f2c5340906e82e040746aae3b94ea4a606","v":1,"via":"run"} Ci-Attestation: {"at":1791910425,"gate":"general/all/dylint-policy","host":"linux-x86_64","key":"7008cbff66577d822341f7dedb5abf20e82d96295bb422c604db7c349d0e8990","lane":"dylint","parents":["b40617fe62c2bf5919e039399efb7ac0c10e38d3"],"secs":287,"stamp":"43da1f2aff0bd22c620f06d07d226409","tree":"37c123f2c5340906e82e040746aae3b94ea4a606","v":1,"via":"run"} Ci-Attestation: {"at":1791910425,"gate":"rust/x86_64-unknown-linux-gnu/dylint-library-check","host":"linux-x86_64","key":"7008cbff66577d822341f7dedb5abf20e82d96295bb422c604db7c349d0e8990","lane":"dylint","parents":["b40617fe62c2bf5919e039399efb7ac0c10e38d3"],"secs":287,"stamp":"bfa4437bbe06ecfa9cafac6cbbb56298","tree":"37c123f2c5340906e82e040746aae3b94ea4a606","v":1,"via":"run"} Ci-Attestation: {"at":1791910425,"gate":"rust/x86_64-unknown-linux-gnu/workspace-dylint","host":"linux-x86_64","key":"7008cbff66577d822341f7dedb5abf20e82d96295bb422c604db7c349d0e8990","lane":"dylint","parents":["b40617fe62c2bf5919e039399efb7ac0c10e38d3"],"secs":287,"stamp":"bf48552f167eebcb94c58766d1fad2a5","tree":"37c123f2c5340906e82e040746aae3b94ea4a606","v":1,"via":"run"} --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index a190faed..2dd05a66 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.10" [[package]] name = "fbuild" -version = "2.5.40" +version = "2.5.41" source = { editable = "." } [package.dev-dependencies]