diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4dd8788 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,167 @@ +name: Release + +# Builds a prebuilt binary for every supported platform on a `v*` tag and attaches the archives to +# the GitHub release. This exists so that trying Oxidiris does not require a Rust toolchain — see +# OXD-077. `workflow_dispatch` runs the same build matrix but publishes nothing, which is how you +# smoke-test a change to this file without cutting a tag. + +on: + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: write + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: Build (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + # musl rather than gnu on Linux: every dependency in the tree is pure Rust, so a musl + # build is fully static and runs on any distribution regardless of its glibc version. + # A gnu build would silently bind to the builder's glibc and fail on older systems. + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + cross: false + - target: aarch64-unknown-linux-musl + os: ubuntu-latest + cross: true + # macos-latest is arm64; the Apple toolchain cross-compiles to x86_64 without extra setup. + - target: x86_64-apple-darwin + os: macos-latest + cross: false + - target: aarch64-apple-darwin + os: macos-latest + cross: false + - target: x86_64-pc-windows-msvc + os: windows-latest + cross: false + + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.target }} + + # The musl linker is not present on the GitHub runner image by default. + - name: Install the musl toolchain + if: matrix.target == 'x86_64-unknown-linux-musl' + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y musl-tools + + # aarch64 musl needs a cross linker that apt does not provide in a usable form; `cross` runs + # the build inside an image that already has one. + - name: Install cross + if: matrix.cross + run: cargo install cross --locked + + - name: Build + shell: bash + run: | + tool=cargo + if [ "${{ matrix.cross }}" = "true" ]; then tool=cross; fi + "$tool" build --release --locked -p oxidiris --target ${{ matrix.target }} + + # The archive holds a top-level directory rather than a bare binary so that unpacking it in a + # working directory cannot overwrite the user's own README or LICENCE. GPL-3.0 requires the + # licence to travel with the binary, so it ships inside the archive. + - name: Package (tar.gz) + if: runner.os != 'Windows' + shell: bash + run: | + stage="oxidiris-${{ matrix.target }}" + mkdir "$stage" + cp "target/${{ matrix.target }}/release/oxidiris" "$stage/" + cp LICENSE README.md CHANGELOG.md "$stage/" + tar -czf "$stage.tar.gz" "$stage" + + - name: Package (zip) + if: runner.os == 'Windows' + shell: pwsh + run: | + $stage = "oxidiris-${{ matrix.target }}" + New-Item -ItemType Directory -Path $stage | Out-Null + Copy-Item "target/${{ matrix.target }}/release/oxidiris.exe" $stage + Copy-Item LICENSE, README.md, CHANGELOG.md $stage + Compress-Archive -Path $stage -DestinationPath "$stage.zip" + + - uses: actions/upload-artifact@v4 + with: + name: oxidiris-${{ matrix.target }} + path: | + oxidiris-${{ matrix.target }}.tar.gz + oxidiris-${{ matrix.target }}.zip + if-no-files-found: error + retention-days: 7 + + publish: + name: Publish the release + needs: build + runs-on: ubuntu-latest + # workflow_dispatch exercises the matrix above but must never create a release. + if: startsWith(github.ref, 'refs/tags/') + steps: + - uses: actions/checkout@v4 + + # v0.1.0 shipped a manifest claim that did not match reality and only CI caught it. The same + # class of mistake — tagging v0.3.0 while Cargo.toml still says 0.2.0 — is cheap to rule out + # here, before any asset is published under the wrong name. + - name: Verify the tag matches the manifest version + run: | + version=$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2) + tag="${GITHUB_REF_NAME#v}" + if [ "$version" != "$tag" ]; then + echo "tag v$tag does not match the workspace version $version in Cargo.toml" + exit 1 + fi + + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Checksum every archive + working-directory: dist + run: sha256sum * > SHA256SUMS + + - name: Create or update the release + env: + GH_TOKEN: ${{ github.token }} + run: | + tag="$GITHUB_REF_NAME" + notes=$(cat <<'EOF' + Prebuilt binaries — no Rust toolchain required. See CHANGELOG.md for what changed. + + **Linux (x86_64)** + ```sh + curl -fsSL https://github.com/AlexanderSlokov/oxidiris/releases/latest/download/oxidiris-x86_64-unknown-linux-musl.tar.gz | tar -xz + sudo install -m755 oxidiris-x86_64-unknown-linux-musl/oxidiris /usr/local/bin/oxidiris + rm -rf oxidiris-x86_64-unknown-linux-musl + ``` + + **macOS (Apple Silicon)** + ```sh + curl -fsSL https://github.com/AlexanderSlokov/oxidiris/releases/latest/download/oxidiris-aarch64-apple-darwin.tar.gz | tar -xz + sudo install -m755 oxidiris-aarch64-apple-darwin/oxidiris /usr/local/bin/oxidiris + rm -rf oxidiris-aarch64-apple-darwin + ``` + + Other platforms and the `SHA256SUMS` file are attached below. + EOF + ) + if gh release view "$tag" >/dev/null 2>&1; then + gh release upload "$tag" dist/* --clobber + else + gh release create "$tag" --title "$tag" --notes "$notes" dist/* + fi \ No newline at end of file diff --git a/BACKLOG.md b/BACKLOG.md index e0b036b..e224aec 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -1139,12 +1139,31 @@ Acceptance Phase 7 · Depends OXD-003 · Crate repo · Spec §9.5 · Size M -Scope — `cargo-dist` for cross-platform binary builds · publish to crates.io · Homebrew tap · AUR · Nix flake · `cargo-binstall`. +Scope — cross-platform binary builds · publish to crates.io · Homebrew tap · AUR · Nix flake · `cargo-binstall`. Acceptance -- [ ] Automated release workflow on tag push -- [ ] Binaries built for Linux (x86_64, aarch64), macOS (x86_64, aarch64), Windows (x86_64) +- [x] Automated release workflow on tag push +- [x] Binaries built for Linux (x86_64, aarch64), macOS (x86_64, aarch64), Windows (x86_64) - [ ] `cargo install oxidiris` works from crates.io +- [ ] Homebrew tap serving the release archives +- [ ] AUR, Nix flake, `cargo-binstall` + +Partially done. `.github/workflows/release.yml` covers the first two criteria; the task stays Todo +until distribution channels exist. + +The binary builds are hand-rolled rather than delegated to `cargo-dist`, which the original scope +named. Reasons: the matrix is five targets of a single binary with no C dependencies, so the +generated config would be larger than the workflow it replaces; and the archive names are load +bearing for the README install commands and the future Homebrew formula, so they are worth owning +directly. Revisit if the target list grows or an installer script is wanted. + +Linux targets musl, not gnu. Every dependency in the tree is pure Rust, so the musl build is fully +static (verified: 2.2 MB x86_64, 1.9 MB aarch64, `ldd` reports "statically linked") and runs on any +distribution. A gnu build would bind to the runner's glibc and fail on older systems. + +Docker was considered as the first distribution channel and rejected. Oxidiris is a full-screen TUI +that reads a file from the user's working directory, so a container adds TTY and volume-mount +friction to every invocation, and on macOS it would demand a VM to run a 2 MB static binary. --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 22991e2..0d3f404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,20 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). While the Phase 3 is not finished: `OXD-033` (`%` jump) and `OXD-035` (ramp-up on resume) remain. Both are small; see [`BACKLOG.md`](BACKLOG.md). +### Added + +- **Prebuilt binaries on every tagged release.** A `Release` workflow builds `oxidiris` for Linux + (x86_64, aarch64), macOS (x86_64, aarch64) and Windows (x86_64) on a `v*` tag, and attaches the + archives plus a `SHA256SUMS` file to the GitHub release. Trying the reader no longer requires a + Rust toolchain. Part of `OXD-077`. +- The Linux builds target musl and are statically linked, so one archive runs on any distribution + rather than binding to the builder's glibc. +- The release job refuses to publish when the tag does not match the workspace version in + `Cargo.toml`. v0.1.0 shipped a manifest claim that did not match reality; this makes the + equivalent mistake impossible to tag. +- README now carries a complete copy-paste install block per platform, plus the `cargo install + --git` route for people who already have Rust. + ## [0.2.0] - 2026-09-02 Phase 3 lands the context features (`OXD-030`…`OXD-036`). They are not extras: RSVP works by diff --git a/README.md b/README.md index d191c91..ffd689e 100644 --- a/README.md +++ b/README.md @@ -55,12 +55,85 @@ paper carefully, and it does not advertise a WPM number as an achievement. ## Install +### Prebuilt binary + +No Rust toolchain required. Every release carries a binary for the platforms below; the Linux +builds are statically linked against musl, so they run on any distribution regardless of its glibc +version. Each block is complete — paste it, and you have `oxidiris` on your `PATH`. + +**Linux, x86_64** + +```sh +curl -fsSL https://github.com/AlexanderSlokov/oxidiris/releases/latest/download/oxidiris-x86_64-unknown-linux-musl.tar.gz | tar -xz +sudo install -m755 oxidiris-x86_64-unknown-linux-musl/oxidiris /usr/local/bin/oxidiris +rm -rf oxidiris-x86_64-unknown-linux-musl +``` + +**Linux, aarch64** + +```sh +curl -fsSL https://github.com/AlexanderSlokov/oxidiris/releases/latest/download/oxidiris-aarch64-unknown-linux-musl.tar.gz | tar -xz +sudo install -m755 oxidiris-aarch64-unknown-linux-musl/oxidiris /usr/local/bin/oxidiris +rm -rf oxidiris-aarch64-unknown-linux-musl +``` + +**macOS, Apple Silicon** + +```sh +curl -fsSL https://github.com/AlexanderSlokov/oxidiris/releases/latest/download/oxidiris-aarch64-apple-darwin.tar.gz | tar -xz +sudo install -m755 oxidiris-aarch64-apple-darwin/oxidiris /usr/local/bin/oxidiris +rm -rf oxidiris-aarch64-apple-darwin +``` + +**macOS, Intel** + +```sh +curl -fsSL https://github.com/AlexanderSlokov/oxidiris/releases/latest/download/oxidiris-x86_64-apple-darwin.tar.gz | tar -xz +sudo install -m755 oxidiris-x86_64-apple-darwin/oxidiris /usr/local/bin/oxidiris +rm -rf oxidiris-x86_64-apple-darwin +``` + +The macOS binaries are unsigned. Downloading with `curl` as above is fine, because Gatekeeper only +quarantines what a browser or Finder wrote. If you fetched the archive in a browser instead, clear +the flag once: `xattr -d com.apple.quarantine /usr/local/bin/oxidiris`. + +**Windows, x86_64** (PowerShell) + +```powershell +$dst = "$env:LOCALAPPDATA\Programs\oxidiris" +Invoke-WebRequest https://github.com/AlexanderSlokov/oxidiris/releases/latest/download/oxidiris-x86_64-pc-windows-msvc.zip -OutFile oxidiris.zip +Expand-Archive oxidiris.zip -DestinationPath $dst -Force +Move-Item "$dst\oxidiris-x86_64-pc-windows-msvc\oxidiris.exe" $dst -Force +Remove-Item oxidiris.zip, "$dst\oxidiris-x86_64-pc-windows-msvc" -Recurse +[Environment]::SetEnvironmentVariable("Path", "$([Environment]::GetEnvironmentVariable('Path','User'));$dst", "User") +``` + +Open a new terminal afterward so the `PATH` change takes effect. + +**Check it worked**, on any platform: + ```sh -cargo install --path crates/oxidiris +oxidiris --version ``` +A `SHA256SUMS` file is attached to every release if you want to verify the download. + +### From source + Requires Rust 1.88 or newer. +```sh +cargo install --git https://github.com/AlexanderSlokov/oxidiris oxidiris --locked +``` + +From a clone of this repository: + +```sh +cargo install --path crates/oxidiris --locked +``` + +A Homebrew tap and a crates.io publish are tracked as `OXD-077` in [`BACKLOG.md`](BACKLOG.md). + ## Use ```sh @@ -144,7 +217,7 @@ The workspace is two crates: - **`oxidiris`** — the terminal application. Design notes live in [`docs/informations/proposals.md`](docs/informations/proposals.md); decisions -that departed from it are recorded in [`docs/decisions/`](docs/decisions/) and back-annotated into +that departed from it are recorded in [`docs/decisions/`](docs/decisions) and back-annotated into the spec. ## Licence