build-dfu-tools #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: build-dfu-tools | |
| # Builds adafruit-nrfutil as a single-file PyInstaller binary on each | |
| # supported host platform and uploads to a GitHub Release. After the run | |
| # completes, run `scripts/update_tools_manifest.py` to refresh the SHA256s | |
| # in src/bert/firmware/manifest.json. | |
| # | |
| # Why: pc-nrfutil pins click<8.0 which conflicts with typer in any venv | |
| # Bert lives in. Shipping a self-contained binary lets end users get | |
| # `bert flash-firmware` working with zero pip-install pain. | |
| # | |
| # Triggered on: | |
| # - manual dispatch (workflow_dispatch), | |
| # - any tag matching `dfu-tools-*`. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| release_tag: | |
| description: "Release tag (e.g. dfu-tools-2026.05.06)" | |
| required: true | |
| adafruit_nrfutil_version: | |
| description: "adafruit-nrfutil pin" | |
| required: true | |
| default: "0.5.3.post16" | |
| push: | |
| tags: | |
| - "dfu-tools-*" | |
| permissions: | |
| contents: write | |
| defaults: | |
| run: | |
| shell: bash | |
| jobs: | |
| build: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - runner: macos-14 # Apple Silicon | |
| platform_tag: darwin-arm64 | |
| artifact_name: bert-dfu-darwin-arm64 | |
| binary_suffix: "" | |
| - runner: macos-13 # Intel | |
| platform_tag: darwin-x86_64 | |
| artifact_name: bert-dfu-darwin-x86_64 | |
| binary_suffix: "" | |
| - runner: ubuntu-22.04 | |
| platform_tag: linux-x86_64 | |
| artifact_name: bert-dfu-linux-x86_64 | |
| binary_suffix: "" | |
| - runner: windows-2022 | |
| platform_tag: windows-x86_64 | |
| artifact_name: bert-dfu-windows-x86_64.exe | |
| binary_suffix: ".exe" | |
| runs-on: ${{ matrix.runner }} | |
| env: | |
| ADAFRUIT_NRFUTIL_VERSION: ${{ inputs.adafruit_nrfutil_version || '0.5.3.post16' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Install adafruit-nrfutil + PyInstaller | |
| run: | | |
| set -euxo pipefail | |
| python -m pip install --upgrade pip | |
| python -m pip install "adafruit-nrfutil==${ADAFRUIT_NRFUTIL_VERSION}" pyinstaller==6.10.0 | |
| - name: Locate adafruit-nrfutil entry point | |
| id: ep | |
| run: | | |
| set -euxo pipefail | |
| # The pip-installed `adafruit-nrfutil` console script wraps a | |
| # call to nordicsemi.__main__ (or similar). PyInstaller can | |
| # follow the entry point if we give it the actual Python script. | |
| ENTRY=$(python -c " | |
| import shutil, sys | |
| p = shutil.which('adafruit-nrfutil') or shutil.which('adafruit-nrfutil.exe') | |
| assert p, 'adafruit-nrfutil not on PATH after install' | |
| print(p) | |
| ") | |
| echo "entry=$ENTRY" >> $GITHUB_OUTPUT | |
| # Grab the actual Python script path PyInstaller can analyse. | |
| # On Windows the .exe is a wrapper; we point PyInstaller at the | |
| # nordicsemi.__main__ module directly via a tiny shim. | |
| - name: Generate PyInstaller shim | |
| run: | | |
| set -euxo pipefail | |
| cat > pyi_shim.py <<'PY' | |
| # Tiny entry point that PyInstaller can analyse cleanly. It just | |
| # invokes the adafruit-nrfutil click app. | |
| import sys | |
| from nordicsemi.__main__ import cli | |
| if __name__ == "__main__": | |
| sys.exit(cli()) | |
| PY | |
| # Smoke-test it via the host Python first. | |
| python pyi_shim.py --help > /dev/null | |
| - name: Build single-file binary with PyInstaller | |
| run: | | |
| set -euxo pipefail | |
| pyinstaller \ | |
| --onefile \ | |
| --name "${{ matrix.artifact_name }}" \ | |
| --collect-all nordicsemi \ | |
| --collect-all click \ | |
| --collect-all serial \ | |
| --collect-all ecdsa \ | |
| --collect-all libusb1 \ | |
| --collect-all intelhex \ | |
| --collect-all pc_ble_driver_py \ | |
| --hidden-import nordicsemi.dfu.dfu_transport_serial \ | |
| --hidden-import nordicsemi.dfu.signing \ | |
| pyi_shim.py | |
| - name: Smoke-test built binary | |
| run: | | |
| set -euxo pipefail | |
| BIN="dist/${{ matrix.artifact_name }}${{ matrix.binary_suffix }}" | |
| if [ ! -f "$BIN" ]; then | |
| echo "expected $BIN not produced"; ls -la dist; exit 1 | |
| fi | |
| # adafruit-nrfutil exposes 'dfu' as the only sub-group with 'genpkg' | |
| # and 'serial' children. `pkg` is a pc-nrfutil thing. | |
| "$BIN" --help > /dev/null | |
| "$BIN" dfu --help > /dev/null | |
| "$BIN" dfu genpkg --help > /dev/null | |
| "$BIN" dfu serial --help > /dev/null | |
| - name: Hash + size | |
| id: meta | |
| run: | | |
| set -euxo pipefail | |
| BIN="dist/${{ matrix.artifact_name }}${{ matrix.binary_suffix }}" | |
| if [[ "${{ matrix.runner }}" == windows-* ]]; then | |
| SHA=$(sha256sum "$BIN" | awk '{print $1}') | |
| SIZE=$(wc -c < "$BIN" | tr -d ' ') | |
| elif [[ "${{ matrix.runner }}" == macos-* ]]; then | |
| SHA=$(shasum -a 256 "$BIN" | awk '{print $1}') | |
| SIZE=$(stat -f%z "$BIN") | |
| else | |
| SHA=$(sha256sum "$BIN" | awk '{print $1}') | |
| SIZE=$(stat -c%s "$BIN") | |
| fi | |
| echo "sha256=$SHA" >> $GITHUB_OUTPUT | |
| echo "size=$SIZE" >> $GITHUB_OUTPUT | |
| echo "## ${{ matrix.artifact_name }}${{ matrix.binary_suffix }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- platform: \`${{ matrix.platform_tag }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- sha256: \`$SHA\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- size: $SIZE bytes" >> $GITHUB_STEP_SUMMARY | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact_name }} | |
| path: dist/${{ matrix.artifact_name }}${{ matrix.binary_suffix }} | |
| if-no-files-found: error | |
| release: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: Determine release tag | |
| id: tag | |
| run: | | |
| if [ -n "${{ inputs.release_tag }}" ]; then | |
| echo "tag=${{ inputs.release_tag }}" >> $GITHUB_OUTPUT | |
| else | |
| echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Flatten artefact tree | |
| run: | | |
| set -euxo pipefail | |
| mkdir -p out | |
| find artifacts -type f -exec cp {} out/ \; | |
| ls -la out | |
| - name: Compute SHA256 manifest | |
| id: hashes | |
| run: | | |
| set -euxo pipefail | |
| cd out | |
| sha256sum * > SHA256SUMS.txt | |
| cat SHA256SUMS.txt | |
| echo "manifest_block<<EOF" >> $GITHUB_OUTPUT | |
| for f in *; do | |
| [ "$f" = "SHA256SUMS.txt" ] && continue | |
| sha=$(sha256sum "$f" | awk '{print $1}') | |
| size=$(stat -c%s "$f") | |
| echo "| \`$f\` | \`$sha\` | $size |" >> $GITHUB_OUTPUT | |
| done | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.tag.outputs.tag }} | |
| name: ${{ steps.tag.outputs.tag }} | |
| files: | | |
| out/* | |
| fail_on_unmatched_files: true | |
| body: | | |
| adafruit-nrfutil bundled as a single static binary per platform. | |
| Self-contained: no Python runtime dependencies on the user's machine. | |
| | file | sha256 | size | | |
| |---|---|---| | |
| ${{ steps.hashes.outputs.manifest_block }} | |
| Update Bert's manifest with `scripts/update_tools_manifest.py`: | |
| ``` | |
| scripts/update_tools_manifest.py --owner ${{ github.repository_owner }} --repo bert --tag ${{ steps.tag.outputs.tag }} | |
| ``` |