Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 98 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Must match rust-toolchain.toml exactly ("1.90.0" not "1.90"): rustup
# Must match rust-toolchain.toml exactly ("1.92.0" not "1.92"): rustup
# treats the two as different toolchain entries, so components installed
# under "1.90" are invisible when cargo activates "1.90.0" from the file.
# under "1.92" are invisible when cargo activates "1.92.0" from the file.
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.92.0"
Expand Down Expand Up @@ -133,6 +133,102 @@ jobs:
fi
echo "OK: facade is free of rawshift-video"

# ── Compile boundaries (issue #34) ──────────────────────────────────────────
# The `hw-*` backend pins are *verified* feature flags: rawshift-hwdec holds
# a `compile_error!` for each explicit backend on any target its platform
# API does not exist on (docs/SUPPORT.md is the permanent matrix). This job
# proves the boundary in both directions: every invalid feature x target
# combination MUST fail to compile with that backend's `compile_error!`
# text, and the valid combinations (an explicit pin on its native target;
# the portable `hw` anywhere — including no-API targets, where it compiles
# the no-backend stub) MUST succeed. `cargo check --target` only needs the
# target's std, which `rustup target add` ships, and the `compile_error!`
# fires long before linking — so Apple and musl targets check fine from a
# Linux host.
compile-boundaries:
name: Compile boundaries (feature x target)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.92.0"
targets: aarch64-apple-darwin,x86_64-unknown-linux-musl
- uses: Swatinem/rust-cache@v2
- name: Invalid combos must fail with the compile_error text
run: |
check_fails() {
local features="$1" target="$2" expect="$3"
echo "::group::EXPECT FAIL: --features ${features} --target ${target}"
if out=$(cargo check -p rawshift-image --no-default-features \
--features "${features}" --target "${target}" 2>&1); then
echo "${out}"
echo "::error::--features ${features} --target ${target} compiled, but must fail"
exit 1
fi
if ! grep -qF "${expect}" <<<"${out}"; then
echo "${out}"
echo "::error::--features ${features} --target ${target} failed, but not with the expected compile_error (\"${expect}\")"
exit 1
fi
echo "OK: failed with the expected compile_error"
echo "::endgroup::"
}
check_fails hw-vaapi aarch64-apple-darwin "requires a linux-gnu target"
check_fails hw-vaapi x86_64-unknown-linux-musl "requires a linux-gnu target"
check_fails hw-videotoolbox x86_64-unknown-linux-gnu "requires an Apple target"
check_fails hw-mediacodec x86_64-unknown-linux-gnu "requires an Android target"
- name: Valid combos must succeed
run: |
# Explicit pin on its native target.
cargo check -p rawshift-image --no-default-features --features hw-vaapi --target x86_64-unknown-linux-gnu
# The portable `hw` is valid everywhere: it selects the native
# backend where one exists and compiles the no-backend stub (with a
# build-script warning) where none does (musl).
cargo check -p rawshift-image --no-default-features --features hw --target aarch64-apple-darwin
cargo check -p rawshift-image --no-default-features --features hw --target x86_64-unknown-linux-musl

