diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 31709aaa..a214d08d 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -4,10 +4,11 @@ CI/CD workflows for the fbuild project, covering lint, test, documentation, and ## CI Checks (push/PR) -- **`check-{macos,ubuntu,windows}.yml`** -- Clippy + tests per platform +- **`check-{ubuntu,windows}.yml`** -- Clippy + tests per platform (no macOS runner: macOS + binaries are cross-built from Linux in the release matrix, see below) - **`fmt.yml`** -- Rustfmt check | **`docs.yml`** -- Doc build with `-D warnings` - **`msrv.yml`** -- MSRV 1.95.0 verification | **`validate-boards.yml`** -- Board JSON validation -- **`platform-boundary-research.yml`** -- Windows/Linux/macOS reconciliation of the #1307 research inventory and RED fixture +- **`platform-boundary-research.yml`** -- Windows/Linux reconciliation of the #1307 research inventory and RED fixture - **`loc-gate.yml`** -- Reject `.rs` files over 1000 LOC | **`lint-subprocess.yml`** -- Forbid direct subprocess spawns - **`crate-gate.yml`** -- Reject new workspace crates (monocrate policy, `ci/check_workspace_crates.py`) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 11f9f237..cf98df98 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,16 +35,21 @@ jobs: linux_cross: true macos_cross: false - # x86_64-apple-darwin removed: Intel Macs use ARM wheel via Rosetta. - # Cross-compiling ring crate ARM→x86 fails; native macos-13 runners - # are perpetually unavailable. Re-enable when ring supports cross or - # GitHub restores Intel runner capacity. + # x86_64-apple-darwin is not built here. This workflow builds + # binaries on demand; the release matrix (release-auto.yml) is + # what produces the Intel archive when a release needs one, and + # it cross-builds that target from Linux too. + # aarch64-apple-darwin cross-compiles from the Linux runner via + # soldr + cargo-zigbuild + soldr's managed Apple SDK, matching the + # release matrix. No macos-latest runner is used anywhere in this + # workflow. - target: aarch64-apple-darwin - runner: macos-latest + runner: ubuntu-latest binary_ext: "" linux_cross: false macos_cross: false + mac_cross_linux: true - target: x86_64-pc-windows-msvc runner: windows-latest @@ -59,4 +64,5 @@ jobs: binary_ext: ${{ matrix.binary_ext }} linux_cross: ${{ matrix.linux_cross }} macos_cross: ${{ matrix.macos_cross }} + mac_cross_linux: ${{ matrix.mac_cross_linux == true }} ref: ${{ github.event.inputs.ref || 'main' }} diff --git a/.github/workflows/check-macos.yml b/.github/workflows/check-macos.yml deleted file mode 100644 index 2419603f..00000000 --- a/.github/workflows/check-macos.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Check macOS - -on: - workflow_dispatch: {} - push: - branches: [main] - pull_request: - branches: [main] - -# Auto-cancel superseded PR runs: pushing again to a feature branch -# supersedes the in-flight run instead of queueing behind it. Non-PR -# events key on run_id so each gets its own group -- a shared group keeps -# only ONE pending run, which would silently drop queued main SHAs. -concurrency: - group: check-macos.yml-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -env: - CARGO_TERM_COLOR: always - RUSTFLAGS: "-D warnings" - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" - -jobs: - check: - name: Check (macos-latest) - runs-on: macos-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v6 - - name: Setup soldr - id: setup-soldr - uses: zackees/setup-soldr@v0 - with: - version: 0.8.23 - cache: true - build-cache: true - target-cache: true - prebuild-deps: none - # Opt out of the implicit `SOLDR_LINKER=fast` injection so cargo / - # rust-toolchain.toml stays in charge. Silences the routine - # "defaulting SOLDR_LINKER=fast" warning (issue #400 / - # setup-soldr#377). - linker: platform-default - # Realistic zccache payload for the workspace check job is - # ~1.5–2 GiB (issue #400). Raise the soft warn threshold above - # the steady-state size so the action only flags genuinely - # runaway caches. Hard cap stays at the action default (6 GiB). - cache-payload-warn-bytes: 2GiB - - - name: Check - shell: bash - run: soldr cargo check --workspace --all-targets - - name: Clippy - shell: bash - run: soldr cargo clippy --workspace --all-targets -- -D warnings - - name: Test - shell: bash - run: soldr cargo test --workspace diff --git a/.github/workflows/platform-boundary-research.yml b/.github/workflows/platform-boundary-research.yml index 69fd586d..d3347246 100644 --- a/.github/workflows/platform-boundary-research.yml +++ b/.github/workflows/platform-boundary-research.yml @@ -27,8 +27,6 @@ jobs: host: linux - os: windows-latest host: windows - - os: macos-latest - host: macos runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 diff --git a/CLAUDE.md b/CLAUDE.md index a5f8093d..d0434d32 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,7 +59,11 @@ The four rules an agent must internalize before doing anything else (all listed - **Always use a globally-installed `soldr` to execute Rust commands.** Bare cargo/rustc and legacy `uv run cargo` shims are blocked by hook. soldr uses `rustup which` to pick the rustup-managed toolchain from `rust-toolchain.toml`. The standard Cargo path is `soldr cargo ...`, so repo Rust builds get soldr's managed zccache path by default; do not add repo-specific `RUSTC_WRAPPER` wiring for normal builds. Install soldr globally via `uv tool install soldr` (or see https://github.com/zackees/soldr). - **Always use `uv` for Python.** Bare `python`/`pip` are blocked by hook. Use `uv run ...` or `uv pip ...`. - MSRV: 1.95.0 | Edition: 2021 | Toolchain: 1.95.0 pinned in `rust-toolchain.toml` (clippy + rustfmt) -- CI: Linux, macOS, Windows. All warnings denied (`RUSTFLAGS="-D warnings"`) +- CI hosts: Linux, Windows. All warnings denied (`RUSTFLAGS="-D warnings"`). There are + **no macOS runners** — macOS is a build *target*, not a test host: every apple-darwin + binary is cross-built from Linux (soldr + `cargo-zigbuild` + managed Apple SDK) by the + Linux-hosted native workflows, `release-auto.yml` and `build.yml`. Do not add a + `macos-latest` lane back. - Every directory with files must have a README.md (enforced by hook) ## Commands diff --git a/README.md b/README.md index 72837cb6..1d035cb4 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ files already used by PlatformIO sketches, but uses a Rust-native, data-driven build pipeline. [![Check Ubuntu](https://github.com/fastled/fbuild/actions/workflows/check-ubuntu.yml/badge.svg)](https://github.com/fastled/fbuild/actions/workflows/check-ubuntu.yml) -[![Check macOS](https://github.com/fastled/fbuild/actions/workflows/check-macos.yml/badge.svg)](https://github.com/fastled/fbuild/actions/workflows/check-macos.yml) [![Check Windows](https://github.com/fastled/fbuild/actions/workflows/check-windows.yml/badge.svg)](https://github.com/fastled/fbuild/actions/workflows/check-windows.yml) [![Formatting](https://github.com/fastled/fbuild/actions/workflows/fmt.yml/badge.svg)](https://github.com/fastled/fbuild/actions/workflows/fmt.yml) [![Documentation](https://github.com/fastled/fbuild/actions/workflows/docs.yml/badge.svg)](https://github.com/fastled/fbuild/actions/workflows/docs.yml) diff --git a/crates/fbuild-library/src/library/esp32_framework/libs.rs b/crates/fbuild-library/src/library/esp32_framework/libs.rs index 15ed7b70..2a290024 100644 --- a/crates/fbuild-library/src/library/esp32_framework/libs.rs +++ b/crates/fbuild-library/src/library/esp32_framework/libs.rs @@ -5,6 +5,10 @@ use std::path::{Path, PathBuf}; use super::Esp32Framework; use super::fs_utils::copy_dir_recursive; +/// Archive whose presence proves a per-MCU SDK tree carries real libraries +/// and not just the partial directory some core archives ship. +const FREERTOS_ARCHIVE: &str = "libfreertos.a"; + const NEW_SDK_LAYOUT: &str = "esp32-arduino-libs"; const OLD_SDK_LAYOUT: &str = "sdk"; @@ -57,7 +61,31 @@ fn mcu_sdk_complete(mcu_dir: &Path) -> bool { .join("FreeRTOS.h") .exists() && mcu_dir.join("flags").join("includes").exists() - && mcu_dir.join("lib").join("libfreertos.a").exists() + && has_freertos_archive(mcu_dir) +} + +/// Locate `libfreertos.a` inside an installed per-MCU SDK tree. +/// +/// Most MCUs put every archive in `lib/`. ESP32-S3 does not: its FreeRTOS +/// build differs per flash/PSRAM mode, so `libfreertos.a` ships **only** +/// under the memory-type variant dirs (`dio_opi`, `qio_qspi`, ...) while +/// `lib/` holds the other 165 archives. Requiring `lib/libfreertos.a` +/// therefore judged a complete S3 install incomplete forever, and +/// [`Esp32Framework::ensure_libs`] re-downloaded and re-extracted the +/// 298 MB SDK archive on *every* build — 132 s of a 136 s no-op +/// (FastLED/fbuild#1411). +fn has_freertos_archive(mcu_dir: &Path) -> bool { + if mcu_dir.join("lib").join(FREERTOS_ARCHIVE).exists() { + return true; + } + // Only one level deep: the variant dirs sit directly under the MCU dir. + std::fs::read_dir(mcu_dir) + .map(|entries| { + entries + .flatten() + .any(|entry| entry.path().join(FREERTOS_ARCHIVE).exists()) + }) + .unwrap_or(false) } /// Merge a requested MCU directory from a skeleton archive, regardless of a @@ -129,6 +157,22 @@ impl Esp32Framework { } } + // A present-but-incomplete MCU tree means the ~300 MB SDK is about + // to be installed again — re-extracted, and re-downloaded too + // unless the archive is still cached below. That is correct on a + // genuinely partial install and catastrophic when the completion + // check is simply wrong about the layout — the silent form of this + // cost every build 132 s on ESP32-S3 (FastLED/fbuild#1411). Say so. + for mcu_dir in mcu_sdk_dir_candidates(&tools_dir, mcu) { + if mcu_dir.exists() { + tracing::warn!( + "{} SDK dir {} exists but is incomplete; reinstalling the SDK libs", + mcu, + mcu_dir.display() + ); + } + } + std::fs::create_dir_all(&tools_dir)?; // Check for already-downloaded archive (skip re-download) @@ -250,6 +294,37 @@ mod tests { write(&mcu_dir.join("lib").join("libfreertos.a"), ""); } + /// ESP32-S3 is the one MCU whose `esp32-arduino-libs` tree ships + /// `libfreertos.a` **only** under the per-memory-type variant dirs + /// (`dio_opi`, `qio_qspi`, ...) — `lib/` holds the other 165 archives + /// but not that one. Requiring `lib/libfreertos.a` therefore judged + /// a fully-installed S3 SDK incomplete on every build, and + /// `ensure_libs` re-downloaded + re-extracted the 298 MB archive each + /// time: 132 s of a 136 s no-op build (FastLED/fbuild#1411). + #[test] + fn mcu_sdk_complete_accepts_memory_type_variant_freertos_lib() { + let tmp = tempfile::TempDir::new().unwrap(); + let mcu_dir = tmp.path().join("esp32s3"); + + write( + &mcu_dir + .join("include") + .join("freertos") + .join("FreeRTOS-Kernel") + .join("include") + .join("freertos") + .join("FreeRTOS.h"), + "", + ); + write(&mcu_dir.join("flags").join("includes"), ""); + // 165 archives land in `lib/`, but not libfreertos.a. + write(&mcu_dir.join("lib").join("libdriver.a"), ""); + assert!(!mcu_sdk_complete(&mcu_dir)); + + write(&mcu_dir.join("qio_qspi").join("libfreertos.a"), ""); + assert!(mcu_sdk_complete(&mcu_dir)); + } + #[test] fn mcu_sdk_complete_requires_freertos_kernel_header() { let tmp = tempfile::TempDir::new().unwrap(); diff --git a/docs/platform-boundary-research-inventory.md b/docs/platform-boundary-research-inventory.md index c2e2a46f..963e4906 100644 --- a/docs/platform-boundary-research-inventory.md +++ b/docs/platform-boundary-research-inventory.md @@ -17,10 +17,12 @@ external/vendor sources are outside this research union. Because the scanner walks source rather than expanded modules, the same checkout produces the same union on every host. `.github/workflows/platform-boundary-research.yml` -runs the drift check and fixture tests on Windows, Linux, and macOS and prints a -host-labelled total. Phase 2 must still reconcile the three raw parser/Dylint -inventories before it freezes its authoritative ledger; compiler hooks alone -cannot see wrong-host or orphaned module files. +runs the drift check and fixture tests on Windows and Linux and prints a +host-labelled total (the macOS runner lane was dropped from CI; macOS binaries +are cross-built from Linux, and the host-independent scanner reports the same +union there). Phase 2 must still reconcile the raw parser/Dylint inventories +before it freezes its authoritative ledger; compiler hooks alone cannot see +wrong-host or orphaned module files. Reproduce locally with: diff --git a/docs/platform-boundary-research.md b/docs/platform-boundary-research.md index 75bfa2ba..792c863e 100644 --- a/docs/platform-boundary-research.md +++ b/docs/platform-boundary-research.md @@ -93,7 +93,9 @@ initially reported 490 candidate occurrences. Phase 2 Dylint/scanner reconciliation first corrected the union to 496 after adding missed constructs and removing local-module false positives, then incorporated eight host-cfg occurrences added to `fbuild-paths` on `main` before the baseline merged. The -authoritative union is therefore 504. The checked-in rows and reproducible three-host protocol are described in +authoritative union is therefore 504. The checked-in rows and the reproducible +cross-host protocol (three hosts as run for phase 1; the macOS lane has since +been dropped from CI) are described in `platform-boundary-research-inventory.md`. This is reviewed research input, not the phase-2 exact-occurrence baseline. @@ -123,7 +125,9 @@ No current fbuild component needs a permanent specialized-artifact zone. | build/test fixtures | Generic or concrete-facade tests | Tests are not an exemption from the source boundary. | Phase 2 must revalidate this decision against its parser-derived three-host -union. If it discovers a genuine artifact ABI constraint, the exception must be +union (the scanner is host-independent; CI now runs it on Linux and Windows +only -- the macOS runner lane was dropped and macOS binaries are cross-built +from Linux). If it discovers a genuine artifact ABI constraint, the exception must be named and narrowly linted; a file/directory wildcard is not acceptable. ## RED evidence @@ -132,8 +136,10 @@ named and narrowly linted; a file/directory wildcard is not acceptable. attribute, active-host native import, `cfg!` expression, and compile-time host fact. It compiles on Windows, Linux, and macOS today because fbuild has no host-platform boundary lint. The phase-1 workflow preserves that positive -compile result on all three hosts. Phase 2 converts the same constructs into -negative Dylint/scanner fixtures whose expected result is a boundary error. +compile result on the Linux and Windows CI hosts (the macOS runner lane was +dropped from CI; macOS binaries are cross-built from Linux). Phase 2 converts +the same constructs into negative Dylint/scanner fixtures whose expected result +is a boundary error. Existing production and test sources provide additional RED evidence: the research inventory includes private/inactive attributes, 77 native paths, and