From 864ac235492656184972701e820f082945d003ae Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 1 Oct 2026 17:14:00 +0300 Subject: [PATCH 1/3] build release binaries for linux, macos and windows --- .github/release.yml | 13 ++++ .github/workflows/release.yml | 141 +++++++++++++++++++++++++++++++--- CONTRIBUTING.md | 9 ++- README.md | 8 ++ 4 files changed, 159 insertions(+), 12 deletions(-) create mode 100644 .github/release.yml diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..5fef69f --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,13 @@ +# Sections for the auto-generated release notes, picked by PR label. +changelog: + categories: + - title: Features + labels: [enhancement] + - title: Fixes + labels: [bug] + - title: Documentation + labels: [documentation] + - title: Build and dependencies + labels: [area:build, dependencies] + - title: Other changes + labels: ["*"] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f357e7..93baf3d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,37 +1,156 @@ name: Release -# Pushing a tag like v0.4.0 publishes that version. The tag has to match -# Cargo.toml and the full suite has to pass again before anything is -# uploaded, since a crates.io version can never be replaced. +# Pushing a tag like v0.4.0 cuts that release: the suite runs again, the +# example programs are built for every platform below, the crate goes to +# crates.io, and a GitHub release is created with the archives attached. +# +# A pull request that touches this file runs everything except the publish +# steps, so a broken release pipeline shows up in review, not on release day. on: push: tags: ['v*'] + pull_request: + paths: [.github/workflows/release.yml] permissions: - contents: write + contents: read env: CARGO_TERM_COLOR: always jobs: - release: - name: Publish ${{ github.ref_name }} + verify: + name: Verify runs-on: ubuntu-latest timeout-minutes: 30 + outputs: + version: ${{ steps.version.outputs.version }} steps: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + # A crates.io version can never be replaced, so a tag that disagrees + # with Cargo.toml stops here, before anything is uploaded. - name: Tag matches Cargo.toml + id: version run: | version=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') - if [ "v$version" != "$GITHUB_REF_NAME" ]; then - echo "::error::Tag $GITHUB_REF_NAME does not match Cargo.toml version $version" - exit 1 + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + if [ "v$version" != "$GITHUB_REF_NAME" ]; then + echo "::error::Tag $GITHUB_REF_NAME does not match Cargo.toml version $version" + exit 1 + fi + echo "version=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT" + else + echo "version=v$version-dev" >> "$GITHUB_OUTPUT" fi - run: cargo test --release + build: + name: Build ${{ matrix.name }} + needs: verify + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + permissions: + contents: read + id-token: write + attestations: write + strategy: + fail-fast: false + matrix: + include: + # 22.04 rather than latest: binaries link against the runner's + # glibc, and an older one runs on more distributions. + - { name: Linux x86_64, os: ubuntu-22.04, target: x86_64-unknown-linux-gnu, suffix: x86_64-linux } + - { name: Linux ARM64, os: ubuntu-22.04-arm, target: aarch64-unknown-linux-gnu, suffix: aarch64-linux } + - { name: Linux x86_64 CUDA, os: ubuntu-22.04, target: x86_64-unknown-linux-gnu, suffix: x86_64-linux-cuda, cuda: true } + - { name: macOS Intel, os: macos-15-intel, target: x86_64-apple-darwin, suffix: x86_64-macos } + - { name: macOS Apple Silicon, os: macos-latest, target: aarch64-apple-darwin, suffix: aarch64-macos } + - { name: Windows x86_64, os: windows-latest, target: x86_64-pc-windows-msvc, suffix: x86_64-windows } + env: + ARCHIVE: fastnn-${{ needs.verify.outputs.version }}-${{ matrix.suffix }} + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install CUDA toolkit + if: matrix.cuda + uses: Jimver/cuda-toolkit@v0.2.36 + with: + method: network + + - uses: Swatinem/rust-cache@v2 + with: + key: release-${{ matrix.suffix }} + + - name: Build the examples + run: cargo build --release --examples --target ${{ matrix.target }} ${{ matrix.cuda && '--features cuda' || '' }} + + - name: Package + run: | + examples=$(cargo metadata --no-deps --format-version 1 \ + | jq -r '.packages[0].targets[] | select(.kind[0] == "example") | .name') + ext=""; [ "$RUNNER_OS" = "Windows" ] && ext=".exe" + mkdir "$ARCHIVE" + for ex in $examples; do + cp "target/${{ matrix.target }}/release/examples/$ex$ext" "$ARCHIVE/" + done + cp README.md LICENSE "$ARCHIVE/" + if [ "$RUNNER_OS" = "Windows" ]; then + 7z a -tzip "$ARCHIVE.zip" "$ARCHIVE" > /dev/null + else + tar czf "$ARCHIVE.tar.gz" "$ARCHIVE" + fi + ls -l "$ARCHIVE" + + # The CUDA build needs a driver to run anything, so only the CPU builds + # get a smoke test. + - name: Smoke test + if: ${{ !matrix.cuda }} + run: | + ext=""; [ "$RUNNER_OS" = "Windows" ] && ext=".exe" + "./$ARCHIVE/simple_mlp$ext" + + - name: Attest build provenance + if: startsWith(github.ref, 'refs/tags/') + uses: actions/attest-build-provenance@v4 + with: + subject-path: ${{ env.ARCHIVE }}.* + + - uses: actions/upload-artifact@v7 + with: + name: ${{ env.ARCHIVE }} + path: ${{ env.ARCHIVE }}.* + if-no-files-found: error + + publish: + name: Publish ${{ needs.verify.outputs.version }} + needs: [verify, build] + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + + - uses: actions/download-artifact@v8 + with: + path: dist + merge-multiple: true + + - name: Checksums + working-directory: dist + run: sha256sum fastnn-* > SHA256SUMS && cat SHA256SUMS + - name: Publish to crates.io env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} @@ -45,4 +164,6 @@ jobs: - name: Create GitHub release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh release create "$GITHUB_REF_NAME" --verify-tag --generate-notes --title "$GITHUB_REF_NAME" + run: | + gh release create "$GITHUB_REF_NAME" dist/* \ + --verify-tag --generate-notes --title "$GITHUB_REF_NAME" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0df15fa..61bb923 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -132,8 +132,13 @@ git push origin v0.4.0 ``` The release workflow checks the tag against `Cargo.toml`, runs the suite in -release, publishes to crates.io, and creates the GitHub release with notes from -the merged PRs. +release, builds the example programs for Linux (x86_64, ARM64, and x86_64 with +CUDA), macOS (Intel and Apple Silicon), and Windows, publishes to crates.io, and +creates the GitHub release with the archives, a `SHA256SUMS` file, and notes +from the merged PRs. Release notes are grouped by PR label, so label your PRs. + +Any PR that touches `release.yml` runs the whole thing except the publish +steps, so the pipeline gets tested before a real tag depends on it. ## License diff --git a/README.md b/README.md index d79b71b..8f054ab 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,14 @@ curl -o shakespeare.txt \ cargo run --example char_lm --release -- shakespeare.txt ``` +### Prebuilt examples + +Every [release](https://github.com/CanReader/FastNN/releases) ships the +example programs prebuilt for Linux (x86_64, ARM64), macOS (Intel, Apple +Silicon), and Windows, plus a Linux build with CUDA. Download the archive for +your platform and run `simple_mlp`, `char_lm`, or `mnist_cnn` directly, no Rust +toolchain needed. The CUDA build expects the CUDA runtime to be installed. + ## How it fits together ``` From bebe91c8a40760b67e5408de997e18117664d982 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 1 Oct 2026 17:22:16 +0300 Subject: [PATCH 2/3] strip carriage returns from example names on windows --- .github/workflows/release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93baf3d..cc75bdf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -95,8 +95,10 @@ jobs: - name: Package run: | + # jq on Windows ends lines with \r, which would end up in the names. examples=$(cargo metadata --no-deps --format-version 1 \ - | jq -r '.packages[0].targets[] | select(.kind[0] == "example") | .name') + | jq -r '.packages[0].targets[] | select(.kind[0] == "example") | .name' \ + | tr -d '\r') ext=""; [ "$RUNNER_OS" = "Windows" ] && ext=".exe" mkdir "$ARCHIVE" for ex in $examples; do From a54a77b2e10fc17361d1afb8924410b80e81734d Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 1 Oct 2026 17:32:13 +0300 Subject: [PATCH 3/3] name the cuda runtime the release binaries need --- .github/workflows/release.yml | 2 +- README.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc75bdf..4af1122 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -133,7 +133,7 @@ jobs: if-no-files-found: error publish: - name: Publish ${{ needs.verify.outputs.version }} + name: Publish needs: [verify, build] if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest diff --git a/README.md b/README.md index 8f054ab..ae26266 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,8 @@ Every [release](https://github.com/CanReader/FastNN/releases) ships the example programs prebuilt for Linux (x86_64, ARM64), macOS (Intel, Apple Silicon), and Windows, plus a Linux build with CUDA. Download the archive for your platform and run `simple_mlp`, `char_lm`, or `mnist_cnn` directly, no Rust -toolchain needed. The CUDA build expects the CUDA runtime to be installed. +toolchain needed. The CUDA build links against the CUDA 13 runtime, so it needs +that installed. ## How it fits together