oximg 0.11.0

High-performance image compression: library, CLI, and self-hostable server (PoC).
Documentation
name: CI

on:
  push:
    branches: [main]
  pull_request:

env:
  CARGO_TERM_COLOR: always
  # The Dockerfile's pinned post-4.1 SVT-AV1 revision (aarch64 QM/IQ
  # kernels; ABI-verified against the pregenerated bindings). Keep the
  # two in sync when bumping.
  SVT_AV1_REV: d3c4cb3947a8bfed0aa5a2be996b37bb117fa1bd

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        include:
          # NEON kernels are production code (Graviton, Apple silicon):
          # both architectures run the full suite in both feature
          # configurations.
          - os: ubuntu-latest
            arch: amd64
          - os: ubuntu-24.04-arm
            arch: arm64
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v5
      - name: Install build dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends cmake nasm pkg-config libdav1d-dev
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Cache SVT-AV1
        id: svt-cache
        uses: actions/cache@v4
        with:
          path: svt-install
          key: svt-av1-${{ env.SVT_AV1_REV }}-${{ matrix.arch }}
      - name: Build SVT-AV1
        if: steps.svt-cache.outputs.cache-hit != 'true'
        run: |
          git clone --depth 1 https://gitlab.com/AOMediaCodec/SVT-AV1.git svt
          git -C svt fetch --depth 1 origin "$SVT_AV1_REV"
          git -C svt checkout "$SVT_AV1_REV"
          cmake -S svt -B svt/build -DCMAKE_BUILD_TYPE=Release \
            -DBUILD_APPS=OFF -DBUILD_TESTING=OFF \
            -DCMAKE_INSTALL_PREFIX="$PWD/svt-install" -DCMAKE_INSTALL_LIBDIR=lib
          make -C svt/build -j"$(nproc)" install
          rm -rf svt
      - name: Install SVT-AV1
        run: |
          sudo cp -r svt-install/* /usr/local/
          sudo ldconfig
      - name: Format
        if: matrix.arch == 'amd64'
        run: cargo fmt --check
      - name: Clippy (default features)
        run: cargo clippy --release --all-targets -- -D warnings
      - name: Clippy (avif)
        run: cargo clippy --release --all-targets --features avif -- -D warnings
      - name: Test (default features)
        run: cargo test --release
      - name: Test (avif)
        run: cargo test --release --features avif
      - name: Build all targets
        run: cargo build --release --all-targets --features avif
      # `--all-targets` above silently skips examples whose
      # required-features are off, so the bench tools never compiled in
      # CI at all: resize_bench and resolve_bench could rot unnoticed
      # while still being the instruments whose numbers we quote. Lint
      # only — running them is a benchmark, not a smoke test.
      - name: Clippy (bench tools)
        if: matrix.arch == 'amd64'
        run: cargo clippy --release --examples --features avif,bench-internals -- -D warnings
      # The library must build without the HTTP stack (no server feature).
      - name: Check (library only)
        if: matrix.arch == 'amd64'
        run: cargo clippy --release --no-default-features --features avif -- -D warnings
      # Runtime smoke for the examples/tools — they compile under
      # --all-targets above, but nothing ran them, so a broken CLI path
      # (arg handling, read_ppm) would slip through.
      - name: Examples smoke
        if: matrix.arch == 'amd64'
        run: |
          cargo run --release --example probe -- tests/fixtures/photo.webp
          cargo run --release --example thumbnail -- tests/fixtures/photo.jpg 64 64 /tmp/t.jpg
          cargo run --release --example transcode --features avif -- tests/fixtures/photo.jpg 64 64 avif /tmp/t.avif
          cargo run --release --example qcli --features avif -- transcode tests/fixtures/photo.jpg 64 64 webp /tmp/q.webp

  # The declared rust-version must actually build: cargo-install users
  # on the MSRV toolchain hit whatever this job would have caught.
  msrv:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - name: Install build dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends cmake nasm pkg-config
      - uses: dtolnay/rust-toolchain@master
        with:
          toolchain: "1.90"
      - uses: Swatinem/rust-cache@v2
      - name: Check (default features, MSRV)
        run: cargo check --release --all-targets

  # Coverage is informational (no threshold yet): the number is most
  # useful over pipeline/'s knob-gated branches.
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - name: Install build dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends cmake nasm pkg-config libdav1d-dev
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview
      - uses: taiki-e/install-action@v2
        with:
          tool: cargo-llvm-cov
      - uses: Swatinem/rust-cache@v2
      - name: Cache SVT-AV1
        id: svt-cache
        uses: actions/cache@v4
        with:
          path: svt-install
          key: svt-av1-${{ env.SVT_AV1_REV }}-amd64
      - name: Build SVT-AV1
        if: steps.svt-cache.outputs.cache-hit != 'true'
        run: |
          git clone --depth 1 https://gitlab.com/AOMediaCodec/SVT-AV1.git svt
          git -C svt fetch --depth 1 origin "$SVT_AV1_REV"
          git -C svt checkout "$SVT_AV1_REV"
          cmake -S svt -B svt/build -DCMAKE_BUILD_TYPE=Release \
            -DBUILD_APPS=OFF -DBUILD_TESTING=OFF \
            -DCMAKE_INSTALL_PREFIX="$PWD/svt-install" -DCMAKE_INSTALL_LIBDIR=lib
          make -C svt/build -j"$(nproc)" install
          rm -rf svt
      - name: Install SVT-AV1
        run: |
          sudo cp -r svt-install/* /usr/local/
          sudo ldconfig
      # The threshold is a collapse detector, not a target: current
      # line coverage sits ~86%, and 80 leaves room for normal churn
      # while making a silent slide into the 70s a red build.
      - name: Coverage summary
        run: cargo llvm-cov --release --features avif --summary-only --fail-under-lines 80

  # The Ruby gems: `oximg` drives the binary, `oximg-rails` builds URLs
  # for the server. Both suites run against a real oximg build rather
  # than a stub — the CLI grammar and the signing scheme are the
  # contracts between the two languages, and a stub would agree with
  # whatever the gem believes.
  rubygem:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        # The floor we declare and the current release: Ruby 3.4 moved
        # base64 out of the default gems, which is why the signer packs
        # its own.
        ruby: ["3.1", "3.4"]
    steps:
      - uses: actions/checkout@v5
      - name: Install build dependencies
        run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cmake nasm
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      # Debug, not release: this job asks whether the CLI still answers
      # what the gem says, not how fast it does it.
      - name: Build the oximg binary
        run: cargo build
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby }}
      - name: Test oximg
        working-directory: rubygem/oximg
        run: |
          bundle install --jobs 4
          bundle exec rake test
      - name: Test oximg-rails
        working-directory: rubygem/oximg-rails
        run: |
          bundle install --jobs 4
          bundle exec rake test

  # The same two suites on Alpine, against a musl build. Alpine is the
  # base image most Ruby containers use, and it is the one platform
  # where "it works on Linux" is not an answer: a glibc binary does not
  # run there at all. Everything happens inside `docker run` rather than
  # a job-level `container:`, because actions/checkout ships a
  # glibc-linked node and cannot run on Alpine itself.
  rubygem-musl:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - name: Build the musl binary in Alpine
        run: |
          docker run --rm -v "$PWD":/src -w /src rust:alpine sh -c '
            apk add --no-cache build-base cmake nasm >/dev/null &&
            cargo build --release --locked'
          # Root inside the container is root on the mount too, so the
          # build output lands root-owned on the runner. Give it back
          # here rather than discovering it in a later step.
          sudo chown -R "$(id -u):$(id -g)" target
      # Only `ruby` and `ruby-bundler` — no build toolchain. If this
      # needs anything else, the platform gem is not self-contained and
      # the whole premise of shipping one has failed.
      - name: Test both gems on Alpine
        run: |
          docker run --rm -v "$PWD":/src -w /src alpine:latest sh -c '
            set -e
            apk add --no-cache ruby ruby-bundler >/dev/null
            test "$(ruby -e "puts Gem::Platform.local")" = "x86_64-linux-musl"
            export BUNDLE_SILENCE_ROOT_WARNING=1
            for gem in oximg oximg-rails; do
              cd "/src/rubygem/$gem"
              bundle config set --local path vendor/bundle >/dev/null
              bundle install --jobs 4 >/dev/null
              bundle exec rake test
            done'
          # bundler wrote vendor/bundle and lockfiles as root.
          sudo chown -R "$(id -u):$(id -g)" rubygem
      # Proves the shipped artifact, not just the source tree: build the
      # platform gem, install it into an empty GEM_HOME, resize a real
      # image through it.
      - name: Install the musl platform gem and use it
        run: |
          # A file, not `ruby -e`: this string would otherwise cross a
          # YAML block, a shell, `docker run`, and another shell, and
          # each layer wants its own quoting.
          cat > /tmp/smoke.rb <<'RUBY'
          require "oximg"
          puts "executable: #{Oximg.executable}"
          Oximg.resize("/src/tests/fixtures/photo.jpg", "/tmp/o.webp", width: 120)
          raise "unexpected output" unless Oximg.probe("/tmp/o.webp") == {content_type: "image/webp", format: :webp, width: 120, height: 90}
          puts "ok: the platform gem resized on bare Alpine"
          RUBY
          docker run --rm -v "$PWD":/src -v /tmp/smoke.rb:/smoke.rb -w /src alpine:latest sh -c '
            set -e
            apk add --no-cache ruby >/dev/null
            cd /src/rubygem/oximg
            mkdir -p exe && install -m 0755 /src/target/release/oximg exe/oximg
            OXIMG_GEM_PLATFORM=x86_64-linux-musl gem build oximg.gemspec -o /tmp/musl.gem >/dev/null
            rm -f exe/oximg
            GEM_HOME=/tmp/gh gem install /tmp/musl.gem --no-document >/dev/null
            env -u OXIMG_BIN PATH=/usr/bin:/bin GEM_HOME=/tmp/gh ruby /smoke.rb'
          sudo chown -R "$(id -u):$(id -g)" rubygem target