diff --git a/.github/workflows/release-auto.yml b/.github/workflows/release-auto.yml index 9eb0fc8f4..02c1226ea 100644 --- a/.github/workflows/release-auto.yml +++ b/.github/workflows/release-auto.yml @@ -6,6 +6,10 @@ on: paths: - Cargo.toml - pyproject.toml + # An incomplete release must be retried when its release machinery is + # repaired without forcing another public version bump. + - .github/workflows/release-auto.yml + - .github/workflows/template_native_build.yml workflow_dispatch: inputs: publish: diff --git a/.github/workflows/template_native_build.yml b/.github/workflows/template_native_build.yml index 686eedce1..5ceeafa29 100644 --- a/.github/workflows/template_native_build.yml +++ b/.github/workflows/template_native_build.yml @@ -41,12 +41,6 @@ env: CARGO_TERM_COLOR: always RUSTFLAGS: "-D warnings" FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" - # Native Windows release builds can be quiet for longer than soldr's - # default 30 minute cargo diagnostic watchdog while rustc/link.exe are - # still active. Match the release-binary step's 45 minute timeout so - # GitHub Actions owns the outer deadline instead of soldr killing the - # build early. - SOLDR_CARGO_WAIT_TIMEOUT_SECS: "2700" # cc-rs env vars for the xwin lanes. ring's build.rs uses cc-rs; # without these, cc-rs invokes plain `clang` and the curve25519.c # compile fails with `error: no such file or directory: '/imsvc'` @@ -71,6 +65,29 @@ jobs: name: Build (${{ inputs.target }}) runs-on: ${{ inputs.runner }} timeout-minutes: 90 + env: + # Native Windows release builds can be quiet for longer than soldr's + # default 30 minute cargo diagnostic watchdog while rustc/link.exe are + # still active. Match the release-binary step's 45 minute timeout so + # GitHub Actions owns the outer deadline instead of soldr killing the + # build early. + SOLDR_CARGO_WAIT_TIMEOUT_SECS: "2700" + # Six cold release lanes run concurrently on two-core hosted runners. + # Bound rustc and soldr concurrency so cross-target codegen stays within + # the runner memory limit instead of being killed by the host. + CARGO_BUILD_JOBS: "1" + SOLDR_JOBS: "1" + # cargo-zigbuild clears generic CFLAGS while constructing a cross-target + # environment. Use cc-rs's target-specific variables so the vendored + # mimalloc-pprof build keeps this narrow Zig diagnostic demotion. + CFLAGS_x86_64_unknown_linux_musl: "-Wno-error=date-time" + CFLAGS_aarch64_unknown_linux_musl: "-Wno-error=date-time" + CFLAGS_x86_64_apple_darwin: "-Wno-error=date-time" + CFLAGS_aarch64_apple_darwin: "-Wno-error=date-time" + # Keep every soldr invocation in this reusable job on the same catalogue. + # soldr >= 0.9.5 consumes its multipart v2 assets, including Apple SDKs + # whose legacy direct-LFS URLs are no longer anonymously downloadable. + SOLDR_TOOLCHAIN_ORIGIN: https://zackees.github.io/soldr-toolchain steps: - uses: actions/checkout@v6 @@ -93,15 +110,13 @@ jobs: # v0.7.85/v0.7.87 were broken releases that shipped silent # binaries (no output at all), which setup-soldr@v0.9.63 # surfaced as a version-JSON parse failure. - # Pin the latest published soldr release; setup-soldr's current - # default can briefly lead a release that has not been published yet. - # soldr v0.8.0 is the - # known-good floor for the Apple SDK URL fix, soldr-clang-shim - # in every release archive, cargo:rustc-env wrapper forwarding, - # and managed cmake/ninja. + # Pin a fully published soldr release. v0.9.5 is the first version + # that consumes catalogue-v2 multipart assets; v0.8.23 still probes + # the retired direct-LFS Apple SDK URLs even though the SDKs are + # available through the v2 catalogue. uses: zackees/setup-soldr@v0 with: - version: 0.8.23 + version: 0.9.6 cache: true build-cache: true target-cache: true @@ -226,20 +241,12 @@ jobs: # cargo invocation so the SDKROOT export is debuggable independent # of the build itself. # - # SOLDR_TOOLCHAIN_ORIGIN redirects soldr's toolchain catalogue - # fetch away from the retired `manifest` branch of - # zackees/soldr (soldr#988 Phase 5 / soldr#992, merged - # 2026-06-27) at the new soldr-toolchain origin. soldr v0.7.66 - # honors this env var via the Phase 2 catalogue-first - # fetch_once (soldr#989); older versions ignore it (and fail - # against the retired manifest branch, which is what we're - # avoiding here). + # SOLDR_TOOLCHAIN_ORIGIN is job-scoped above so this prepare step and + # both following build steps resolve the exact same catalogue. - name: Prepare Apple SDK (Linux → mac cross) if: inputs.mac_cross_linux timeout-minutes: 15 shell: bash - env: - SOLDR_TOOLCHAIN_ORIGIN: https://zackees.github.io/soldr-toolchain run: soldr prepare --target ${{ inputs.target }} - name: Build release binaries @@ -301,6 +308,9 @@ jobs: # soldr's bin cache, which has been serving a corrupted # cargo-zigbuild binary (`Syntax error: ")" unexpected` at # line 10) and blocking PyPI publishes. See #331. + # mimalloc-pprof's vendored diagnostic banner expands + # __DATE__/__TIME__. Zig promotes that one warning to an error; + # demote only that diagnostic for this C dependency build. cargo zigbuild --release --target ${{ inputs.target }} \ -p fbuild-cli \ -p fbuild-daemon diff --git a/crates/fbuild-python/tests/pyo3_policy.rs b/crates/fbuild-python/tests/pyo3_policy.rs index dc5d84529..79d671a31 100644 --- a/crates/fbuild-python/tests/pyo3_policy.rs +++ b/crates/fbuild-python/tests/pyo3_policy.rs @@ -1,4 +1,4 @@ -use std::fs; +use std::{fs, ops::Range}; use fbuild_core::path::NormalizedPath; @@ -13,6 +13,116 @@ fn repo_root() -> NormalizedPath { ) } +fn yaml_indent(line: &str) -> usize { + line.len() - line.trim_start().len() +} + +fn yaml_mapping_entry(line: &str) -> Option<(&str, &str)> { + let content = line.trim_start(); + if content.is_empty() || content.starts_with(['#', '-']) { + return None; + } + let (key, value) = content.split_once(':')?; + Some((key.trim(), value.trim())) +} + +fn yaml_scalar(value: &str) -> &str { + value + .strip_prefix('"') + .and_then(|value| value.strip_suffix('"')) + .unwrap_or(value) +} + +fn yaml_significant(line: &str) -> bool { + let content = line.trim_start(); + !content.is_empty() && !content.starts_with('#') +} + +fn yaml_mapping_block_in_scope( + lines: &[&str], + mut scope: Range, + mut entry_indent: usize, + path: &[&str], +) -> Option<(Range, usize)> { + for key in path { + let entry = scope.clone().find(|&index| { + yaml_indent(lines[index]) == entry_indent + && yaml_mapping_entry(lines[index]) == Some((*key, "")) + })?; + let end = ((entry + 1)..scope.end) + .find(|&index| { + yaml_significant(lines[index]) && yaml_indent(lines[index]) <= entry_indent + }) + .unwrap_or(scope.end); + scope = (entry + 1)..end; + entry_indent += 2; + } + + Some((scope, entry_indent)) +} + +fn yaml_mapping_block(lines: &[&str], path: &[&str]) -> Option<(Range, usize)> { + yaml_mapping_block_in_scope(lines, 0..lines.len(), 0, path) +} + +fn yaml_mapping_value_in_scope<'a>( + lines: &[&'a str], + scope: Range, + entry_indent: usize, + path: &[&str], +) -> Option<&'a str> { + let (key, parent_path) = path.split_last()?; + let (scope, entry_indent) = + yaml_mapping_block_in_scope(lines, scope, entry_indent, parent_path)?; + scope + .filter_map(|index| { + (yaml_indent(lines[index]) == entry_indent) + .then(|| yaml_mapping_entry(lines[index])) + .flatten() + }) + .find_map(|(candidate, value)| (candidate == *key).then(|| yaml_scalar(value))) +} + +fn yaml_mapping_value<'a>(lines: &[&'a str], path: &[&str]) -> Option<&'a str> { + yaml_mapping_value_in_scope(lines, 0..lines.len(), 0, path) +} + +fn yaml_sequence_values<'a>(lines: &[&'a str], path: &[&str]) -> Option> { + let (scope, item_indent) = yaml_mapping_block(lines, path)?; + Some( + scope + .filter_map(|index| { + (yaml_indent(lines[index]) == item_indent) + .then(|| lines[index].trim_start().strip_prefix("- ")) + .flatten() + .map(yaml_scalar) + }) + .collect(), + ) +} + +fn yaml_step_mapping_value<'a>( + lines: &[&'a str], + step_name: &str, + path: &[&str], +) -> Option<&'a str> { + let (steps, item_indent) = yaml_mapping_block(lines, &["jobs", "build", "steps"])?; + let step = steps.clone().find(|&index| { + if yaml_indent(lines[index]) != item_indent { + return false; + } + let Some(item) = lines[index].trim_start().strip_prefix("- ") else { + return false; + }; + yaml_mapping_entry(item) + .is_some_and(|(key, value)| key == "name" && yaml_scalar(value) == step_name) + })?; + let step_end = ((step + 1)..steps.end) + .find(|&index| yaml_significant(lines[index]) && yaml_indent(lines[index]) <= item_indent) + .unwrap_or(steps.end); + yaml_mapping_value_in_scope(lines, (step + 1)..step_end, item_indent + 2, path) +} + #[test] fn pyo3_029_policy_stays_target_python_independent() { // FastLED/fbuild#1025: keep every cross-build branch explicit until @@ -93,3 +203,62 @@ fn pyo3_029_policy_stays_target_python_independent() { ); } } + +#[test] +fn native_release_workflow_uses_current_cross_toolchains() { + let root = repo_root(); + let workflow = + fs::read_to_string(root.join(".github/workflows/template_native_build.yml")).unwrap(); + let release_workflow = + fs::read_to_string(root.join(".github/workflows/release-auto.yml")).unwrap(); + let workflow_lines = workflow.lines().collect::>(); + let release_workflow_lines = release_workflow.lines().collect::>(); + + assert_eq!( + yaml_step_mapping_value(&workflow_lines, "Setup soldr", &["with", "version"]), + Some("0.9.6"), + "the setup-soldr step needs soldr >= 0.9.5 for catalogue-v2 Apple SDK assets" + ); + assert_eq!( + yaml_mapping_value( + &workflow_lines, + &["jobs", "build", "env", "SOLDR_TOOLCHAIN_ORIGIN"] + ), + Some("https://zackees.github.io/soldr-toolchain"), + "Apple SDK prepare and build steps must share a job-scoped catalogue origin" + ); + for target in [ + "x86_64_unknown_linux_musl", + "aarch64_unknown_linux_musl", + "x86_64_apple_darwin", + "aarch64_apple_darwin", + ] { + let target_cflags = format!("CFLAGS_{target}"); + assert_eq!( + yaml_mapping_value( + &workflow_lines, + &["jobs", "build", "env", target_cflags.as_str()] + ), + Some("-Wno-error=date-time"), + "zig cross builds need a job-scoped mimalloc-pprof diagnostic override: {target_cflags}" + ); + } + for job_limit in ["CARGO_BUILD_JOBS", "SOLDR_JOBS"] { + assert_eq!( + yaml_mapping_value(&workflow_lines, &["jobs", "build", "env", job_limit]), + Some("1"), + "native release lanes need a job-scoped hosted-runner memory limit: {job_limit}" + ); + } + let release_paths = yaml_sequence_values(&release_workflow_lines, &["on", "push", "paths"]) + .expect("release workflow must define on.push.paths"); + for release_input in [ + ".github/workflows/release-auto.yml", + ".github/workflows/template_native_build.yml", + ] { + assert!( + release_paths.contains(&release_input), + "release workflow fixes must retrigger an incomplete publication: {release_input}" + ); + } +} diff --git a/python/fbuild/__init__.py b/python/fbuild/__init__.py index 77fef08fa..beb1c9317 100644 --- a/python/fbuild/__init__.py +++ b/python/fbuild/__init__.py @@ -19,12 +19,13 @@ find_firmware, ) + __all__ = [ - "__version__", "AsyncDaemon", "AsyncDaemonConnection", "Daemon", "DaemonConnection", + "__version__", "connect_daemon", "connect_daemon_async", "find_firmware",