# ── `full` on tier-1 targets (issue #34) ────────────────────────────────────
# docs/SUPPORT.md tier 1: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu,
# aarch64-apple-darwin, aarch64-apple-ios, aarch64-linux-android, and
# x86_64-pc-windows-msvc. The four with native GitHub runners get an
# explicit `--features full` check here (the test matrix only exercises
# default features at the OS level). aarch64-apple-ios and
# aarch64-linux-android are excluded: `full` includes `jxl-encode`
# (cmake-built libjxl via gamut-jxl-sys) and `webp` (libwebp, C), and hosted
# runners have no cross C/C++ toolchain for those targets; their
# boundary-relevant half (the hw pins) is covered by compile-boundaries.
full-check:
name: Check (full)
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
- os: macos-latest
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.92.0"
- uses: Swatinem/rust-cache@v2
# `full` pulls `jxl-encode` (gamut-jxl-sys cmake-builds libjxl; clang for
# its bindings). The macOS and Windows runners ship cmake + LLVM already.
- name: Install system libraries (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends cmake clang libclang-dev
- run: cargo check -p rawshift-image --features full
- run: cargo check -p rawshift --features full

coverage-report:
name: Coverage Report
runs-on: ubuntu-latest
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release-plz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
# Match rust-toolchain.toml exactly ("1.90.0"): rustup resolves the repo's
# Match rust-toolchain.toml exactly ("1.92.0"): rustup resolves the repo's
# cargo to this toolchain regardless of what we install, so install it here.
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.90.0"
toolchain: "1.92.0"
- name: Run release-plz
uses: release-plz/action@v0.5.129
with:
Expand Down Expand Up @@ -56,7 +56,7 @@ jobs:
persist-credentials: false
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.90.0"
toolchain: "1.92.0"
- name: Run release-plz
uses: release-plz/action@v0.5.129
with:
Expand Down
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Contributor and maintainer guide for `rawshift`. For library design rules see

## Building & testing

The toolchain is pinned by [`rust-toolchain.toml`](rust-toolchain.toml) (1.90.0).
The toolchain is pinned by [`rust-toolchain.toml`](rust-toolchain.toml) (1.92.0).

```sh
cargo build --workspace
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,9 @@ The image and video libraries each carry their own feature systems — restating
them here would duplicate state that drifts. See each crate's README:

- [`rawshift-image` feature flags](./crates/rawshift-image/README.md#feature-flags)
— a five-tier per-format system (bundles → formats → directions →
implementations → infrastructure) with selectable codec backends.
— a tiered per-format system (bundles → formats → directions →
infrastructure; gamut is the backend, plus six retained implementation
aliases for the permanent exceptions and blocked migrations).
- [`rawshift-video` feature flags](./crates/rawshift-video/README.md#feature-flags)
— mirrors the image tiers; currently gates no code (video is unimplemented).

Expand Down
60 changes: 39 additions & 21 deletions crates/rawshift-image/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,22 @@ tracing = { workspace = true, features = ["release_max_level_debug"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

[features]
# Feature flags are organised in five tiers, high-level to low-level. See the
# "Feature Flags" section of README.md for the full hierarchy and rationale.
# Feature flags — the consolidated post-migration tree. gamut is the backend:
# every gamut-backed direction feature pulls its `dep:gamut-*` directly, so
# the tier-4 implementation layer has collapsed to the six retained aliases
# documented in the tier-4 section below (permanent exceptions and blocked
# migrations only). See the "Feature Flags" section of README.md and the
# feature table in docs/V1_DESIGN.md.
#
# tier 1 bundles full, experimental, raw-stabilizing, …
# tier 2 format jpeg, png, dng, … (decode + encode)
# tier 3 direction jpeg-decode, dng-encode, … (default impl alias)
# tier 4 implementation ppm-decode-zune, … (compressed only)
# tier 5 infrastructure ifd-parser, serde, zune-runtime, exif, …
# tier 3 direction jpeg-decode, dng-encode, … (pulls the backend)
# tier 4 implementation six retained aliases only (see section comment)
# tier 5 infrastructure ifd-parser, serde, exif, hw/hw-*, …
#
# Only tier-4 features (plus RAW tier-3 features and the gamut-backed
# `jpeg-decode` / `jpeg-encode` / `png-decode` / `png-encode` / `jxl-decode` /
# `jxl-encode` / `avif-decode` / `avif-encode` / `heic-decode`, which have a
# single implementation) reference `dep:*`.
# `jxl-encode` is deliberately absent from `default`: it wraps the reference
# libjxl (cmake + C++ toolchain via gamut-jxl-sys), which a default build must
# not require. It is part of `jxl` and `full`.
default = ["jpeg", "png", "webp", "jxl-decode", "gif-decode", "tiff-decode", "ppm-decode"]

# ── Tier 1: bundle features ───────────────────────────────────────────────────
Expand Down Expand Up @@ -157,10 +160,13 @@ nef = ["nef-decode"]
raf = ["raf-decode"]

# ── Tier 3: direction features ────────────────────────────────────────────────
# For compressed formats a direction feature is an alias for that
# format+direction's DEFAULT implementation (tier 4) — this is where the notion
# of a per-format default lives. RAW direction features reference infra/deps
# directly: RAW formats have a single in-repo implementation and no tier-4 layer.
# gamut-backed direction features pull their `dep:gamut-*` directly — gamut is
# the backend, so there is no implementation choice and no tier-4 layer. Only
# the six retained tier-4 aliases (webp/gif/tiff/svg/ppm — permanent
# exceptions or blocked migrations, see the tier-4 section) still route a
# direction feature through an implementation feature. RAW direction features
# reference infra/deps directly: RAW formats have a single in-repo
# implementation and no tier-4 layer.
# JPEG is backed directly by gamut-jpeg (no tier-4 layer): like PNG
# and the RAW direction features, each half has a single gamut-backed
# implementation, so the format-direction feature itself pulls the dependency.
Expand Down Expand Up @@ -231,14 +237,26 @@ dng-encode = ["dep:gamut-dng"]
nef-decode = ["ifd-parser"]
raf-decode = []

# ── Tier 4: implementation features (compressed formats only) ─────────────────
# Named `format-direction-impl`. The only tier that pulls a `dep:*`. Each impl
# feature also enables its tier-3 direction feature, so it can be selected
# standalone. Multiple implementations of the same format+direction may be
# enabled at once; the active backend is chosen at the API level — see
# `DecodeOptions` and `EncodeOptions`.
# `zune-ppm` builds on the `zune-core` codec primitives, so its impl feature
# also pulls `zune-runtime`.
# ── Tier 4: implementation features (six retained aliases only) ───────────────
# The gamut migrations collapsed this tier for every gamut-backed format — the
# direction features above pull their `dep:gamut-*` directly. The six flags
# below are the deliberate remainder; each names its gate:
#
# gif-decode-gif permanent exception (`gif`) — AGENTS.md upstream policy
# svg-decode-resvg permanent exception (`resvg`) — AGENTS.md upstream policy
# ppm-decode-zune permanent exception (`zune-ppm`) — AGENTS.md upstream
# policy; builds on `zune-core`, so also pulls
# `zune-runtime`
# tiff-decode-tiff blocked migration to gamut-tiff — gamut#299/#300,
# tracked by rawshift#22 (`blocked-upstream`)
# webp-decode-libwebp blocked migration to gamut-webp — gamut#302,
# tracked by rawshift#24 (`blocked-upstream`)
# webp-encode-libwebp blocked migration to gamut-webp — gamut#302,
# tracked by rawshift#24 (`blocked-upstream`)
#
# Each also enables its tier-3 direction feature, so it can be selected
# standalone. When a blocked migration unblocks, its alias dissolves into the
# direction feature exactly like the gamut-backed formats above.
webp-decode-libwebp = ["webp-decode", "dep:libwebp-sys", "exif"]
webp-encode-libwebp = ["webp-encode", "dep:libwebp-sys", "exif"]
gif-decode-gif = ["gif-decode", "dep:gif"]
Expand Down
Loading