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
4 changes: 4 additions & 0 deletions .github/workflows/release-auto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
56 changes: 33 additions & 23 deletions .github/workflows/template_native_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'`
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
171 changes: 170 additions & 1 deletion crates/fbuild-python/tests/pyo3_policy.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::fs;
use std::{fs, ops::Range};

use fbuild_core::path::NormalizedPath;

Expand All @@ -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<usize>,
mut entry_indent: usize,
path: &[&str],
) -> Option<(Range<usize>, 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>, usize)> {
yaml_mapping_block_in_scope(lines, 0..lines.len(), 0, path)
}

fn yaml_mapping_value_in_scope<'a>(
lines: &[&'a str],
scope: Range<usize>,
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<Vec<&'a str>> {
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
Expand Down Expand Up @@ -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::<Vec<_>>();
let release_workflow_lines = release_workflow.lines().collect::<Vec<_>>();

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}"
);
}
}
3 changes: 2 additions & 1 deletion python/fbuild/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@
find_firmware,
)


__all__ = [
"__version__",
"AsyncDaemon",
"AsyncDaemonConnection",
"Daemon",
"DaemonConnection",
"__version__",
"connect_daemon",
"connect_daemon_async",
"find_firmware",
Expand Down
Loading