diff --git a/.github/actions/build-tests/action.yml b/.github/actions/build-tests/action.yml deleted file mode 100644 index 2ca8bbe7731..00000000000 --- a/.github/actions/build-tests/action.yml +++ /dev/null @@ -1,137 +0,0 @@ -name: Build HIL tests (composite) -description: Checkout, install toolchains, build tests for a SoC, upload artifact -inputs: - event_name: - description: github.event_name - required: true - - repository: - description: owner/repo for workflow_dispatch - required: true - - branch: - description: branch/tag to checkout if no SHA - required: true - - soc: - description: SoC (e.g. esp32c3) - required: true - - rust_target: - description: rust target triple - required: true - - tests: - description: Optional comma-separated test names - required: false - default: "" - - package: - description: '"hil-test", "hil-test-radio", or "all"' - required: false - default: "all" - -runs: - using: composite - steps: - - uses: actions/checkout@v6 - if: ${{ inputs.event_name == 'merge_group' }} - - uses: actions/checkout@v6 - if: ${{ inputs.event_name == 'workflow_dispatch' }} - with: - repository: ${{ inputs.repository }} - ref: ${{ inputs.branch }} - - - if: ${{ !contains(fromJson('["esp32","esp32s2","esp32s3"]'), inputs.soc) }} - uses: dtolnay/rust-toolchain@v1 - with: - target: ${{ inputs.rust_target }} - toolchain: stable - components: rust-src - - - if: ${{ !contains(fromJson('["esp32","esp32s2","esp32s3"]'), inputs.soc) }} - name: Install nightly Rust toolchain - uses: dtolnay/rust-toolchain@v1 - with: - target: ${{ inputs.rust_target }} - toolchain: nightly - components: rust-src - - - if: ${{ contains(fromJson('["esp32","esp32s2","esp32s3"]'), inputs.soc) }} - uses: esp-rs/xtensa-toolchain@v1.6 - with: - buildtargets: ${{ inputs.soc }} - default: true - version: 1.97.0.0 - - - name: Check if chip is supported by this ref's xtask - id: chip - shell: bash - run: | - if cargo xtask build tests ${{ inputs.soc }} --help >/dev/null 2>&1; then - echo "supported=true" >> $GITHUB_OUTPUT - else - echo "supported=false" >> $GITHUB_OUTPUT - echo "Skipping ${{ inputs.soc }}: this ref's xtask does not support it." - fi - - - name: Build tests - if: ${{ steps.chip.outputs.supported == 'true' }} - shell: bash - env: - CARGO_TARGET_DIR: ${{ github.workspace }}/target - run: | - build_package() { - local pkg=$1 - if [ -n "${{ inputs.tests }}" ]; then - cargo xtask build tests ${{ inputs.soc }} $pkg --tests "${{ inputs.tests }}" - else - cargo xtask build tests ${{ inputs.soc }} $pkg - fi - - # Always separate artifacts by package so upload paths are consistent - # for full and selected-test builds. - local src="${{ github.workspace }}/target/tests/${{ inputs.soc }}" - local dst="${src}/${pkg}" - mkdir -p "$dst" - find "$src" -maxdepth 1 -type f -exec mv {} "$dst/" \; - } - - if [ "${{ inputs.package }}" = "all" ]; then - build_package hil-test - build_package hil-test-radio - else - build_package "${{ inputs.package }}" - fi - - - uses: actions/upload-artifact@v6 - if: ${{ steps.chip.outputs.supported == 'true' && inputs.tests == '' && (inputs.package == 'all' || inputs.package == 'hil-test') }} - with: - name: tests-${{ inputs.soc }}-hil-test - path: ${{ github.workspace }}/target/tests/${{ inputs.soc }}/hil-test - if-no-files-found: error - overwrite: true - - - uses: actions/upload-artifact@v6 - if: ${{ steps.chip.outputs.supported == 'true' && inputs.tests == '' && (inputs.package == 'all' || inputs.package == 'hil-test-radio') && hashFiles(format('{0}/target/tests/{1}/hil-test-radio/**', github.workspace, inputs.soc)) != '' }} - with: - name: tests-${{ inputs.soc }}-hil-test-radio - path: ${{ github.workspace }}/target/tests/${{ inputs.soc }}/hil-test-radio - if-no-files-found: error - overwrite: true - - - uses: actions/upload-artifact@v6 - if: ${{ steps.chip.outputs.supported == 'true' && inputs.tests != '' && (inputs.package == 'all' || inputs.package == 'hil-test') }} - with: - name: tests-${{ inputs.soc }}-hil-test - path: ${{ github.workspace }}/target/tests/${{ inputs.soc }}/hil-test - if-no-files-found: ignore - overwrite: true - - - uses: actions/upload-artifact@v6 - if: ${{ steps.chip.outputs.supported == 'true' && inputs.tests != '' && (inputs.package == 'all' || inputs.package == 'hil-test-radio') }} - with: - name: tests-${{ inputs.soc }}-hil-test-radio - path: ${{ github.workspace }}/target/tests/${{ inputs.soc }}/hil-test-radio - if-no-files-found: ignore - overwrite: true diff --git a/.github/actions/setup-nightly/action.yml b/.github/actions/setup-nightly/action.yml index 8cbe49e9d26..ff45fdf1db9 100644 --- a/.github/actions/setup-nightly/action.yml +++ b/.github/actions/setup-nightly/action.yml @@ -3,7 +3,9 @@ description: Install the repo-wide pinned Rust nightly toolchain, with an option inputs: targets: - description: Comma-separated list of target triples to install. + description: >- + Comma-separated list of target triples to install, or "all" for every RISC-V + triple in chips.json. required: false default: "" components: @@ -24,10 +26,25 @@ runs: shell: bash run: echo "nightly=nightly" >> "$GITHUB_OUTPUT" + - id: targets + shell: bash + run: | + if [ '${{ inputs.targets }}' = all ]; then + # Order-preserving dedupe, so the triple list reads the same as + # chips.json rather than being alphabetised. + VALUE=$(jq -r ' + [.[] | select(.arch == "riscv") | ."rust-target"] + | reduce .[] as $t ([]; if index($t) then . else . + [$t] end) + | join(",")' .github/chips.json) + else + VALUE='${{ inputs.targets }}' + fi + echo "value=$VALUE" >> "$GITHUB_OUTPUT" + - uses: dtolnay/rust-toolchain@v1 with: toolchain: ${{ steps.version.outputs.nightly }} - targets: ${{ inputs.targets }} + targets: ${{ steps.targets.outputs.value }} components: ${{ inputs.components }} # xtask docs/doc-tests call `cargo +nightly`; make that name resolve to the pin. diff --git a/.github/actions/setup-toolchains/action.yml b/.github/actions/setup-toolchains/action.yml new file mode 100644 index 00000000000..89294368bb5 --- /dev/null +++ b/.github/actions/setup-toolchains/action.yml @@ -0,0 +1,126 @@ +name: Setup toolchains +description: >- + Install the repo-wide pinned Rust toolchains for Xtensa and/or RISC-V targets. + +inputs: + channel: + description: 'Which pin to install: "current" or "msrv".' + required: false + default: current + current-xtensa-version: + description: Xtensa Rust release for the "current" channel. + required: false + default: "1.97.0.0" + xtensa: + description: When "true", install the Xtensa toolchain. + required: false + default: "false" + xtensa-buildtargets: + description: Comma-separated SoCs to build the Xtensa toolchain for. Defaults to every Xtensa chip in chips.json. + required: false + default: "" + xtensa-default: + description: When "true", make the Xtensa toolchain the rustup default. + required: false + default: "false" + riscv: + description: When "true", install the RISC-V toolchain. + required: false + default: "false" + riscv-targets: + description: '"all" for every RISC-V triple in chips.json, "none" for a host-only toolchain, or an explicit comma-separated list.' + required: false + default: all + riscv-extra-targets: + description: Comma-separated extra triples to append to the RISC-V toolchain. + required: false + default: "" + riscv-components: + description: >- + Comma-separated components for the RISC-V toolchain. + required: false + default: rust-src + +outputs: + xtensa-version: + description: The Xtensa toolchain version for the selected channel. + value: ${{ steps.pins.outputs.xtensa-version }} + riscv-toolchain: + description: The rustup channel used for the RISC-V toolchain. + value: ${{ steps.pins.outputs.riscv-toolchain }} + +runs: + using: composite + steps: + # Pins for the two channels CI exercises. api-baseline-generation.yml pins + # the current Xtensa version inline as well, since it runs against trees + # that may not contain this action. + - id: pins + shell: bash + run: | + case '${{ inputs.channel }}' in + current) + echo "xtensa-version=${{ inputs.current-xtensa-version }}" >> "$GITHUB_OUTPUT" + echo "riscv-toolchain=stable" >> "$GITHUB_OUTPUT" + ;; + msrv) + # The first `rust-version` in the file is the one in `[package]`. + MSRV=$(sed -n 's/^rust-version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' esp-hal/Cargo.toml | head -n1) + if [ -z "$MSRV" ]; then + echo "::error::Could not read rust-version from esp-hal/Cargo.toml" + exit 1 + fi + echo "Resolved MSRV: $MSRV" + # espup resolves this three-component prefix to the newest 1.x.y.* release. + echo "xtensa-version=$MSRV" >> "$GITHUB_OUTPUT" + echo "riscv-toolchain=$MSRV" >> "$GITHUB_OUTPUT" + ;; + *) + echo "::error::Unknown channel '${{ inputs.channel }}'; expected 'current' or 'msrv'" + exit 1 + ;; + esac + + # Chip-derived defaults, so adding a chip to chips.json does not require + # touching any workflow. + - id: chips + shell: bash + run: | + BUILDTARGETS='${{ inputs.xtensa-buildtargets }}' + if [ -z "$BUILDTARGETS" ]; then + BUILDTARGETS=$(jq -r '[.[] | select(.arch == "xtensa") | .soc] | join(",")' .github/chips.json) + fi + echo "buildtargets=$BUILDTARGETS" >> "$GITHUB_OUTPUT" + + case '${{ inputs.riscv-targets }}' in + all) + # Order-preserving dedupe, so the triple list reads the same as + # chips.json rather than being alphabetised. + TARGETS=$(jq -r ' + [.[] | select(.arch == "riscv") | ."rust-target"] + | reduce .[] as $t ([]; if index($t) then . else . + [$t] end) + | join(",")' .github/chips.json) + ;; + none) TARGETS="" ;; + *) TARGETS='${{ inputs.riscv-targets }}' ;; + esac + + EXTRA='${{ inputs.riscv-extra-targets }}' + if [ -n "$EXTRA" ]; then + TARGETS="${TARGETS:+$TARGETS,}$EXTRA" + fi + echo "targets=$TARGETS" >> "$GITHUB_OUTPUT" + + - if: inputs.xtensa == 'true' + uses: esp-rs/xtensa-toolchain@v1.6 + with: + buildtargets: ${{ steps.chips.outputs.buildtargets }} + default: ${{ inputs.xtensa-default }} + version: ${{ steps.pins.outputs.xtensa-version }} + + - if: inputs.riscv == 'true' + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ steps.pins.outputs.riscv-toolchain }} + targets: ${{ steps.chips.outputs.targets }} + components: ${{ inputs.riscv-components }} diff --git a/.github/chips.json b/.github/chips.json new file mode 100644 index 00000000000..e383b7d4c7a --- /dev/null +++ b/.github/chips.json @@ -0,0 +1,13 @@ +[ + { "soc": "esp32", "arch": "xtensa", "rust-target": "xtensa-esp32-none-elf", "ci-toolchain": "esp", "hil-runner": "esp32-jtag" }, + { "soc": "esp32s2", "arch": "xtensa", "rust-target": "xtensa-esp32s2-none-elf", "ci-toolchain": "esp", "hil-runner": "esp32s2-jtag" }, + { "soc": "esp32s3", "arch": "xtensa", "rust-target": "xtensa-esp32s3-none-elf", "ci-toolchain": "esp", "quick": true, "hil-runner": "esp32s3-usb", "hil-radio-runner": "esp32s3-radio" }, + { "soc": "esp32c2", "arch": "riscv", "rust-target": "riscv32imc-unknown-none-elf", "ci-toolchain": "esp", "hil-runner": "esp32c2-jtag" }, + { "soc": "esp32c3", "arch": "riscv", "rust-target": "riscv32imc-unknown-none-elf", "ci-toolchain": "esp", "hil-runner": "esp32c3-usb" }, + { "soc": "esp32c5", "arch": "riscv", "rust-target": "riscv32imac-unknown-none-elf", "ci-toolchain": "stable", "hil-runner": "esp32c5-usb" }, + { "soc": "esp32c6", "arch": "riscv", "rust-target": "riscv32imac-unknown-none-elf", "ci-toolchain": "stable", "quick": true, "hil-runner": "esp32c6-usb", "hil-radio-runner": "esp32c6-radio" }, + { "soc": "esp32c61", "arch": "riscv", "rust-target": "riscv32imac-unknown-none-elf", "ci-toolchain": "stable", "hil-runner": "esp32c61-usb" }, + { "soc": "esp32h2", "arch": "riscv", "rust-target": "riscv32imac-unknown-none-elf", "ci-toolchain": "stable", "hil-runner": "esp32h2-usb" }, + { "soc": "esp32p4", "arch": "riscv", "rust-target": "riscv32imafc-unknown-none-elf", "ci-toolchain": "stable", "hil-runner": "esp32p4" }, + { "soc": "esp32s31", "arch": "riscv", "rust-target": "riscv32imafc-unknown-none-elf", "ci-toolchain": "stable", "hil-runner": "esp32s31-jtag" } +] diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c3cd4b29b7b..56b0adb41c8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,6 +1,6 @@ # esp-hal agent instructions -Bare-metal `no_std` Rust HAL for Espressif SoCs. MSRV: **1.95.0** (source: `MSRV` env in `.github/workflows/ci.yml`). +Bare-metal `no_std` Rust HAL for Espressif SoCs. MSRV: see `rust-version` in `esp-hal/Cargo.toml`. ## Chip reference @@ -128,5 +128,6 @@ Prefer these over `#[cfg(feature = "esp32c3")]` where possible. - `documentation/API-DOC-RULES.md` — API / rustdoc writing rules (STE, EMoS, item docs) - `documentation/CONTRIBUTING.md` — contribution workflow - `xtask/README.md` — metadata annotations and xtask usage -- `.github/workflows/ci.yml` — CI steps and MSRV +- `.github/workflows/ci.yml` — CI steps +- `.github/chips.json` — chip list CI builds from (arch, rust target, CI build group, HIL runner labels) - `esp-metadata/devices/*.toml` — per-chip peripheral definitions diff --git a/.github/scripts/hil-gate.js b/.github/scripts/hil-gate.js index c4f017cf76d..2f7576c5092 100644 --- a/.github/scripts/hil-gate.js +++ b/.github/scripts/hil-gate.js @@ -2,7 +2,7 @@ const RUN_TESTS_STEP = "Run Tests"; function isHilRunMatrixJob(name) { // Matches "hil-run (…)" but not "hil-run-radio (…)". - return /^hil-run \(/i.test(String(name || "")); + return /(?:^|\/\s*)hil-run \(/i.test(String(name || "")); } function classifyMatrixJob(job) { @@ -14,21 +14,13 @@ function classifyMatrixJob(job) { const conclusion = runTests.conclusion; if (conclusion === "skipped") { - // A successful job intentionally skipped this chip because it had no ELFs. - // An unsuccessful job skipped this step because an earlier step failed. + // The guard step skips a chip that produced no ELFs. A job that failed, was + // cancelled or timed out leaves the same skipped step behind, so only a + // successful job means "not tested". if (job.conclusion === "success") { return { kind: "skipped" }; } - if ( - job.conclusion === "failure" || - job.conclusion === "cancelled" || - job.conclusion === null - ) { - return { kind: "failed" }; - } - return { - error: `job "${job.name}" has skipped "${RUN_TESTS_STEP}" step and unexpected conclusion: ${job.conclusion}`, - }; + return { kind: "failed" }; } if (conclusion === "success") { return { kind: "passed" }; @@ -75,8 +67,12 @@ async function listWorkflowRunJobs(github, context) { page, }); - jobs.push(...(data.jobs || [])); - if (jobs.length >= data.total_count) { + const page_jobs = data.jobs || []; + jobs.push(...page_jobs); + // An empty page ends the walk even when total_count claims more: the count + // can drift while the run is still going, and trusting it alone spins here + // until the job times out. + if (page_jobs.length === 0 || jobs.length >= data.total_count) { break; } page += 1; diff --git a/.github/scripts/hil-parse.js b/.github/scripts/hil-parse.js index e25a2190c1c..0df1a2ea5fa 100644 --- a/.github/scripts/hil-parse.js +++ b/.github/scripts/hil-parse.js @@ -1,26 +1,15 @@ -const DEFAULT_ALLOWED = [ - "esp32c2", - "esp32c3", - "esp32c5", - "esp32c6", - "esp32c61", - "esp32h2", - "esp32p4", - "esp32s31", - "esp32", - "esp32s2", - "esp32s3", -]; +const DEFAULT_ALLOWED = require("../chips.json").map((c) => c.soc); + +// Longest first, because "hil-test" is a prefix of "hil-test-radio". +const PACKAGES = ["hil-test-radio", "hil-test"]; + +// What an ELF basename can look like, plus `::` for the `test::filter` form +// xtask accepts. +const TEST_NAME = /^[A-Za-z0-9_:-]+$/; function parsePackage(body) { const text = String(body || "").trim().toLowerCase(); - if (text.includes("hil-test-radio")) { - return "hil-test-radio"; - } - if (text.includes("hil-test")) { - return "hil-test"; - } - return "hil-test"; + return PACKAGES.find((pkg) => text.includes(pkg)) || "hil-test"; } function parseTests(body) { @@ -32,6 +21,10 @@ function parseTests(body) { .split(/[,\s]+/) .map((s) => s.trim()) .filter(Boolean) + // `--tests` runs to the end of the line, so a package selector written + // after it would otherwise be picked up as a test name. + .filter((s) => !PACKAGES.includes(s.toLowerCase())) + .filter((s) => TEST_NAME.test(s)) .join(","); } diff --git a/.github/scripts/hil-status.js b/.github/scripts/hil-status.js index 40df5f3e7ff..72a8dd2cd4c 100644 --- a/.github/scripts/hil-status.js +++ b/.github/scripts/hil-status.js @@ -17,7 +17,7 @@ async function pollRun({ runId, commentId, kind, - maxPolls = 60, + maxPolls = 180, pollIntervalMs = 15000, }) { const { owner, repo } = context.repo; @@ -25,7 +25,8 @@ async function pollRun({ let conclusion = null; - // Poll up to ~15 minutes by default (60 * 15s) + // Poll up to ~45 minutes by default (180 * 15s), matching what dispatch.yml + // passes for every HIL mode. for (let i = 0; i < maxPolls; i++) { await delay(pollIntervalMs); diff --git a/.github/workflows/api-baseline-check.yml b/.github/workflows/api-baseline-check.yml index b0ce4d95e54..76ed3c89369 100644 --- a/.github/workflows/api-baseline-check.yml +++ b/.github/workflows/api-baseline-check.yml @@ -22,17 +22,10 @@ jobs: steps: - uses: actions/checkout@v6 - # Install the Rust toolchain for Xtensa devices: - - uses: esp-rs/xtensa-toolchain@v1.6 + - uses: ./.github/actions/setup-toolchains with: - version: 1.97.0.0 - - # Install the Rust stable toolchain for RISC-V devices: - - uses: dtolnay/rust-toolchain@v1 - with: - target: riscv32imc-unknown-none-elf,riscv32imac-unknown-none-elf,riscv32imafc-unknown-none-elf - toolchain: stable - components: rust-src + xtensa: "true" + riscv: "true" - name: Semver-Check run: | diff --git a/.github/workflows/binary-size.yml b/.github/workflows/binary-size.yml index 7644c743572..b7526d1ebf1 100644 --- a/.github/workflows/binary-size.yml +++ b/.github/workflows/binary-size.yml @@ -51,45 +51,31 @@ jobs: soc: ${{ fromJSON(inputs.chips) }} steps: - # Install the Rust toolchain for RISC-V devices: + # Checked out first so the chip metadata is available to the step below. + - name: Checkout Repo (Initial) + uses: actions/checkout@v6 + - name: Resolve Rust target id: target run: | - case "${{ matrix.soc }}" in - esp32|esp32s2|esp32s3) - echo "rust-target=xtensa-${{ matrix.soc }}-none-elf" >> $GITHUB_OUTPUT - ;; - esp32c2|esp32c3) - echo "rust-target=riscv32imc-unknown-none-elf" >> $GITHUB_OUTPUT - ;; - esp32c5|esp32c6|esp32c61|esp32h2) - echo "rust-target=riscv32imac-unknown-none-elf" >> $GITHUB_OUTPUT - ;; - esp32p4|esp32s31) - echo "rust-target=riscv32imafc-unknown-none-elf" >> $GITHUB_OUTPUT - ;; - *) - echo "Error: Unknown SoC: ${{ matrix.soc }}" - exit 1 - ;; - esac - - - name: Setup Rust (RISC-V) - if: ${{ !contains(fromJson('["esp32", "esp32s2", "esp32s3"]'), matrix.soc) }} - uses: dtolnay/rust-toolchain@v1 - with: - target: ${{ steps.target.outputs.rust-target }} - toolchain: stable - components: rust-src, llvm-tools - - # Install the Rust toolchain for Xtensa devices: - - name: Setup Rust (Xtensa) - if: contains(fromJson('["esp32", "esp32s2", "esp32s3"]'), matrix.soc) - uses: esp-rs/xtensa-toolchain@v1.6 + ENTRY=$(jq -c --arg s "${{ matrix.soc }}" '.[] | select(.soc == $s)' .github/chips.json) + if [ -z "$ENTRY" ]; then + echo "Error: Unknown SoC: ${{ matrix.soc }}" + exit 1 + fi + echo "rust-target=$(echo "$ENTRY" | jq -r '."rust-target"')" >> $GITHUB_OUTPUT + echo "arch=$(echo "$ENTRY" | jq -r '.arch')" >> $GITHUB_OUTPUT + + # Only the toolchain for this leg's architecture is installed. + - name: Setup Rust + uses: ./.github/actions/setup-toolchains with: - buildtargets: ${{ matrix.soc }} - default: true - version: 1.97.0.0 + xtensa: ${{ steps.target.outputs.arch == 'xtensa' }} + xtensa-buildtargets: ${{ matrix.soc }} + xtensa-default: "true" + riscv: ${{ steps.target.outputs.arch == 'riscv' }} + riscv-targets: ${{ steps.target.outputs.rust-target }} + riscv-components: rust-src, llvm-tools - name: Install Binutils and Hub run: | @@ -97,10 +83,7 @@ jobs: sudo apt-get update sudo apt-get install -y hub - # Checkout repo & PR - - name: Checkout Repo (Initial) - uses: actions/checkout@v6 - + # Checkout the PR on top of the initial checkout above. # context https://github.com/actions/checkout/issues/331 - name: Checkout Pull Request (PR Code at Root) run: hub pr checkout ${{ github.event.inputs.pr_number }} diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 8873a860a50..146951d8a14 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -12,8 +12,26 @@ env: DEFMT_LOG: trace jobs: + chips: + runs-on: ubuntu-latest + outputs: + riscv: ${{ steps.resolve.outputs.riscv }} + xtensa: ${{ steps.resolve.outputs.xtensa }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - id: resolve + run: | + for arch in riscv xtensa; do + echo "$arch=$(jq -c --arg a "$arch" \ + '[.[] | select(.arch == $a) | .soc]' .github/chips.json)" >> "$GITHUB_OUTPUT" + done + esp-hal-riscv-nightly: name: esp-hal | nightly (${{ matrix.device }}) + needs: chips runs-on: ubuntu-latest env: CI: 1 @@ -26,24 +44,14 @@ jobs: strategy: fail-fast: false matrix: - device: - [ - "esp32c2", - "esp32c3", - "esp32c5", - "esp32c6", - "esp32c61", - "esp32h2", - "esp32p4", - "esp32s31", - ] + device: ${{ fromJSON(needs.chips.outputs.riscv) }} steps: - uses: actions/checkout@v6 # Install the Rust nightly toolchain for RISC-V devices: - uses: ./.github/actions/setup-nightly with: - targets: riscv32imc-unknown-none-elf,riscv32imac-unknown-none-elf,riscv32imafc-unknown-none-elf + targets: all components: rust-src, clippy, rustfmt alias: "true" @@ -53,6 +61,7 @@ jobs: esp-hal-xtensa-clippy: name: esp-hal | xtensa clippy (${{ matrix.device }}) + needs: chips runs-on: ubuntu-latest env: CI: 1 @@ -64,14 +73,13 @@ jobs: strategy: fail-fast: false matrix: - device: ["esp32", "esp32s2", "esp32s3"] + device: ${{ fromJSON(needs.chips.outputs.xtensa) }} steps: - uses: actions/checkout@v6 - # Install the Rust toolchain for Xtensa devices: - - uses: esp-rs/xtensa-toolchain@v1.6 + - uses: ./.github/actions/setup-toolchains with: - version: 1.97.0.0 + xtensa: "true" - name: Lint esp-hal on Xtensa chip run: cargo xtask lint-packages --chips ${{ matrix.device }} --toolchain esp @@ -82,10 +90,9 @@ jobs: steps: - uses: actions/checkout@v6 - # Install the Rust toolchain for Xtensa devices: - - uses: esp-rs/xtensa-toolchain@v1.6 + - uses: ./.github/actions/setup-toolchains with: - version: 1.97.0.0 + xtensa: "true" # Check all chips at once only for esp-hal - name: Run check-global-symbols @@ -94,7 +101,13 @@ jobs: create-issue: name: Create GitHub Issue if any job failed runs-on: ubuntu-latest - needs: [esp-hal-riscv-nightly, esp-hal-xtensa-clippy, check-global-symbols] + needs: + [ + chips, + esp-hal-riscv-nightly, + esp-hal-xtensa-clippy, + check-global-symbols, + ] if: ${{ failure() }} steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 501d492f6c4..6289a3c3289 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,13 +1,13 @@ # NOTE: # -# When adding support for a new chip to `esp-hal`, there are a number of -# updates which must be made to the CI workflow in order to reflect this; the -# changes are: +# Adding support for a new chip to `esp-hal` only needs `.github/chips.json`, +# which feeds the 'esp-hal', 'docs' and 'msrv' jobs, the toolchain setup +# actions, ci-nightly, binary-size and the HIL matrix. # -# 1.) In the 'esp-hal' job, add the appropriate build command. -# 1a.) If the device has a low-power core (which is supported in -# `esp-lp-hal`), then update the `if` condition to build prerequisites. -# 2.) In the 'msrv' job, add checks as needed for the new chip. +# `ci-toolchain` picks the 'esp-hal' build group the chip lands in, which is +# also how the load is balanced across the runners. Give the chip a +# `hil-runner` (and `hil-radio-runner`) once it has a board on the rack; +# without one it is built but not HIL tested. name: CI @@ -20,7 +20,6 @@ on: env: CARGO_TERM_COLOR: always GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MSRV: "1.95.0" DEFMT_LOG: trace PROBE_RS_CONFIG_PRESET: local-hil @@ -71,20 +70,18 @@ jobs: PACKAGES="" if [ -n "$LABELS" ]; then - PACKAGES=$(echo "$LABELS" \ - | grep -oP '(?<=^breaking-change-)[a-z0-9-]+$' || true \ - | tr '\n' ',' \ - | sed 's/,$//') + PACKAGES=$(printf '%s\n' "$LABELS" \ + | { grep -oP '(?<=^breaking-change-)[a-z0-9-]+$' || true; } \ + | tr '\n' ' ' | xargs) fi LABELS_SPACE=$(echo "$LABELS" | tr '\n' ' ' | xargs) - PACKAGES_SPACE=$(echo "$PACKAGES" | tr ',' ' ') echo "labels=$LABELS_SPACE" >> "$GITHUB_OUTPUT" - echo "packages=$PACKAGES_SPACE" >> "$GITHUB_OUTPUT" + echo "packages=$PACKAGES" >> "$GITHUB_OUTPUT" echo "Detected labels: $LABELS_SPACE" - echo "Detected packages: $PACKAGES_SPACE" + echo "Detected packages: $PACKAGES" - name: Decide what to run id: decide @@ -139,28 +136,17 @@ jobs: strategy: matrix: - # load-blance runners a bit - group: - - chips: [esp32, esp32s2, esp32s3, esp32c2, esp32c3] - toolchain: esp - - chips: [esp32c5, esp32c6, esp32c61, esp32h2, esp32p4, esp32s31] - toolchain: stable + # Which chips each toolchain builds comes from `ci-toolchain` in + # .github/chips.json, which is also how the runner load is balanced. + toolchain: [esp, stable] steps: - uses: actions/checkout@v6 - # Install the Rust toolchain for Xtensa devices: - - uses: esp-rs/xtensa-toolchain@v1.6 - if: matrix.group.toolchain == 'esp' - with: - version: 1.97.0.0 - - # Install the Rust stable toolchain for RISC-V devices: - - uses: dtolnay/rust-toolchain@v1 + - uses: ./.github/actions/setup-toolchains with: - target: riscv32imc-unknown-none-elf,riscv32imac-unknown-none-elf,riscv32imafc-unknown-none-elf - toolchain: stable - components: rust-src + xtensa: ${{ matrix.toolchain == 'esp' }} + riscv: "true" - name: Setup cargo-batch run: | @@ -168,11 +154,18 @@ jobs: cargo install --git https://github.com/embassy-rs/cargo-batch cargo --bin cargo-batch --locked --force fi + - name: Resolve chips for this toolchain + id: chips + shell: bash + run: | + echo "chips=$(jq -r --arg t '${{ matrix.toolchain }}' \ + '[.[] | select(."ci-toolchain" == $t) | .soc] | join(" ")' .github/chips.json)" >> "$GITHUB_OUTPUT" + - name: Build and Check shell: bash env: - CHIPS: ${{ join(matrix.group.chips, ' ') }} - TOOLCHAIN: ${{ matrix.group.toolchain }} + CHIPS: ${{ steps.chips.outputs.chips }} + TOOLCHAIN: ${{ matrix.toolchain }} run: | # lints and docs are checked separately for chip in $CHIPS; do @@ -194,54 +187,52 @@ jobs: steps: - uses: actions/checkout@v6 - # Install the Rust toolchain for Xtensa devices: - - uses: esp-rs/xtensa-toolchain@v1.6 + # Docs mix toolchains: Xtensa builds on the MSRV pin while + # RISC-V builds on stable. + - uses: ./.github/actions/setup-toolchains with: - version: 1.95.0.0 + channel: msrv + xtensa: "true" - # Install the Rust stable toolchain for RISC-V devices: - - uses: dtolnay/rust-toolchain@v1 + - uses: ./.github/actions/setup-toolchains with: - target: riscv32imc-unknown-none-elf,riscv32imac-unknown-none-elf,riscv32imafc-unknown-none-elf - toolchain: stable - components: rust-src + riscv: "true" + # Install the Rust nightly toolchain for RISC-V devices: - uses: ./.github/actions/setup-nightly with: - targets: riscv32imc-unknown-none-elf,riscv32imac-unknown-none-elf,riscv32imafc-unknown-none-elf + targets: all components: rust-src alias: "true" - - name: Build Xtensa docs - if: matrix.group == 'xtensa' - shell: bash - run: cargo xtask build documentation --chips esp32,esp32s2,esp32s3 + # The xtensa and riscv build disjoint chip sets, so give them + # separate cache entries + - uses: Swatinem/rust-cache@v2 + with: + shared-key: docs-${{ matrix.group }} + # Every merge_group run gets a unique ephemeral ref, so a cache saved + # there can never be restored. Restore everywhere, save elsewhere. + save-if: ${{ github.event_name != 'merge_group' }} - - name: Build RISC-V docs - if: matrix.group == 'riscv' + - name: Resolve chips for this group + id: chips shell: bash - run: cargo xtask build documentation --chips esp32c2,esp32c3,esp32c5,esp32c6,esp32c61,esp32h2,esp32p4,esp32s31 + run: | + echo "chips=$(jq -r --arg a '${{ matrix.group }}' \ + '[.[] | select(.arch == $a) | .soc] | join(",")' .github/chips.json)" >> "$GITHUB_OUTPUT" - - name: Run Xtensa doc tests - if: matrix.group == 'xtensa' + - name: Build docs shell: bash - run: | - cargo xtask run doc-tests esp32 - cargo xtask run doc-tests esp32s2 - cargo xtask run doc-tests esp32s3 + run: cargo xtask build documentation --chips ${{ steps.chips.outputs.chips }} - - name: Run RISC-V doc tests - if: matrix.group == 'riscv' + - name: Run doc tests shell: bash + env: + CHIPS: ${{ steps.chips.outputs.chips }} run: | - cargo xtask run doc-tests esp32c2 - cargo xtask run doc-tests esp32c3 - cargo xtask run doc-tests esp32c5 - cargo xtask run doc-tests esp32c6 - cargo xtask run doc-tests esp32c61 - cargo xtask run doc-tests esp32h2 - cargo xtask run doc-tests esp32p4 - cargo xtask run doc-tests esp32s31 + for chip in ${CHIPS//,/ }; do + cargo xtask run doc-tests "$chip" + done # -------------------------------------------------------------------------- # MSRV @@ -261,36 +252,40 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: esp-rs/xtensa-toolchain@v1.6 - if: matrix.group == 'xtensa' + - id: toolchains + uses: ./.github/actions/setup-toolchains with: - version: ${{ env.MSRV }} + channel: msrv + xtensa: ${{ matrix.group == 'xtensa' }} + riscv: ${{ matrix.group == 'riscv' }} + riscv-extra-targets: x86_64-unknown-linux-gnu + riscv-components: rust-src,clippy - name: esp toolchain checks if: matrix.group == 'xtensa' run: rustc +esp --version --verbose - - uses: dtolnay/rust-toolchain@v1 - if: matrix.group == 'riscv' - with: - target: riscv32imc-unknown-none-elf,riscv32imac-unknown-none-elf,riscv32imafc-unknown-none-elf,x86_64-unknown-linux-gnu - toolchain: ${{ env.MSRV }} - components: rust-src,clippy - - name: Stable toolchain checks if: matrix.group == 'riscv' - run: rustc +${{ env.MSRV }} --version --verbose + run: rustc +${{ steps.toolchains.outputs.riscv-toolchain }} --version --verbose - # Verify the MSRV for all chips by running a lint session - - name: msrv RISC-V (esp-hal) - if: matrix.group == 'riscv' + # The xtensa and riscv lint disjoint chip sets under different + # toolchains, so keep their caches separate. + - uses: Swatinem/rust-cache@v2 + with: + shared-key: msrv-${{ matrix.group }} + save-if: ${{ github.event_name != 'merge_group' }} + + - name: Resolve chips for this group + id: chips run: | - cargo xtask lint-packages --chips esp32c2,esp32c3,esp32c5,esp32c6,esp32c61,esp32h2,esp32p4,esp32s31 --toolchain ${{ env.MSRV }} + echo "chips=$(jq -r --arg a '${{ matrix.group }}' \ + '[.[] | select(.arch == $a) | .soc] | join(",")' .github/chips.json)" >> "$GITHUB_OUTPUT" - - name: msrv Xtensa (esp-hal) - if: matrix.group == 'xtensa' + - name: msrv lint (esp-hal) run: | - cargo xtask lint-packages --chips esp32,esp32s2,esp32s3 --toolchain esp + cargo xtask lint-packages --chips ${{ steps.chips.outputs.chips }} \ + --toolchain ${{ matrix.group == 'xtensa' && 'esp' || steps.toolchains.outputs.riscv-toolchain }} # -------------------------------------------------------------------------- # Xtensa LLD linking @@ -316,12 +311,11 @@ jobs: steps: - uses: actions/checkout@v6 - # Install the Rust toolchain for Xtensa devices. Keep this in sync with - # the `esp-hal` build job above so the LLD linking test exercises the - # same compiler as the rest of CI. - - uses: esp-rs/xtensa-toolchain@v1.6 + # Uses the same pin as the `esp-hal` build job above, so the LLD linking + # test exercises the same compiler as the rest of CI. + - uses: ./.github/actions/setup-toolchains with: - version: 1.97.0.0 + xtensa: "true" - name: Build hello_world with LLD working-directory: examples/hello_world @@ -359,6 +353,18 @@ jobs: components: rustfmt,miri alias: "true" + # xtask funnels every package it drives into the root `target`, but the + # `extras` crates are built directly and keep their own target dirs. + - uses: Swatinem/rust-cache@v2 + with: + shared-key: host-tests + save-if: ${{ github.event_name != 'merge_group' }} + workspaces: | + . -> target + extras/bench-server -> target + extras/esp-wifishark -> target + extras/ieee802154-sniffer -> target + # Run xtask tests - name: Run xtask tests run: cd xtask && cargo test --features release @@ -415,6 +421,8 @@ jobs: # -------------------------------------------------------------------------- # Check links in .rs, .md, and .toml files + # + # Deliberately absent from `ci-result` below. link-check: needs: calculate @@ -462,273 +470,14 @@ jobs: # -------------------------------------------------------------------------- # HIL — build jobs gate artifacts; hil-gate gates device runs (≥50% rule). - hil-build: - needs: calculate - if: needs.calculate.outputs.run-hil == 'true' - runs-on: macos-m1-self-hosted - outputs: - matrix: ${{ steps.chips.outputs.matrix }} - steps: - - name: Resolve chip matrix - id: chips - run: | - ALL_CHIPS='[ - {"soc":"esp32", "rust-target":"xtensa-esp32-none-elf", "runner":"esp32-jtag", "host":"aarch64"}, - {"soc":"esp32s2", "rust-target":"xtensa-esp32s2-none-elf", "runner":"esp32s2-jtag", "host":"aarch64"}, - {"soc":"esp32s3", "rust-target":"xtensa-esp32s3-none-elf", "runner":"esp32s3-usb", "host":"aarch64"}, - {"soc":"esp32c2", "rust-target":"riscv32imc-unknown-none-elf", "runner":"esp32c2-jtag", "host":"aarch64"}, - {"soc":"esp32c3", "rust-target":"riscv32imc-unknown-none-elf", "runner":"esp32c3-usb", "host":"aarch64"}, - {"soc":"esp32c5", "rust-target":"riscv32imac-unknown-none-elf", "runner":"esp32c5-usb", "host":"aarch64"}, - {"soc":"esp32c6", "rust-target":"riscv32imac-unknown-none-elf", "runner":"esp32c6-usb", "host":"aarch64"}, - {"soc":"esp32c61","rust-target":"riscv32imac-unknown-none-elf", "runner":"esp32c61-usb", "host":"aarch64"}, - {"soc":"esp32h2", "rust-target":"riscv32imac-unknown-none-elf", "runner":"esp32h2-usb", "host":"aarch64"}, - {"soc":"esp32p4", "rust-target":"riscv32imafc-unknown-none-elf","runner":"esp32p4", "host":"aarch64"}, - {"soc":"esp32s31","rust-target":"riscv32imafc-unknown-none-elf","runner":"esp32s31-jtag", "host":"aarch64"} - ]' - echo "matrix={\"target\":$(echo "$ALL_CHIPS" | jq -c .)}" >> "$GITHUB_OUTPUT" - - - uses: actions/checkout@v6 - - - uses: esp-rs/xtensa-toolchain@v1.6 - with: - buildtargets: esp32,esp32s2,esp32s3 - default: true - version: 1.97.0.0 - - - uses: ./.github/actions/setup-nightly - with: - targets: riscv32imc-unknown-none-elf,riscv32imac-unknown-none-elf,riscv32imafc-unknown-none-elf - components: rust-src - alias: "true" - - - name: Build hil-test tests - run: | - for chip in esp32 esp32s2 esp32s3 esp32s31 esp32c2 esp32c3 esp32c5 esp32c6 esp32c61 esp32h2 esp32p4; do - if cargo xtask build tests "$chip" --help >/dev/null 2>&1; then - echo "Building ALL hil-test tests for $chip" - cargo xtask build tests "$chip" - else - echo "Skipping $chip: not supported by this ref's xtask" - fi - done - - - uses: actions/upload-artifact@v6 - with: - name: hil-tests - path: target/tests/ - if-no-files-found: error - - # Build radio HIL tests (hil-test-radio). Chips for which the radio - # package has no applicable binaries produce no output and are silently - # skipped via `if-no-files-found: ignore`. - - name: Reset tests output directory for radio build - run: rm -rf target/tests - - - name: Build hil-test-radio tests - run: | - for chip in esp32 esp32s2 esp32s3 esp32c2 esp32c3 esp32c5 esp32c6 esp32c61 esp32h2; do - if cargo xtask build tests "$chip" hil-test-radio --help >/dev/null 2>&1; then - echo "Building ALL hil-test-radio tests for $chip" - cargo xtask build tests "$chip" hil-test-radio - else - echo "Skipping $chip: not supported by this ref's xtask" - fi - done - - - uses: actions/upload-artifact@v6 - with: - name: hil-tests-radio - path: target/tests/ - if-no-files-found: ignore - - hil-devtool: + hil: needs: calculate if: needs.calculate.outputs.run-hil == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Cache Dependencies - uses: Swatinem/rust-cache@v2 - - # xtask runs on the AArch64 HIL runners as a host tool. - # Restore the binary after the dependency cache, which may replace the - # target directory while restoring its own cache. - - name: Cache xtask binary - id: xtask-cache - uses: actions/cache@v6 - with: - path: target/aarch64-unknown-linux-gnu/release/esp-devtool - key: xtask-hil-aarch64-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'xtask/**', 'xtask-mcp-macros/**') }} - - - name: Install cross-compilation target - if: steps.xtask-cache.outputs.cache-hit != 'true' - run: rustup target add aarch64-unknown-linux-gnu - - - uses: taiki-e/install-action@v2 - if: steps.xtask-cache.outputs.cache-hit != 'true' - with: - tool: cross - - - name: Build xtask for HIL runners - if: steps.xtask-cache.outputs.cache-hit != 'true' - run: cross build --release --target aarch64-unknown-linux-gnu -p esp-devtool - - - uses: actions/upload-artifact@v6 - with: - name: xtask-aarch64 - path: target/aarch64-unknown-linux-gnu/release/esp-devtool - if-no-files-found: error - - # Device runs — aggregated by hil-gate (see below). - hil-run: - needs: [hil-build, hil-devtool] - runs-on: - labels: [self-hosted, "${{ matrix.target.runner }}"] - strategy: - fail-fast: false - matrix: ${{ fromJSON(needs.hil-build.outputs.matrix) }} - steps: - - uses: actions/download-artifact@v4 - with: - name: hil-tests - path: tests/ - continue-on-error: true - - - uses: actions/download-artifact@v4 - with: - name: xtask-${{ matrix.target.host }} - - - name: Skip if this target wasn't built - id: guard - run: | - if [ ! -d "tests/${{ matrix.target.soc }}" ]; then - echo "skip=true" >> $GITHUB_OUTPUT - echo "No tests for ${{ matrix.target.soc }} in this run; skipping." - fi - - - name: Run Tests - if: steps.guard.outputs.skip != 'true' - run: | - [ -f ~/setup.sh ] && source ~/setup.sh - - export PATH=$PATH:/home/espressif/.cargo/bin - chmod +x esp-devtool - ./esp-devtool run elfs ${{ matrix.target.soc }} tests/${{ matrix.target.soc }} - - - name: Clean up - if: always() - run: | - rm -rf tests/ || true - rm -f esp-devtool || true - - # Radio HIL device runs — wifi/BT capable chips on dedicated radio runners. - # Not gated by hil-gate or ci-result. - hil-run-radio: - needs: [hil-build, hil-devtool] - runs-on: - labels: [self-hosted, "${{ matrix.target.runner }}"] - strategy: - fail-fast: false - matrix: - target: - - soc: esp32c6 - runner: esp32c6-radio - host: aarch64 - - soc: esp32s3 - runner: esp32s3-radio - host: aarch64 - steps: - - uses: actions/download-artifact@v4 - with: - name: hil-tests-radio - path: tests/ - continue-on-error: true - - - uses: actions/download-artifact@v4 - with: - name: xtask-${{ matrix.target.host }} - - - name: Skip if this target wasn't built - id: guard - run: | - if [ ! -d "tests/${{ matrix.target.soc }}" ]; then - echo "skip=true" >> $GITHUB_OUTPUT - echo "No radio tests for ${{ matrix.target.soc }} in this run; skipping." - fi - - - name: Reset ESP USB-JTAG devices - if: steps.guard.outputs.skip != 'true' - timeout-minutes: 1 - shell: bash - run: | - # The support firmware runs forever on the harness probe; a previous - # run can leave the in-chip USB-Serial-JTAG bridge wedged so the next - # flash hangs. Issue USBDEVFS_RESET to force a clean re-enumeration. - python3 - <<'PY' - import fcntl, glob, os, time - USBDEVFS_RESET = (ord('U') << 8) | 20 # _IO('U', 20) - reset_any = False - for dev in glob.glob('/sys/bus/usb/devices/*'): - try: - vid = open(os.path.join(dev, 'idVendor')).read().strip().lower() - pid = open(os.path.join(dev, 'idProduct')).read().strip().lower() - except OSError: - continue - if (vid, pid) != ('303a', '1001'): - continue - busnum = int(open(os.path.join(dev, 'busnum')).read()) - devnum = int(open(os.path.join(dev, 'devnum')).read()) - node = f'/dev/bus/usb/{busnum:03d}/{devnum:03d}' - try: - with open(node, 'wb') as f: - fcntl.ioctl(f, USBDEVFS_RESET, 0) - print(f'reset {node} ({vid}:{pid})') - reset_any = True - except OSError as e: - print(f'failed to reset {node}: {e}') - if reset_any: - time.sleep(2) # allow udev to re-enumerate before probe-rs runs - PY - - - name: Run Radio Tests - if: steps.guard.outputs.skip != 'true' - timeout-minutes: 3 - run: | - [ -f ~/setup.sh ] && source ~/setup.sh - - export PATH=$PATH:/home/espressif/.cargo/bin - chmod +x esp-devtool - - ./esp-devtool run elfs ${{ matrix.target.soc }} tests/${{ matrix.target.soc }} - - - name: Clean up - if: always() - run: | - rm -rf tests/ || true - rm -f esp-devtool || true - - # Merge gate for hil-run matrix legs. Blocks when ≥50% of legs that actually - # ran tests failed or were cancelled. Skipped legs (no ELFs for that chip) - # are excluded. hil-run-radio is not part of this gate. - hil-gate: - needs: [hil-build, hil-run] - if: > - always() && - needs.hil-build.result == 'success' && - needs.hil-run.result != 'skipped' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 1 - - - name: Evaluate hil-run results - uses: actions/github-script@v7 - with: - script: | - const { evaluateHilGate } = require('./.github/scripts/hil-gate.js'); - await evaluateHilGate({ github, context, core }); + uses: ./.github/workflows/hil.yml + with: + chips: all + package: all + secrets: inherit # -------------------------------------------------------------------------- # Gate job for branch protection @@ -737,21 +486,24 @@ jobs: # Skipped jobs (due to calculate outputs) are treated as success. # Failed or cancelled jobs cause this gate to fail. # - # hil-build gates test compilation, and hil-devtool gates the runner tool. - # hil-gate gates device runs when ≥50% of executed hil-run matrix legs fail. - # hil-run-radio is informational only. + # The `hil` job is the reusable hil.yml workflow, so it covers test + # compilation and the runner tool as well; its hil-gate job blocks when ≥50% + # of executed hil-run matrix legs fail. hil-run-radio is informational only. ci-result: if: ${{ always() }} needs: + # `calculate` gates everything below it, so a failure there leaves every + # other job `skipped` — which this gate treats as success. Depend on it + # directly, or a broken decision step passes CI without running any. + - calculate - esp-hal - docs - msrv + - xtensa-lld - host-tests - changelog - semver-check - - hil-build - - hil-devtool - - hil-gate + - hil runs-on: ubuntu-latest steps: - name: Check CI results diff --git a/.github/workflows/dispatch.yml b/.github/workflows/dispatch.yml index 69af7814729..bf39ed0c334 100644 --- a/.github/workflows/dispatch.yml +++ b/.github/workflows/dispatch.yml @@ -238,240 +238,70 @@ jobs: } core.setOutput('allowed', allowed ? 'true' : 'false'); - hil-quick: + hil: needs: auth if: github.event_name == 'issue_comment' && needs.auth.outputs.allowed == 'true' && - startsWith(github.event.comment.body, '/hil quick') + startsWith(github.event.comment.body, '/hil ') runs-on: ubuntu-latest outputs: run_id: ${{ steps.find-run.outputs.run_id }} comment_id: ${{ steps.comment.outputs.comment-id }} + kind: ${{ steps.mode.outputs.kind }} + max_polls: ${{ steps.mode.outputs.max_polls }} steps: - uses: actions/checkout@v6 # for `require` with: fetch-depth: 1 - - name: Parse tests - id: parse-tests + - name: Resolve dispatch parameters + id: mode uses: actions/github-script@v7 with: script: | - const { parseTests, parsePackage } = require('./.github/scripts/hil-parse.js'); + const { parseChips, parseTests, parsePackage } = require('./.github/scripts/hil-parse.js'); + const chips = require('./.github/chips.json'); const body = context.payload.comment.body; - core.setOutput('tests', parseTests(body)); - core.setOutput('package', parsePackage(body)); - - - name: Dispatch HIL (quick) - uses: benc-uk/workflow-dispatch@v1 - with: - workflow: hil.yml - ref: ${{ github.event.repository.default_branch }} - inputs: | - { - "repository": "${{ github.repository }}", - "branch": "refs/pull/${{ github.event.issue.number }}/head", - "pr_number": "${{ github.event.issue.number }}", - "chips": "esp32c6 esp32s3", - "tests": "${{ steps.parse-tests.outputs.tests }}", - "package": "${{ steps.parse-tests.outputs.package }}", - "distinct_id": "${{ env.DISTINCT_ID }}" + // Even a quick run can legitimately take well over 15 minutes once + // queueing is counted, and a full one always does, so give every + // mode the same 45 minute budget (180 * 15s). + let mode, selected, kind, maxPolls = 180, error = ''; + if (body.startsWith('/hil quick')) { + mode = 'quick'; + selected = chips.filter(c => c.quick).map(c => c.soc).join(' '); + kind = 'HIL (quick)'; + } else if (body.startsWith('/hil full')) { + mode = 'full'; + selected = chips.map(c => c.soc).join(' '); + kind = 'HIL (full)'; + } else { + mode = 'chips'; + const res = parseChips(body); + selected = res.chips; + kind = 'HIL (per-chip)'; + error = res.error; } - - name: Find HIL run URL (quick) - id: find-run - uses: actions/github-script@v7 - with: - script: | - const { findHilRun } = require('./.github/scripts/hil-find-run.js'); - - const { runId, body } = await findHilRun({ - github, - context, - pr: context.payload.issue.number, - selector: 'quick', - distinctId: '${{ env.DISTINCT_ID }}', - }); - - core.setOutput('run_id', runId); - core.setOutput('body', body); - - - name: Confirm in PR - id: comment - uses: peter-evans/create-or-update-comment@v4 - with: - issue-number: ${{ github.event.issue.number }} - body: ${{ steps.find-run.outputs.body }} - - hil-quick-status: - needs: hil-quick - if: needs.hil-quick.outputs.run_id != '' && needs.hil-quick.outputs.comment_id != '' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 # for `require` - with: - fetch-depth: 1 - - - name: Wait for quick HIL run and update comment - uses: actions/github-script@v7 - with: - script: | - const { pollRun } = require('./.github/scripts/hil-status.js'); - - await pollRun({ - github, - context, - runId: Number('${{ needs.hil-quick.outputs.run_id }}'), - commentId: Number('${{ needs.hil-quick.outputs.comment_id }}'), - kind: 'HIL (quick)', - // Quick runs can legitimately take >15min, especially with queueing. - maxPolls: 180, - }); - - hil-full: - needs: auth - if: github.event_name == 'issue_comment' && - needs.auth.outputs.allowed == 'true' && - startsWith(github.event.comment.body, '/hil full') - runs-on: ubuntu-latest - outputs: - run_id: ${{ steps.find-run.outputs.run_id }} - comment_id: ${{ steps.comment.outputs.comment-id }} - steps: - - uses: actions/checkout@v6 # for `require` - with: - fetch-depth: 1 - - - name: Parse tests - id: parse-tests - uses: actions/github-script@v7 - with: - script: | - const { parseTests, parsePackage } = require('./.github/scripts/hil-parse.js'); - const body = context.payload.comment.body; - - core.setOutput('tests', parseTests(body)); - core.setOutput('package', parsePackage(body)); - - - name: Dispatch HIL (full) - uses: benc-uk/workflow-dispatch@v1 - with: - workflow: hil.yml - ref: ${{ github.event.repository.default_branch }} - inputs: | - { - "repository": "${{ github.repository }}", - "branch": "refs/pull/${{ github.event.issue.number }}/head", - "pr_number": "${{ github.event.issue.number }}", - "chips": "esp32 esp32s2 esp32s3 esp32s31 esp32c2 esp32c3 esp32c5 esp32c6 esp32c61 esp32h2 esp32p4", - "tests": "${{ steps.parse-tests.outputs.tests }}", - "package": "${{ steps.parse-tests.outputs.package }}", - "distinct_id": "${{ env.DISTINCT_ID }}" - } - - - name: Find HIL run URL (full) - id: find-run - uses: actions/github-script@v7 - with: - script: | - const { findHilRun } = require('./.github/scripts/hil-find-run.js'); - - const { runId, body } = await findHilRun({ - github, - context, - pr: context.payload.issue.number, - selector: 'full', - distinctId: '${{ env.DISTINCT_ID }}', - }); - - core.setOutput('run_id', runId); - core.setOutput('body', body); - - - name: Confirm in PR - id: comment - uses: peter-evans/create-or-update-comment@v4 - with: - issue-number: ${{ github.event.issue.number }} - body: ${{ steps.find-run.outputs.body }} - - hil-full-status: - needs: hil-full - if: needs.hil-full.outputs.run_id != '' && needs.hil-full.outputs.comment_id != '' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 # for `require` - with: - fetch-depth: 1 - - - name: Wait for full HIL run and update comment - uses: actions/github-script@v7 - with: - script: | - const { pollRun } = require('./.github/scripts/hil-status.js'); - - await pollRun({ - github, - context, - runId: Number('${{ needs.hil-full.outputs.run_id }}'), - commentId: Number('${{ needs.hil-full.outputs.comment_id }}'), - kind: 'HIL (full)', - }); - - # --------------------------------------------------------------------------- - # PER-CHIP HIL: /hil esp32c3 [esp32s3 ...] [--tests ...] - # --------------------------------------------------------------------------- - - hil-chips: - needs: auth - if: github.event_name == 'issue_comment' && - needs.auth.outputs.allowed == 'true' && - startsWith(github.event.comment.body, '/hil ') && - !startsWith(github.event.comment.body, '/hil quick') && - !startsWith(github.event.comment.body, '/hil full') - runs-on: ubuntu-latest - outputs: - run_id: ${{ steps.find-run.outputs.run_id }} - comment_id: ${{ steps.comment.outputs.comment-id }} - steps: - - uses: actions/checkout@v6 # for `require` - with: - fetch-depth: 1 - - - name: Parse chips - id: parse - uses: actions/github-script@v7 - with: - script: | - const { parseChips } = require('./.github/scripts/hil-parse.js'); - - const res = parseChips(context.payload.comment.body); - - core.setOutput('chips', res.chips); - core.setOutput('chips_label', res.chipsLabel); - core.setOutput('error', res.error); - - - name: Parse tests - id: parse-tests - uses: actions/github-script@v7 - with: - script: | - const { parseTests, parsePackage } = require('./.github/scripts/hil-parse.js'); - const body = context.payload.comment.body; - + core.setOutput('selector', mode === 'chips' ? selected : mode); + core.setOutput('mode', mode); + core.setOutput('chips', selected); + core.setOutput('kind', kind); + core.setOutput('max_polls', String(maxPolls)); + core.setOutput('error', error); core.setOutput('tests', parseTests(body)); core.setOutput('package', parsePackage(body)); - name: Report invalid chips - if: steps.parse.outputs.chips == '' && steps.parse.outputs.error != '' + if: steps.mode.outputs.chips == '' && steps.mode.outputs.error != '' uses: peter-evans/create-or-update-comment@v4 with: issue-number: ${{ github.event.issue.number }} body: | - @${{ github.event.comment.user.login }}, HIL **per-chip** request failed: ${{ steps.parse.outputs.error }} + @${{ github.event.comment.user.login }}, HIL **per-chip** request failed: ${{ steps.mode.outputs.error }} - - name: Dispatch HIL (per-chip) - if: steps.parse.outputs.chips != '' + - name: Dispatch HIL + if: steps.mode.outputs.chips != '' uses: benc-uk/workflow-dispatch@v1 with: workflow: hil.yml @@ -481,14 +311,14 @@ jobs: "repository": "${{ github.repository }}", "branch": "refs/pull/${{ github.event.issue.number }}/head", "pr_number": "${{ github.event.issue.number }}", - "chips": "${{ steps.parse.outputs.chips }}", - "tests": "${{ steps.parse-tests.outputs.tests }}", - "package": "${{ steps.parse-tests.outputs.package }}", + "chips": "${{ steps.mode.outputs.chips }}", + "tests": "${{ steps.mode.outputs.tests }}", + "package": "${{ steps.mode.outputs.package }}", "distinct_id": "${{ env.DISTINCT_ID }}" } - - name: Find HIL run URL (per-chip) - if: steps.parse.outputs.chips != '' + - name: Find HIL run URL + if: steps.mode.outputs.chips != '' id: find-run uses: actions/github-script@v7 with: @@ -499,7 +329,7 @@ jobs: github, context, pr: context.payload.issue.number, - selector: '${{ steps.parse.outputs.chips }}', + selector: '${{ steps.mode.outputs.selector }}', distinctId: '${{ env.DISTINCT_ID }}', }); @@ -507,23 +337,23 @@ jobs: core.setOutput('body', body); - name: Confirm in PR - if: steps.parse.outputs.chips != '' + if: steps.mode.outputs.chips != '' id: comment uses: peter-evans/create-or-update-comment@v4 with: issue-number: ${{ github.event.issue.number }} body: ${{ steps.find-run.outputs.body }} - hil-chips-status: - needs: hil-chips - if: needs.hil-chips.outputs.run_id != '' && needs.hil-chips.outputs.comment_id != '' + hil-status: + needs: hil + if: needs.hil.outputs.run_id != '' && needs.hil.outputs.comment_id != '' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 # for `require` with: fetch-depth: 1 - - name: Wait for per-chip HIL run and update comment + - name: Wait for HIL run and update comment uses: actions/github-script@v7 with: script: | @@ -532,18 +362,17 @@ jobs: await pollRun({ github, context, - runId: Number('${{ needs.hil-chips.outputs.run_id }}'), - commentId: Number('${{ needs.hil-chips.outputs.comment_id }}'), - kind: 'HIL (per-chip)', + runId: Number('${{ needs.hil.outputs.run_id }}'), + commentId: Number('${{ needs.hil.outputs.comment_id }}'), + kind: '${{ needs.hil.outputs.kind }}', + maxPolls: Number('${{ needs.hil.outputs.max_polls }}'), }); hil-deny: needs: auth if: github.event_name == 'issue_comment' && needs.auth.outputs.allowed != 'true' && - (startsWith(github.event.comment.body, '/hil quick') || - startsWith(github.event.comment.body, '/hil full') || - startsWith(github.event.comment.body, '/hil ') || + (startsWith(github.event.comment.body, '/hil ') || startsWith(github.event.comment.body, '/test-size')) runs-on: ubuntu-latest steps: @@ -603,9 +432,10 @@ jobs: - name: Extract parameters from comment id: extract + env: + COMMENT: ${{ github.event.comment.body }} run: | # Expected format: /test-size example_name chip(s) - COMMENT="${{ github.event.comment.body }}" # Use only the line that holds the command (let users add free-form # text in the rest of the comment) and strip any CR characters that # GitHub may include on CRLF line endings. @@ -626,7 +456,7 @@ jobs: fi # Known chips — everything matching these is a chip, first token is the example - KNOWN_CHIPS="esp32 esp32c2 esp32c3 esp32c5 esp32c6 esp32c61 esp32h2 esp32s2 esp32s3 esp32s31 esp32p4" + KNOWN_CHIPS=$(jq -r '[.[].soc] | join(" ")' .github/chips.json) EXAMPLE_NAME="" CHIPS_JSON="[" diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 01a015879e1..cbdd5e0d47d 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -109,10 +109,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: esp-rs/xtensa-toolchain@v1.6 + - uses: ./.github/actions/setup-toolchains with: - default: true - version: 1.97.0.0 + xtensa: "true" + xtensa-default: "true" # xtensa-toolchain installs rustup and a basic toolchain, but doesn't install rust-src - name: rust-src diff --git a/.github/workflows/hil.yml b/.github/workflows/hil.yml index 99b94b9fed8..123203afcfb 100644 --- a/.github/workflows/hil.yml +++ b/.github/workflows/hil.yml @@ -12,7 +12,7 @@ on: workflow_call: inputs: chips: - description: "Space-separated list of SoCs to build and test" + description: 'Space-separated list of SoCs to build and test, or "all"' required: true type: string tests: @@ -25,6 +25,18 @@ on: required: false type: string default: "hil-test" + # Set only by the workflow_dispatch path. Declared here with empty + # defaults so the `inputs.branch == ''` tests that select the plain + # checkout, enable continue-on-error and enable hil-gate are + # well-defined on the workflow_call path too. + repository: + required: false + type: string + default: "" + branch: + required: false + type: string + default: "" # Not used by the workflow_call path, but referenced in run-name; declared # here (with empty defaults) so that expression is well-defined for both # triggers. @@ -51,9 +63,9 @@ on: required: false default: "" chips: - description: "Space-separated list of SoCs to build and test" + description: 'Space-separated list of SoCs to build and test, or "all"' required: true - default: "esp32 esp32s2 esp32s3 esp32s31 esp32c2 esp32c3 esp32c5 esp32c6 esp32c61 esp32h2 esp32p4" + default: "all" tests: description: "Optional list of test names to run (ELF basenames)" required: false @@ -77,98 +89,103 @@ jobs: outputs: matrix: ${{ steps.chips.outputs.matrix }} radio_matrix: ${{ steps.chips.outputs.radio_matrix }} + has_radio: ${{ steps.chips.outputs.has_radio }} steps: + # Unconditional and first, because the chip table below has to come from + # the tree this workflow file came from. + - uses: actions/checkout@v6 + - name: Resolve chip matrix id: chips + env: + REQUESTED_CHIPS: ${{ inputs.chips }} + PKG: ${{ inputs.package }} run: | - ALL_CHIPS='[ - {"soc":"esp32", "rust-target":"xtensa-esp32-none-elf", "runner":"esp32-jtag", "host":"aarch64"}, - {"soc":"esp32s2", "rust-target":"xtensa-esp32s2-none-elf", "runner":"esp32s2-jtag", "host":"aarch64"}, - {"soc":"esp32s3", "rust-target":"xtensa-esp32s3-none-elf", "runner":"esp32s3-usb", "host":"aarch64"}, - {"soc":"esp32c2", "rust-target":"riscv32imc-unknown-none-elf", "runner":"esp32c2-jtag", "host":"aarch64"}, - {"soc":"esp32c3", "rust-target":"riscv32imc-unknown-none-elf", "runner":"esp32c3-usb", "host":"aarch64"}, - {"soc":"esp32c5", "rust-target":"riscv32imac-unknown-none-elf", "runner":"esp32c5-usb", "host":"aarch64"}, - {"soc":"esp32c6", "rust-target":"riscv32imac-unknown-none-elf", "runner":"esp32c6-usb", "host":"aarch64"}, - {"soc":"esp32c61","rust-target":"riscv32imac-unknown-none-elf", "runner":"esp32c61-usb", "host":"aarch64"}, - {"soc":"esp32h2", "rust-target":"riscv32imac-unknown-none-elf", "runner":"esp32h2-usb", "host":"aarch64"}, - {"soc":"esp32p4", "rust-target":"riscv32imafc-unknown-none-elf","runner":"esp32p4", "host":"aarch64"}, - {"soc":"esp32s31","rust-target":"riscv32imafc-unknown-none-elf","runner":"esp32s31-jtag", "host":"aarch64"} - ]' - - # Chips that have a dedicated radio HIL runner. - RADIO_CHIPS='[ - {"soc":"esp32c6", "rust-target":"riscv32imac-unknown-none-elf", "runner":"esp32c6-radio", "host":"aarch64"}, - {"soc":"esp32s3", "rust-target":"xtensa-esp32s3-none-elf", "runner":"esp32s3-radio", "host":"aarch64"} - ]' - - REQUESTED="${{ inputs.chips }}" - MATRIX=$(echo "$ALL_CHIPS" | jq -c --arg chips "$REQUESTED" \ - '[.[] | select(.soc as $s | $chips | split(" ") | index($s))]') + REQUESTED="$REQUESTED_CHIPS" + # `all` saves callers from restating the chip list. Chips without a + # `hil-runner` have no board on the rack, so they stay out of the + # matrix: a null runner label would queue forever. + if [ "$REQUESTED" = "all" ]; then + REQUESTED=$(jq -r '[.[] | select(has("hil-runner")) | .soc] | join(" ")' \ + .github/chips.json) + echo "Expanded 'all' to: $REQUESTED" + fi + + MATRIX=$(jq -c --arg chips "$REQUESTED" ' + [ .[] + | select(has("hil-runner")) + | select(.soc as $s | $chips | split(" ") | index($s)) + | { soc, arch, runner: ."hil-runner" } ]' .github/chips.json) if [ "$(echo "$MATRIX" | jq 'length')" -eq 0 ]; then echo "::error::No valid chips selected from input: '$REQUESTED'" exit 1 fi - RADIO_MATRIX=$(echo "$RADIO_CHIPS" | jq -c --arg chips "$REQUESTED" \ - '[.[] | select(.soc as $s | $chips | split(" ") | index($s))]') + RADIO_MATRIX=$(jq -c --arg chips "$REQUESTED" ' + [ .[] + | select(has("hil-radio-runner")) + | select(.soc as $s | $chips | split(" ") | index($s)) + | { soc, runner: ."hil-radio-runner" } ]' .github/chips.json) - PKG="${{ inputs.package }}" # `package: hil-test` disables the radio HIL run entirely. if [ "$PKG" = "hil-test" ]; then RADIO_MATRIX='[]' fi - echo "matrix={\"target\":$(echo "$MATRIX" | jq -c .)}" >> "$GITHUB_OUTPUT" - echo "radio_matrix={\"target\":$(echo "$RADIO_MATRIX" | jq -c .)}" >> "$GITHUB_OUTPUT" - echo "Selected chips: $(echo "$MATRIX" | jq -r '.[].soc' | tr '\n' ' ')" - echo "Selected radio chips: $(echo "$RADIO_MATRIX" | jq -r '.[].soc' | tr '\n' ' ')" + echo "matrix={\"target\":$MATRIX}" >> "$GITHUB_OUTPUT" + echo "radio_matrix={\"target\":$RADIO_MATRIX}" >> "$GITHUB_OUTPUT" + # A plain flag for the hil-run-radio `if`, so that job condition does + # not have to string-match the serialized matrix. + echo "has_radio=$(echo "$RADIO_MATRIX" | jq -r 'length > 0')" >> "$GITHUB_OUTPUT" - - name: Determine required architectures - id: arch - run: | - NEED_XTENSA=false - NEED_RISCV=false - for chip in ${{ inputs.chips }}; do - case $chip in - esp32|esp32s2|esp32s3) NEED_XTENSA=true ;; - *) NEED_RISCV=true ;; - esac - done - echo "xtensa=$NEED_XTENSA" >> "$GITHUB_OUTPUT" - echo "riscv=$NEED_RISCV" >> "$GITHUB_OUTPUT" + # Which toolchains the build below needs. + echo "xtensa=$(echo "$MATRIX" | jq 'any(.[]; .arch == "xtensa")')" >> "$GITHUB_OUTPUT" + echo "riscv=$(echo "$MATRIX" | jq 'any(.[]; .arch == "riscv")')" >> "$GITHUB_OUTPUT" - - uses: actions/checkout@v6 - if: github.event_name != 'workflow_dispatch' - - uses: actions/checkout@v6 - if: github.event_name == 'workflow_dispatch' - with: - repository: ${{ github.event.inputs.repository }} - ref: ${{ github.event.inputs.branch }} + echo "Selected chips: $(echo "$MATRIX" | jq -r '.[].soc' | tr '\n' ' ')" + echo "Selected radio chips: $(echo "$RADIO_MATRIX" | jq -r '.[].soc' | tr '\n' ' ')" - - uses: esp-rs/xtensa-toolchain@v1.6 - if: steps.arch.outputs.xtensa == 'true' + # Xtensa before RISC-V, because dtolnay/rust-toolchain makes the toolchain + # it installs the rustup default. + - uses: ./.github/actions/setup-toolchains + if: steps.chips.outputs.xtensa == 'true' with: - buildtargets: esp32,esp32s2,esp32s3 - default: true - version: 1.97.0.0 + xtensa: "true" + xtensa-default: "true" - uses: ./.github/actions/setup-nightly - if: steps.arch.outputs.riscv == 'true' + if: steps.chips.outputs.riscv == 'true' with: - targets: riscv32imc-unknown-none-elf,riscv32imac-unknown-none-elf,riscv32imafc-unknown-none-elf + targets: all components: rust-src alias: "true" + # Only the code under test comes from the dispatched ref. The chip table + # and the toolchains above come from the tree this workflow came from. + - uses: actions/checkout@v6 + if: inputs.branch != '' + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.branch }} + # Build hil-test ELFs for each requested chip (unless the dispatch # asked for radio-only via package=hil-test-radio). - name: Build hil-test tests if: ${{ inputs.package != 'hil-test-radio' }} + env: + # The probe below only earns its keep on a dispatch, where the ref that + # got checked out may predate a chip its xtask never learned about. On + # the workflow_call path the tree and the chip list ship together, so + # every chip is by definition supported and probing is pure overhead. + PROBE_XTASK: ${{ inputs.branch != '' }} + MATRIX: ${{ steps.chips.outputs.matrix }} + TESTS: ${{ inputs.tests }} run: | - CHIPS=$(echo '${{ steps.chips.outputs.matrix }}' | jq -r '.target[].soc') - TESTS="${{ inputs.tests }}" + CHIPS=$(echo "$MATRIX" | jq -r '.target[].soc') for chip in $CHIPS; do - if ! cargo xtask build tests "$chip" --help >/dev/null 2>&1; then + if [ "$PROBE_XTASK" = "true" ] && \ + ! cargo xtask build tests "$chip" --help >/dev/null 2>&1; then echo "Skipping $chip: not supported by this ref's xtask" continue fi @@ -187,7 +204,9 @@ jobs: with: name: hil-tests path: target/tests/ - if-no-files-found: warn + # An unfiltered build must produce ELFs, so silence there is a real + # failure. A --tests filter legitimately matches nothing for a chip. + if-no-files-found: ${{ inputs.tests == '' && 'error' || 'warn' }} # Build hil-test-radio ELFs (unless the dispatch asked for hil-test # only). Chips for which the radio package has no applicable binaries @@ -198,10 +217,20 @@ jobs: - name: Build hil-test-radio tests if: ${{ inputs.package != 'hil-test' }} + env: + # Same reasoning as the hil-test build above: only a dispatch can + # land on a ref whose xtask predates a chip in the list. + PROBE_XTASK: ${{ inputs.branch != '' }} + MATRIX: ${{ steps.chips.outputs.matrix }} + TESTS: ${{ inputs.tests }} run: | - CHIPS=$(echo '${{ steps.chips.outputs.matrix }}' | jq -r '.target[].soc') - TESTS="${{ inputs.tests }}" + CHIPS=$(echo "$MATRIX" | jq -r '.target[].soc') for chip in $CHIPS; do + if [ "$PROBE_XTASK" = "true" ] && \ + ! cargo xtask build tests "$chip" hil-test-radio --help >/dev/null 2>&1; then + echo "Skipping $chip: not supported by this ref's xtask" + continue + fi if [ -n "$TESTS" ]; then echo "Building selected hil-test-radio tests for $chip: $TESTS" cargo xtask build tests "$chip" hil-test-radio --tests "$TESTS" @@ -223,12 +252,23 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - if: github.event_name != 'workflow_dispatch' + if: inputs.branch == '' - uses: actions/checkout@v6 - if: github.event_name == 'workflow_dispatch' + if: inputs.branch != '' + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.branch }} + + - name: Cache Dependencies + uses: Swatinem/rust-cache@v2 with: - repository: ${{ github.event.inputs.repository }} - ref: ${{ github.event.inputs.branch }} + # Restore everywhere, but only the dispatch path is worth saving + # from: it runs on the default branch ref, so the entry lands in the + # base-branch scope every later run can read. The workflow_call path + # runs under merge_group, whose ephemeral ref nothing can restore + # from. `cross` mounts $CARGO_HOME, so a warm registry does speed up + # the cross build whenever the esp-devtool binary cache misses. + save-if: ${{ inputs.branch != '' }} # xtask runs on the AArch64 HIL runners as a host tool. # We build the actual tests separately using the correct toolchain. @@ -264,9 +304,13 @@ jobs: path: target/aarch64-unknown-linux-gnu/release/esp-devtool if-no-files-found: error - hil: + hil-run: needs: [build, devtool] if: ${{ inputs.package != 'hil-test-radio' }} + # When called from ci.yml, hil-gate below decides the verdict from the ≥50% + # rule, so a single flaky runner must not fail the run and with it the + # caller's job. + continue-on-error: ${{ inputs.branch == '' }} runs-on: labels: [self-hosted, "${{ matrix.target.runner }}"] strategy: @@ -281,7 +325,7 @@ jobs: - uses: actions/download-artifact@v4 with: - name: xtask-${{ matrix.target.host }} + name: xtask-aarch64 - name: Skip if this target wasn't built id: guard @@ -291,26 +335,26 @@ jobs: echo "No tests for ${{ matrix.target.soc }} in this run; skipping." fi - - name: Compute tests arg - if: steps.guard.outputs.skip != 'true' - id: tests - run: | - if [ -n "${{ inputs.tests }}" ]; then - echo "value=--filter ${{ inputs.tests }}" >> $GITHUB_OUTPUT - else - echo "value=" >> $GITHUB_OUTPUT - fi - - name: Run Tests if: steps.guard.outputs.skip != 'true' id: run-tests + env: + SOC: ${{ matrix.target.soc }} + TESTS: ${{ inputs.tests }} run: | [ -f ~/setup.sh ] && source ~/setup.sh export PATH=$PATH:/home/espressif/.cargo/bin chmod +x esp-devtool - ./esp-devtool run elfs ${{ matrix.target.soc }} tests/${{ matrix.target.soc }} ${{ steps.tests.outputs.value }} + # An array keeps the flag off the command line entirely when no + # filter was asked for, without word-splitting the value itself. + FILTER=() + if [ -n "$TESTS" ]; then + FILTER=(--filter "$TESTS") + fi + + ./esp-devtool run elfs "$SOC" "tests/$SOC" "${FILTER[@]}" - name: Clean up if: always() @@ -321,11 +365,12 @@ jobs: # Radio HIL device runs (hil-test-radio) — wifi/BT capable chips on dedicated # radio runners. The matrix is empty when no requested chip has a radio # runner, which causes this job to be skipped entirely. - hil-radio: + hil-run-radio: needs: [build, devtool] if: > inputs.package != 'hil-test' && - needs.build.outputs.radio_matrix != '{"target":[]}' + needs.build.outputs.has_radio == 'true' + continue-on-error: ${{ inputs.branch == '' }} runs-on: labels: [self-hosted, "${{ matrix.target.runner }}"] strategy: @@ -340,7 +385,7 @@ jobs: - uses: actions/download-artifact@v4 with: - name: xtask-${{ matrix.target.host }} + name: xtask-aarch64 - name: Skip if this target wasn't built id: guard @@ -350,16 +395,6 @@ jobs: echo "No radio tests for ${{ matrix.target.soc }} in this run; skipping." fi - - name: Compute tests arg - if: steps.guard.outputs.skip != 'true' - id: tests - run: | - if [ -n "${{ inputs.tests }}" ]; then - echo "value=--filter ${{ inputs.tests }}" >> $GITHUB_OUTPUT - else - echo "value=" >> $GITHUB_OUTPUT - fi - - name: Reset ESP USB-JTAG devices if: steps.guard.outputs.skip != 'true' timeout-minutes: 1 @@ -398,15 +433,47 @@ jobs: if: steps.guard.outputs.skip != 'true' id: run-tests timeout-minutes: 3 + env: + SOC: ${{ matrix.target.soc }} + TESTS: ${{ inputs.tests }} run: | [ -f ~/setup.sh ] && source ~/setup.sh export PATH=$PATH:/home/espressif/.cargo/bin chmod +x esp-devtool - ./esp-devtool run elfs ${{ matrix.target.soc }} tests/${{ matrix.target.soc }} ${{ steps.tests.outputs.value }} + + FILTER=() + if [ -n "$TESTS" ]; then + FILTER=(--filter "$TESTS") + fi + + ./esp-devtool run elfs "$SOC" "tests/$SOC" "${FILTER[@]}" - name: Clean up if: always() run: | rm -rf tests/ || true rm -f esp-devtool || true + + # Merge gate for hil-run matrix legs. Blocks when ≥50% of legs that actually + # ran tests failed or were cancelled. Skipped legs (no ELFs for that chip) + # are excluded. hil-run-radio is not part of this gate. + hil-gate: + needs: [build, hil-run] + if: > + always() && + inputs.branch == '' && + needs.build.result == 'success' && + needs.hil-run.result != 'skipped' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - name: Evaluate hil-run results + uses: actions/github-script@v7 + with: + script: | + const { evaluateHilGate } = require('./.github/scripts/hil-gate.js'); + await evaluateHilGate({ github, context, core }); diff --git a/.github/workflows/pre-rel-check.yml b/.github/workflows/pre-rel-check.yml index 01227bbfb7e..cbbe6a4134d 100644 --- a/.github/workflows/pre-rel-check.yml +++ b/.github/workflows/pre-rel-check.yml @@ -22,22 +22,15 @@ jobs: steps: - uses: actions/checkout@v6 - # Install the Rust toolchain for Xtensa devices: - - uses: esp-rs/xtensa-toolchain@v1.6 + - uses: ./.github/actions/setup-toolchains with: - version: 1.97.0.0 - - # Install the Rust stable toolchain for RISC-V devices: - - uses: dtolnay/rust-toolchain@v1 - with: - target: riscv32imc-unknown-none-elf,riscv32imac-unknown-none-elf,riscv32imafc-unknown-none-elf - toolchain: stable - components: rust-src + xtensa: "true" + riscv: "true" # Install the Rust nightly toolchain for RISC-V devices: - uses: ./.github/actions/setup-nightly with: - targets: riscv32imc-unknown-none-elf,riscv32imac-unknown-none-elf,riscv32imafc-unknown-none-elf + targets: all components: rust-src alias: "true" diff --git a/hil-test-radio/.cargo/config.toml b/hil-test-radio/.cargo/config.toml index 69a1b14939e..1ae7d0ba218 100644 --- a/hil-test-radio/.cargo/config.toml +++ b/hil-test-radio/.cargo/config.toml @@ -20,6 +20,9 @@ rustflags = [ "-C", "link-arg=-Wl,-Tlinkall.x", ] +[env] +DEFMT_LOG = "info,embedded_test=warn" + [unstable] build-std = ["core", "alloc"] diff --git a/xtask/src/commands/release.rs b/xtask/src/commands/release.rs index 8a56ea67494..8240d8150b2 100644 --- a/xtask/src/commands/release.rs +++ b/xtask/src/commands/release.rs @@ -81,8 +81,8 @@ pub enum Release { Publish(PublishArgs), /// Generate git tags for all new package releases. TagReleases(TagReleasesArgs), - /// Update the MSRV (Badges in README.md, "rust-version" in Cargo.toml, the - /// toolchain used in CI) + /// Update the MSRV (Badges in README.md, "rust-version" in Cargo.toml). CI + /// derives its MSRV toolchain from esp-hal's "rust-version". #[cfg(feature = "release")] BumpMsrv(bump_msrv::BumpMsrvArgs), } diff --git a/xtask/src/commands/release/bump_msrv.rs b/xtask/src/commands/release/bump_msrv.rs index 03bf8367911..9ec97470fdf 100644 --- a/xtask/src/commands/release/bump_msrv.rs +++ b/xtask/src/commands/release/bump_msrv.rs @@ -29,8 +29,9 @@ pub struct BumpMsrvArgs { /// This will process /// - `Cargo.toml` for the packages (adjust (or add if not present) the "rust-version") /// - `README.md` for the packages if it exists (adjusts the MSRV badge) -/// - IF the esp-hal package was touched: .github/workflows/ci.yml (adapts the `MSRV: ""` -/// entry) +/// +/// CI needs no changes: `.github/actions/setup-toolchains` reads the MSRV from +/// esp-hal's "rust-version". /// /// Non-published packages are not touched. /// @@ -65,8 +66,6 @@ pub fn bump_msrv(workspace: &Path, args: BumpMsrvArgs) -> Result<()> { published }; - let adjust_ci = to_process.contains(&Package::EspHal); - // process packages let badge_re = Regex::new( r"(?https://img.shields.io/badge/MSRV-)(?[0123456789.]*)(?-)", @@ -122,21 +121,6 @@ pub fn bump_msrv(workspace: &Path, args: BumpMsrvArgs) -> Result<()> { } } - if adjust_ci { - // process ".github/workflows/ci.yml" - println!("Processing .github/workflows/ci.yml"); - let ci_yml_path = workspace.join(".github/workflows/ci.yml"); - - let ci_yml = std::fs::read_to_string(&ci_yml_path)?; - let ci_yml = Regex::new("(MSRV:.*\\\")([0123456789.]*)(\\\")")? - .replace(&ci_yml, |caps: &Captures| { - format!("{}{new_msrv}{}", &caps[1], &caps[3]) - }); - if !args.dry_run { - std::fs::write(ci_yml_path, ci_yml.as_bytes())?; - } - } - println!("\nPlease review the changes before committing."); Ok(()) }