Establish release CI matrix (#99)
Some checks failed
Native code generation / deterministic (push) Failing after 2m4s
Imaging and meshing gate / native (push) Successful in 6m35s
JPEG 2000 feature / linux (push) Successful in 2m45s
Release platform and feature matrix / audit (push) Successful in 34s
Native Rust workspace compile / compile (push) Failing after 56s
Skia feature / linux (push) Successful in 30m55s
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Some checks failed
Native code generation / deterministic (push) Failing after 2m4s
Imaging and meshing gate / native (push) Successful in 6m35s
JPEG 2000 feature / linux (push) Successful in 2m45s
Release platform and feature matrix / audit (push) Successful in 34s
Native Rust workspace compile / compile (push) Failing after 56s
Skia feature / linux (push) Successful in 30m55s
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
This commit is contained in:
122
.gitea/workflows/release-matrix.yml
Normal file
122
.gitea/workflows/release-matrix.yml
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
name: Release platform and feature matrix
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- ".gitea/workflows/release-matrix.yml"
|
||||||
|
- "ci/release-matrix.json"
|
||||||
|
- "tools/ci-matrix/**"
|
||||||
|
- "tools/install_openjpeg_2_5_4.sh"
|
||||||
|
- "docs/release-ci-matrix.md"
|
||||||
|
- "README.md"
|
||||||
|
- "RUSTREWRITE.md"
|
||||||
|
- "Cargo.toml"
|
||||||
|
- "Cargo.lock"
|
||||||
|
- "crates/**"
|
||||||
|
- "programs/**"
|
||||||
|
- "tests/**"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- ".gitea/workflows/release-matrix.yml"
|
||||||
|
- "ci/release-matrix.json"
|
||||||
|
- "tools/ci-matrix/**"
|
||||||
|
- "tools/install_openjpeg_2_5_4.sh"
|
||||||
|
- "docs/release-ci-matrix.md"
|
||||||
|
- "README.md"
|
||||||
|
- "RUSTREWRITE.md"
|
||||||
|
- "Cargo.toml"
|
||||||
|
- "Cargo.lock"
|
||||||
|
- "crates/**"
|
||||||
|
- "programs/**"
|
||||||
|
- "tests/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_BUILD_JOBS: 1
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
CARGO_PROFILE_DEV_DEBUG: 0
|
||||||
|
CARGO_PROFILE_TEST_DEBUG: 0
|
||||||
|
FORCE_SKIA_BINARIES_DOWNLOAD: 1
|
||||||
|
RUSTDOCFLAGS: -D warnings
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Validate matrix coverage and ubuntu-only policy
|
||||||
|
run: cargo run --locked -p metacrate-ci-matrix -- audit
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
needs: audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- profile: linux-msrv-portable
|
||||||
|
toolchain: 1.96.0
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
native_dependencies: false
|
||||||
|
- profile: linux-stable-default
|
||||||
|
toolchain: stable
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
native_dependencies: true
|
||||||
|
- profile: linux-stable-minimal
|
||||||
|
toolchain: stable
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
native_dependencies: false
|
||||||
|
- profile: linux-stable-features
|
||||||
|
toolchain: stable
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
native_dependencies: true
|
||||||
|
- profile: linux-stable-release-surface
|
||||||
|
toolchain: stable
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
native_dependencies: true
|
||||||
|
- profile: windows-stable-portable
|
||||||
|
toolchain: stable
|
||||||
|
target: x86_64-pc-windows-gnu
|
||||||
|
native_dependencies: false
|
||||||
|
- profile: macos-stable-portable
|
||||||
|
toolchain: stable
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
native_dependencies: false
|
||||||
|
env:
|
||||||
|
OPENJPEG_PREFIX: /tmp/metacrate-openjpeg-${{ github.run_id }}-${{ matrix.profile }}
|
||||||
|
PKG_CONFIG_PATH: /tmp/metacrate-openjpeg-${{ github.run_id }}-${{ matrix.profile }}/lib/pkgconfig
|
||||||
|
LD_LIBRARY_PATH: /tmp/metacrate-openjpeg-${{ github.run_id }}-${{ matrix.profile }}/lib
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Cache immutable Cargo downloads only
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: release-downloads-${{ runner.os }}-${{ matrix.toolchain }}-${{ hashFiles('Cargo.lock') }}
|
||||||
|
- uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
- name: Install pinned native feature prerequisites
|
||||||
|
if: matrix.native_dependencies == true
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install --yes build-essential cmake pkg-config libopus-dev libasound2-dev
|
||||||
|
tools/install_openjpeg_2_5_4.sh "$OPENJPEG_PREFIX"
|
||||||
|
pkg-config --modversion libopenjp2
|
||||||
|
pkg-config --modversion opus
|
||||||
|
pkg-config --modversion alsa
|
||||||
|
- name: Revalidate matrix with selected toolchain
|
||||||
|
run: cargo run --locked -p metacrate-ci-matrix -- audit
|
||||||
|
- name: Execute isolated release profile
|
||||||
|
run: cargo run --locked -p metacrate-ci-matrix -- run ${{ matrix.profile }} --evidence artifacts/ci-matrix/${{ matrix.profile }}.json
|
||||||
|
- name: Upload exact matrix evidence
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-matrix-${{ matrix.profile }}
|
||||||
|
path: artifacts/ci-matrix/${{ matrix.profile }}.json
|
||||||
|
if-no-files-found: error
|
||||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -1780,6 +1780,14 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "metacrate-ci-matrix"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ members = [
|
|||||||
"programs",
|
"programs",
|
||||||
"tests/compat",
|
"tests/compat",
|
||||||
"tools/codegen",
|
"tools/codegen",
|
||||||
|
"tools/ci-matrix",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -56,20 +56,26 @@ The currently available opt-in codec backends are:
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Raw J2K and JP2 decoding/encoding through OpenJPEG | `libremetaverse/jpeg2000` | `cargo build -p libremetaverse --features jpeg2000` | OpenJPEG 2.5.4 or newer |
|
| Raw J2K and JP2 decoding/encoding through OpenJPEG | `libremetaverse/jpeg2000` | `cargo build -p libremetaverse --features jpeg2000` | OpenJPEG 2.5.4 or newer |
|
||||||
| BMP, GIF, ICO, JPEG, PNG, WBMP, and WebP decoding through Skia | `libremetaverse-imaging-skia/skia` | `cargo build -p libremetaverse-imaging-skia --features skia` | A matching rust-skia binary cache, or the documented native source-build tools |
|
| BMP, GIF, ICO, JPEG, PNG, WBMP, and WebP decoding through Skia | `libremetaverse-imaging-skia/skia` | `cargo build -p libremetaverse-imaging-skia --features skia` | A matching rust-skia binary cache, or the documented native source-build tools |
|
||||||
|
| PCM to Ogg Vorbis encoding | `libremetaverse/vorbis` | `cargo build -p libremetaverse --features vorbis` | `vorbis_rs` 0.5.6 and its native encoder build prerequisites |
|
||||||
|
|
||||||
Run their focused test suites with:
|
Run their focused test suites with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo test -p libremetaverse-imaging --features jpeg2000
|
cargo test -p libremetaverse-imaging --features jpeg2000
|
||||||
cargo test -p libremetaverse-imaging-skia --features skia
|
cargo test -p libremetaverse-imaging-skia --features skia
|
||||||
|
cargo test -p libremetaverse --no-default-features --features vorbis
|
||||||
```
|
```
|
||||||
|
|
||||||
`cargo build --workspace --all-features` enables both optional backends and
|
`cargo build --workspace --all-features` enables all optional backends and
|
||||||
therefore requires both sets of native prerequisites. Platform installation,
|
therefore requires both sets of native prerequisites. Platform installation,
|
||||||
offline-build, cache, and redistribution details are in the
|
offline-build, cache, and redistribution details are in the
|
||||||
[`OpenJPEG adapter guide`](crates/libremetaverse-openjpeg/README.md) and
|
[`OpenJPEG adapter guide`](crates/libremetaverse-openjpeg/README.md) and
|
||||||
[`Skia adapter guide`](crates/libremetaverse-imaging-skia/README.md).
|
[`Skia adapter guide`](crates/libremetaverse-imaging-skia/README.md).
|
||||||
|
|
||||||
|
The validated Linux/MSRV/Windows/macOS target and feature inventory, clean
|
||||||
|
profile runner, native prerequisite evidence, and manual platform boundaries
|
||||||
|
are documented in the [release CI matrix](docs/release-ci-matrix.md).
|
||||||
|
|
||||||
An independent optional `rust-j2k` backend is planned but has not been
|
An independent optional `rust-j2k` backend is planned but has not been
|
||||||
implemented yet. There is deliberately no enablement command for it today;
|
implemented yet. There is deliberately no enablement command for it today;
|
||||||
the existing `jpeg2000` feature always selects the OpenJPEG backend.
|
the existing `jpeg2000` feature always selects the OpenJPEG backend.
|
||||||
|
|||||||
187
ci/release-matrix.json
Normal file
187
ci/release-matrix.json
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
{
|
||||||
|
"schema": 1,
|
||||||
|
"msrv": "1.96.0",
|
||||||
|
"current": "stable",
|
||||||
|
"native_prerequisites": [
|
||||||
|
{
|
||||||
|
"id": "openjpeg-2.5.4",
|
||||||
|
"version": "2.5.4",
|
||||||
|
"purpose": "Opt-in raw J2K and JP2 codec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "skia-safe-0.99.0",
|
||||||
|
"version": "0.99.0",
|
||||||
|
"purpose": "Opt-in Skia image formats using the target-specific binary cache or source build"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audiopus-0.2",
|
||||||
|
"version": "0.2",
|
||||||
|
"purpose": "Native Opus codec used by WebRTC voice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "alsa-1.2",
|
||||||
|
"version": "1.2.x",
|
||||||
|
"purpose": "Linux CPAL real-audio adapter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "vorbis-rs-0.5.6",
|
||||||
|
"version": "0.5.6",
|
||||||
|
"purpose": "Opt-in PCM to Ogg Vorbis encoder"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"manual_gates": [
|
||||||
|
"Live grid credentials and reversible dedicated-account validation remain in the explicit live-grid smoke gate.",
|
||||||
|
"Physical audio input and output devices remain an explicit manual device gate; CI uses virtual peers.",
|
||||||
|
"Proprietary services, credentials, and redistributable platform packages are never inferred or downloaded by the matrix."
|
||||||
|
],
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"id": "linux-msrv-portable",
|
||||||
|
"platform": "linux",
|
||||||
|
"host": "ubuntu-latest",
|
||||||
|
"execution": "native",
|
||||||
|
"toolchain": "1.96.0",
|
||||||
|
"target": "x86_64-unknown-linux-gnu",
|
||||||
|
"feature_sets": ["no-default-features"],
|
||||||
|
"prerequisites": [],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"label": "Compile the portable public crates on the declared MSRV",
|
||||||
|
"args": ["check", "--locked", "-j", "1", "--all-targets", "--no-default-features", "-p", "libremetaverse-types", "-p", "libremetaverse-structured-data", "-p", "libremetaverse-imaging", "-p", "libremetaverse-imaging-skia", "-p", "libremetaverse-prim-mesher", "-p", "libremetaverse-lsl-tools", "-p", "libremetaverse-rlv", "-p", "metacrate-ci-matrix"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "linux-stable-default",
|
||||||
|
"platform": "linux",
|
||||||
|
"host": "ubuntu-latest",
|
||||||
|
"execution": "native",
|
||||||
|
"toolchain": "stable",
|
||||||
|
"target": "x86_64-unknown-linux-gnu",
|
||||||
|
"feature_sets": ["default", "tests", "examples", "docs"],
|
||||||
|
"prerequisites": ["openjpeg-2.5.4", "skia-safe-0.99.0", "audiopus-0.2"],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"label": "Compile every default workspace target",
|
||||||
|
"args": ["check", "--workspace", "--all-targets", "--locked", "-j", "1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Compile every default test without running live gates",
|
||||||
|
"args": ["test", "--workspace", "--no-run", "--locked", "-j", "1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Build warning-free default API documentation",
|
||||||
|
"args": ["doc", "--workspace", "--no-deps", "--locked", "-j", "1"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "linux-stable-minimal",
|
||||||
|
"platform": "linux",
|
||||||
|
"host": "ubuntu-latest",
|
||||||
|
"execution": "native",
|
||||||
|
"toolchain": "stable",
|
||||||
|
"target": "x86_64-unknown-linux-gnu",
|
||||||
|
"feature_sets": ["no-default-features", "tests"],
|
||||||
|
"prerequisites": [],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"label": "Compile core and its tests without optional codecs",
|
||||||
|
"args": ["check", "-p", "libremetaverse", "--all-targets", "--no-default-features", "--locked", "-j", "1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Run core tests without optional codecs",
|
||||||
|
"args": ["test", "-p", "libremetaverse", "--no-default-features", "--locked", "-j", "1"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "linux-stable-features",
|
||||||
|
"platform": "linux",
|
||||||
|
"host": "ubuntu-latest",
|
||||||
|
"execution": "native",
|
||||||
|
"toolchain": "stable",
|
||||||
|
"target": "x86_64-unknown-linux-gnu",
|
||||||
|
"feature_sets": ["all-features", "dds-bc67", "jpeg2000", "skia", "vorbis", "real-audio", "tests"],
|
||||||
|
"prerequisites": ["openjpeg-2.5.4", "skia-safe-0.99.0", "audiopus-0.2", "alsa-1.2", "vorbis-rs-0.5.6"],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"label": "Compile core with only pure Rust BC6H and BC7",
|
||||||
|
"args": ["check", "-p", "libremetaverse", "--no-default-features", "--features", "dds-bc67", "--all-targets", "--locked", "-j", "1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Test the OpenJPEG feature independently",
|
||||||
|
"args": ["test", "-p", "libremetaverse-imaging", "--no-default-features", "--features", "jpeg2000", "--locked", "-j", "1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Test the Skia feature independently",
|
||||||
|
"args": ["test", "-p", "libremetaverse-imaging-skia", "--no-default-features", "--features", "skia", "--locked", "-j", "1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Test the Vorbis feature independently",
|
||||||
|
"args": ["test", "-p", "libremetaverse", "--no-default-features", "--features", "vorbis", "--lib", "--locked", "-j", "1", "--", "asset_pipeline_semantics"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Compile the CPAL real-audio adapter",
|
||||||
|
"args": ["check", "-p", "libremetaverse-voice-webrtc", "--all-targets", "--features", "real-audio", "--locked", "-j", "1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Compile complete feature unification across the workspace",
|
||||||
|
"args": ["check", "--workspace", "--all-targets", "--all-features", "--locked", "-j", "1"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "linux-stable-release-surface",
|
||||||
|
"platform": "linux",
|
||||||
|
"host": "ubuntu-latest",
|
||||||
|
"execution": "native",
|
||||||
|
"toolchain": "stable",
|
||||||
|
"target": "x86_64-unknown-linux-gnu",
|
||||||
|
"feature_sets": ["default", "tests", "examples", "docs"],
|
||||||
|
"prerequisites": ["openjpeg-2.5.4", "skia-safe-0.99.0", "audiopus-0.2"],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"label": "Run all default documentation tests",
|
||||||
|
"args": ["test", "--workspace", "--doc", "--locked", "-j", "1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Build every shipped example program",
|
||||||
|
"args": ["build", "-p", "libremetaverse-programs", "--bins", "--locked", "-j", "1"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "windows-stable-portable",
|
||||||
|
"platform": "windows",
|
||||||
|
"host": "ubuntu-latest",
|
||||||
|
"execution": "cross",
|
||||||
|
"toolchain": "stable",
|
||||||
|
"target": "x86_64-pc-windows-gnu",
|
||||||
|
"feature_sets": ["no-default-features", "dds-bc67"],
|
||||||
|
"prerequisites": [],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"label": "Cross-check portable crates for Windows GNU",
|
||||||
|
"args": ["check", "--locked", "-j", "1", "--target", "x86_64-pc-windows-gnu", "--all-targets", "--no-default-features", "-p", "libremetaverse-types", "-p", "libremetaverse-structured-data", "-p", "libremetaverse-imaging", "-p", "libremetaverse-imaging-skia", "-p", "libremetaverse-prim-mesher", "-p", "libremetaverse-lsl-tools", "-p", "libremetaverse-rlv", "-p", "metacrate-ci-matrix"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "macos-stable-portable",
|
||||||
|
"platform": "macos",
|
||||||
|
"host": "ubuntu-latest",
|
||||||
|
"execution": "cross",
|
||||||
|
"toolchain": "stable",
|
||||||
|
"target": "x86_64-apple-darwin",
|
||||||
|
"feature_sets": ["no-default-features", "dds-bc67"],
|
||||||
|
"prerequisites": [],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"label": "Cross-check portable crates for macOS",
|
||||||
|
"args": ["check", "--locked", "-j", "1", "--target", "x86_64-apple-darwin", "--all-targets", "--no-default-features", "-p", "libremetaverse-types", "-p", "libremetaverse-structured-data", "-p", "libremetaverse-imaging", "-p", "libremetaverse-imaging-skia", "-p", "libremetaverse-prim-mesher", "-p", "libremetaverse-lsl-tools", "-p", "libremetaverse-rlv", "-p", "metacrate-ci-matrix"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -8,9 +8,10 @@ repository.workspace = true
|
|||||||
description = "Rust rewrite shell for the LibreMetaverse client library"
|
description = "Rust rewrite shell for the LibreMetaverse client library"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["dds-bc67", "jpeg2000"]
|
default = ["dds-bc67"]
|
||||||
dds-bc67 = ["dep:bcdec_rs"]
|
dds-bc67 = ["dep:bcdec_rs"]
|
||||||
jpeg2000 = ["libremetaverse-imaging/jpeg2000"]
|
jpeg2000 = ["libremetaverse-imaging/jpeg2000"]
|
||||||
|
vorbis = ["dep:vorbis_rs"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
@@ -28,7 +29,7 @@ roxmltree = "0.21.1"
|
|||||||
serde_json = "1.0.143"
|
serde_json = "1.0.143"
|
||||||
tar = "0.4.46"
|
tar = "0.4.46"
|
||||||
tokio = { version = "1.47.1", features = ["macros", "net", "rt", "sync", "time"] }
|
tokio = { version = "1.47.1", features = ["macros", "net", "rt", "sync", "time"] }
|
||||||
vorbis_rs = "0.5.5"
|
vorbis_rs = { version = "0.5.6", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.47.1", features = ["macros", "net", "rt-multi-thread", "sync", "test-util", "time"] }
|
tokio = { version = "1.47.1", features = ["macros", "net", "rt-multi-thread", "sync", "test-util", "time"] }
|
||||||
|
|||||||
@@ -155,20 +155,14 @@ raw_asset!(AssetScriptBinary, AssetType::LSLBytecode);
|
|||||||
raw_asset!(AssetSound, AssetType::Sound);
|
raw_asset!(AssetSound, AssetType::Sound);
|
||||||
|
|
||||||
impl AssetSound {
|
impl AssetSound {
|
||||||
|
#[cfg(feature = "vorbis")]
|
||||||
pub fn pcm_to_ogg(
|
pub fn pcm_to_ogg(
|
||||||
pcm_data: Vec<u8>,
|
pcm_data: Vec<u8>,
|
||||||
sample_rate: i32,
|
sample_rate: i32,
|
||||||
channels: i32,
|
channels: i32,
|
||||||
bits_per_sample: Option<i32>,
|
bits_per_sample: Option<i32>,
|
||||||
) -> Result<Vec<u8>, Error> {
|
) -> Result<Vec<u8>, Error> {
|
||||||
validate_bytes(&pcm_data)?;
|
validate_pcm(&pcm_data, sample_rate, channels, bits_per_sample)?;
|
||||||
if pcm_data.is_empty()
|
|
||||||
|| sample_rate <= 0
|
|
||||||
|| !matches!(channels, 1 | 2)
|
|
||||||
|| !matches!(bits_per_sample.unwrap_or(16), 8 | 16)
|
|
||||||
{
|
|
||||||
return Err(Error::Argument);
|
|
||||||
}
|
|
||||||
let bits = bits_per_sample.unwrap_or(16);
|
let bits = bits_per_sample.unwrap_or(16);
|
||||||
let bytes_per_sample = usize::try_from(bits / 8).map_err(|_| Error::Argument)?;
|
let bytes_per_sample = usize::try_from(bits / 8).map_err(|_| Error::Argument)?;
|
||||||
let channels = usize::try_from(channels).map_err(|_| Error::Argument)?;
|
let channels = usize::try_from(channels).map_err(|_| Error::Argument)?;
|
||||||
@@ -215,6 +209,43 @@ impl AssetSound {
|
|||||||
validate_bytes(&ogg_data)?;
|
validate_bytes(&ogg_data)?;
|
||||||
Ok(ogg_data)
|
Ok(ogg_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "vorbis"))]
|
||||||
|
pub fn pcm_to_ogg(
|
||||||
|
pcm_data: Vec<u8>,
|
||||||
|
sample_rate: i32,
|
||||||
|
channels: i32,
|
||||||
|
bits_per_sample: Option<i32>,
|
||||||
|
) -> Result<Vec<u8>, Error> {
|
||||||
|
validate_pcm(&pcm_data, sample_rate, channels, bits_per_sample)?;
|
||||||
|
Err(Error::InvalidOperation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_pcm(
|
||||||
|
pcm_data: &[u8],
|
||||||
|
sample_rate: i32,
|
||||||
|
channels: i32,
|
||||||
|
bits_per_sample: Option<i32>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
validate_bytes(pcm_data)?;
|
||||||
|
if pcm_data.is_empty()
|
||||||
|
|| sample_rate <= 0
|
||||||
|
|| !matches!(channels, 1 | 2)
|
||||||
|
|| !matches!(bits_per_sample.unwrap_or(16), 8 | 16)
|
||||||
|
{
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
let bytes_per_sample =
|
||||||
|
usize::try_from(bits_per_sample.unwrap_or(16) / 8).map_err(|_| Error::Argument)?;
|
||||||
|
let channels = usize::try_from(channels).map_err(|_| Error::Argument)?;
|
||||||
|
let frame_size = bytes_per_sample
|
||||||
|
.checked_mul(channels)
|
||||||
|
.ok_or(Error::Argument)?;
|
||||||
|
if !pcm_data.len().is_multiple_of(frame_size) {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use crate::assets::{AssetMesh, AssetMutable, AssetSound, AssetTexture};
|
#[cfg(feature = "jpeg2000")]
|
||||||
|
use crate::assets::AssetTexture;
|
||||||
|
use crate::assets::{AssetMesh, AssetMutable, AssetSound};
|
||||||
use crate::{
|
use crate::{
|
||||||
AssetCache, AssetCacheComputeAssetCacheFilenameDelegate, Error, GridClient, ImageCodec,
|
AssetCache, AssetCacheComputeAssetCacheFilenameDelegate, Error, GridClient, ImageCodec,
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "jpeg2000")]
|
||||||
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
|
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
|
||||||
use libremetaverse_types::{AssetType, UUID};
|
use libremetaverse_types::{AssetType, UUID};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -33,10 +36,28 @@ fn asset_models_retain_type_and_reject_malformed_meshes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sound_and_texture_codecs_produce_real_valid_payloads() {
|
#[cfg(feature = "vorbis")]
|
||||||
|
fn sound_codec_produces_real_valid_payload() {
|
||||||
let ogg = AssetSound::pcm_to_ogg(vec![0_u8; 256 * 2], 44_100, 1, Some(16)).unwrap();
|
let ogg = AssetSound::pcm_to_ogg(vec![0_u8; 256 * 2], 44_100, 1, Some(16)).unwrap();
|
||||||
assert!(ogg.starts_with(b"OggS"));
|
assert!(ogg.starts_with(b"OggS"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(feature = "vorbis"))]
|
||||||
|
fn sound_codec_is_explicitly_feature_gated() {
|
||||||
|
assert_eq!(
|
||||||
|
AssetSound::pcm_to_ogg(vec![0_u8; 256 * 2], 44_100, 1, Some(16)),
|
||||||
|
Err(Error::InvalidOperation)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
AssetSound::pcm_to_ogg(Vec::new(), 44_100, 1, Some(16)),
|
||||||
|
Err(Error::Argument)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "jpeg2000")]
|
||||||
|
fn texture_codec_produces_real_valid_payload() {
|
||||||
let mut image = ManagedImage::new(2, 2, ManagedImageImageChannels::COLOR).unwrap();
|
let mut image = ManagedImage::new(2, 2, ManagedImageImageChannels::COLOR).unwrap();
|
||||||
image.red.fill(255);
|
image.red.fill(255);
|
||||||
let texture = AssetTexture::new_with_managed_image(image).unwrap();
|
let texture = AssetTexture::new_with_managed_image(image).unwrap();
|
||||||
|
|||||||
@@ -2913,12 +2913,10 @@ mod tests {
|
|||||||
block.name = b"Native object".to_vec();
|
block.name = b"Native object".to_vec();
|
||||||
block.description = b"decoded".to_vec();
|
block.description = b"decoded".to_vec();
|
||||||
packet.object_data = vec![block];
|
packet.object_data = vec![block];
|
||||||
|
let wire = transport_wire(&packet.to_bytes_with_method().expect("wire packet"));
|
||||||
manager
|
manager
|
||||||
.inner
|
.inner
|
||||||
.handle_object_properties(
|
.handle_object_properties(&wire, simulator.clone())
|
||||||
&packet.to_bytes_with_method().expect("wire packet"),
|
|
||||||
simulator.clone(),
|
|
||||||
)
|
|
||||||
.expect("properties reply");
|
.expect("properties reply");
|
||||||
assert!(observed.load(Ordering::Acquire));
|
assert!(observed.load(Ordering::Acquire));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -104,9 +104,9 @@ fn generated_constructors_initialize_blocks_headers_and_sizing_metadata() {
|
|||||||
|
|
||||||
let ack = PacketAckPacket::new_with_constructor().unwrap();
|
let ack = PacketAckPacket::new_with_constructor().unwrap();
|
||||||
assert!(ack.packets.is_empty());
|
assert!(ack.packets.is_empty());
|
||||||
// The source generator includes one variable-block metadata byte in the
|
// The low-frequency header contributes ten bytes and the empty variable
|
||||||
// base and one in the block contribution; preserve that exact Length API.
|
// block contributes its single count byte.
|
||||||
assert_eq!(ack.length(), 12);
|
assert_eq!(ack.length(), 11);
|
||||||
assert!(!ack.uses_buffer_pooling());
|
assert!(!ack.uses_buffer_pooling());
|
||||||
|
|
||||||
let circuit = UseCircuitCodePacket::new_with_constructor().unwrap();
|
let circuit = UseCircuitCodePacket::new_with_constructor().unwrap();
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ and mutable raw payload. Concrete animation, sound, script, calling-card,
|
|||||||
landmark, settings, mesh, texture, wearable, and notecard models enforce a 64 MiB
|
landmark, settings, mesh, texture, wearable, and notecard models enforce a 64 MiB
|
||||||
input ceiling and reject malformed structured formats with typed errors. Mesh and
|
input ceiling and reject malformed structured formats with typed errors. Mesh and
|
||||||
settings parsing use StructuredData. Texture encode/decode uses the imaging
|
settings parsing use StructuredData. Texture encode/decode uses the imaging
|
||||||
crate's bounded JPEG 2000 adapter, while PCM conversion streams bounded planar
|
crate's bounded `jpeg2000` adapter, while the opt-in `vorbis` feature streams
|
||||||
blocks through the cross-platform Ogg Vorbis encoder; neither API exposes codec
|
PCM conversion through bounded planar Ogg Vorbis blocks; neither API exposes
|
||||||
implementation types.
|
codec implementation types. With either native codec feature disabled, its
|
||||||
|
public API remains present and returns a typed `InvalidOperation` for otherwise
|
||||||
|
valid input instead of discovering or linking a system codec implicitly.
|
||||||
|
|
||||||
`AssetManager` is client-owned and shares one `DownloadManager` and `AssetCache`.
|
`AssetManager` is client-owned and shares one `DownloadManager` and `AssetCache`.
|
||||||
ViewerAsset, GetMesh, GetTexture, and server-bake requests check the cache before
|
ViewerAsset, GetMesh, GetTexture, and server-bake requests check the cache before
|
||||||
|
|||||||
78
docs/release-ci-matrix.md
Normal file
78
docs/release-ci-matrix.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Release platform and feature CI matrix
|
||||||
|
|
||||||
|
The release matrix is declared in [`ci/release-matrix.json`](../ci/release-matrix.json)
|
||||||
|
and validated and executed by the native Rust `metacrate-ci-matrix` tool. The
|
||||||
|
manifest is the single reviewed inventory of toolchains, targets, feature sets,
|
||||||
|
native prerequisites, Cargo commands, and explicit manual gates. The runner
|
||||||
|
does not execute shell fragments from the manifest: every entry is an
|
||||||
|
allow-listed Cargo subcommand and argument list.
|
||||||
|
|
||||||
|
## Platform boundary
|
||||||
|
|
||||||
|
Repository policy requires every Gitea Actions job to use `ubuntu-latest`.
|
||||||
|
Accordingly, the matrix runs the complete default, minimal, optional-feature,
|
||||||
|
test, example, and documentation surfaces natively on Linux. Windows GNU and
|
||||||
|
macOS use their official Rust standard-library targets from the same Ubuntu
|
||||||
|
host to compile the portable public crates with default features disabled.
|
||||||
|
Those cross-target profiles cover code selected by `cfg` without pretending to
|
||||||
|
run Windows or macOS executables or to possess an Apple SDK.
|
||||||
|
|
||||||
|
Platform-native Skia, OpenJPEG, Opus, Vorbis, and CPAL adapters are exercised
|
||||||
|
in isolated Linux profiles with their versions recorded. WASAPI, CoreAudio,
|
||||||
|
platform packaging, and physical-device behavior remain explicit release/manual
|
||||||
|
gates; a successful Ubuntu cross check is never reported as a native runtime
|
||||||
|
test on another operating system.
|
||||||
|
|
||||||
|
## Feature isolation
|
||||||
|
|
||||||
|
`libremetaverse` defaults to the pure-Rust `dds-bc67` feature. OpenJPEG-backed
|
||||||
|
JPEG 2000 and Vorbis encoding are independently selectable as `jpeg2000` and
|
||||||
|
`vorbis`; neither native codec is discovered or linked for a default library
|
||||||
|
consumer. Skia and CPAL remain isolated in their existing `skia` and
|
||||||
|
`real-audio` features. The matrix validates each feature independently before
|
||||||
|
checking workspace-wide `--all-features` unification.
|
||||||
|
|
||||||
|
The checked profiles cover:
|
||||||
|
|
||||||
|
- Rust 1.96.0 and current stable;
|
||||||
|
- native Linux default, no-default, individual optional features, and all
|
||||||
|
features;
|
||||||
|
- all targets, test compilation and execution, doctests, shipped
|
||||||
|
example-program binaries, and API documentation;
|
||||||
|
- portable Windows GNU and macOS cross-target compilation;
|
||||||
|
- exact OpenJPEG, Skia, Opus, ALSA, and Vorbis prerequisite declarations.
|
||||||
|
|
||||||
|
## Clean-build evidence
|
||||||
|
|
||||||
|
Each profile uses `target/ci/<profile>` and refuses to start if that directory
|
||||||
|
already exists. Incremental compilation is disabled. This makes an accidental
|
||||||
|
cache hit a hard failure instead of allowing it to hide a missing clean-build
|
||||||
|
dependency. Gitea may cache Cargo registry and Git downloads using a key derived
|
||||||
|
from the lockfile and toolchain; it never caches a `target` directory. Each
|
||||||
|
successful or failed run creates one JSON evidence file with the source commit,
|
||||||
|
requested toolchain and target, actual `rustc` and Cargo versions, feature sets,
|
||||||
|
native prerequisite versions, exact Cargo commands, completion count,
|
||||||
|
timestamp, and final status.
|
||||||
|
|
||||||
|
Run the audit locally with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --locked -p metacrate-ci-matrix -- audit
|
||||||
|
```
|
||||||
|
|
||||||
|
Run one clean profile with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --locked -p metacrate-ci-matrix -- \
|
||||||
|
run linux-stable-minimal \
|
||||||
|
--evidence /tmp/metacrate-linux-stable-minimal.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Before rerunning the same profile, use Cargo's scoped cleanup command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo clean --target-dir target/ci/linux-stable-minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
Live-grid credentials, physical audio devices, and proprietary services remain
|
||||||
|
outside automatic CI and retain their dedicated opt-in gates.
|
||||||
16
tools/ci-matrix/Cargo.toml
Normal file
16
tools/ci-matrix/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "metacrate-ci-matrix"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
description = "Validated release CI matrix runner for MetaCrate"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
653
tools/ci-matrix/src/lib.rs
Normal file
653
tools/ci-matrix/src/lib.rs
Normal file
@@ -0,0 +1,653 @@
|
|||||||
|
//! Validated, shell-free execution of the release CI matrix.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::fmt;
|
||||||
|
use std::fs::{self, OpenOptions};
|
||||||
|
use std::io::Write as _;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Command, ExitStatus, Stdio};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
pub const MATRIX_PATH: &str = "ci/release-matrix.json";
|
||||||
|
const WORKFLOW_PATH: &str = ".gitea/workflows/release-matrix.yml";
|
||||||
|
const REQUIRED_PROFILES: [&str; 7] = [
|
||||||
|
"linux-msrv-portable",
|
||||||
|
"linux-stable-default",
|
||||||
|
"linux-stable-minimal",
|
||||||
|
"linux-stable-features",
|
||||||
|
"linux-stable-release-surface",
|
||||||
|
"windows-stable-portable",
|
||||||
|
"macos-stable-portable",
|
||||||
|
];
|
||||||
|
const REQUIRED_FEATURE_SETS: [&str; 11] = [
|
||||||
|
"all-features",
|
||||||
|
"dds-bc67",
|
||||||
|
"default",
|
||||||
|
"docs",
|
||||||
|
"examples",
|
||||||
|
"jpeg2000",
|
||||||
|
"no-default-features",
|
||||||
|
"real-audio",
|
||||||
|
"skia",
|
||||||
|
"tests",
|
||||||
|
"vorbis",
|
||||||
|
];
|
||||||
|
const REQUIRED_FEATURES: [(&str, &str); 6] = [
|
||||||
|
("libremetaverse", "dds-bc67"),
|
||||||
|
("libremetaverse", "jpeg2000"),
|
||||||
|
("libremetaverse", "vorbis"),
|
||||||
|
("libremetaverse-imaging", "jpeg2000"),
|
||||||
|
("libremetaverse-imaging-skia", "skia"),
|
||||||
|
("libremetaverse-voice-webrtc", "real-audio"),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MatrixError(String);
|
||||||
|
|
||||||
|
impl MatrixError {
|
||||||
|
fn new(message: impl Into<String>) -> Self {
|
||||||
|
Self(message.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for MatrixError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
formatter.write_str(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for MatrixError {}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for MatrixError {
|
||||||
|
fn from(error: std::io::Error) -> Self {
|
||||||
|
Self::new(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for MatrixError {
|
||||||
|
fn from(error: serde_json::Error) -> Self {
|
||||||
|
Self::new(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, MatrixError>;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct ReleaseMatrix {
|
||||||
|
pub schema: u32,
|
||||||
|
pub msrv: String,
|
||||||
|
pub current: String,
|
||||||
|
pub native_prerequisites: Vec<NativePrerequisite>,
|
||||||
|
pub manual_gates: Vec<String>,
|
||||||
|
pub profiles: Vec<Profile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct NativePrerequisite {
|
||||||
|
pub id: String,
|
||||||
|
pub version: String,
|
||||||
|
pub purpose: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Profile {
|
||||||
|
pub id: String,
|
||||||
|
pub platform: String,
|
||||||
|
pub host: String,
|
||||||
|
pub execution: String,
|
||||||
|
pub toolchain: String,
|
||||||
|
pub target: String,
|
||||||
|
pub feature_sets: Vec<String>,
|
||||||
|
pub prerequisites: Vec<String>,
|
||||||
|
pub commands: Vec<CargoInvocation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct CargoInvocation {
|
||||||
|
pub label: String,
|
||||||
|
pub args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct Evidence<'a> {
|
||||||
|
schema: u32,
|
||||||
|
profile: &'a str,
|
||||||
|
platform: &'a str,
|
||||||
|
host: &'a str,
|
||||||
|
execution: &'a str,
|
||||||
|
target: &'a str,
|
||||||
|
requested_toolchain: &'a str,
|
||||||
|
rustc: String,
|
||||||
|
cargo: String,
|
||||||
|
source_commit: String,
|
||||||
|
recorded_unix_seconds: u64,
|
||||||
|
feature_sets: &'a [String],
|
||||||
|
native_prerequisites: Vec<&'a NativePrerequisite>,
|
||||||
|
commands: &'a [CargoInvocation],
|
||||||
|
completed_commands: usize,
|
||||||
|
status: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn workspace_root(start: &Path) -> Option<PathBuf> {
|
||||||
|
start.ancestors().find_map(|directory| {
|
||||||
|
let manifest = directory.join("Cargo.toml");
|
||||||
|
fs::read_to_string(&manifest)
|
||||||
|
.ok()
|
||||||
|
.filter(|contents| contents.contains("[workspace]"))
|
||||||
|
.map(|_| directory.to_path_buf())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the canonical release matrix from a workspace root.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error when the matrix cannot be read or is not valid JSON.
|
||||||
|
pub fn load(root: &Path) -> Result<ReleaseMatrix> {
|
||||||
|
let bytes = fs::read(root.join(MATRIX_PATH))?;
|
||||||
|
Ok(serde_json::from_slice(&bytes)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates the matrix, Cargo feature declarations, and Gitea workflow coverage.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error when any release invariant is missing or inconsistent.
|
||||||
|
pub fn audit(root: &Path, matrix: &ReleaseMatrix) -> Result<()> {
|
||||||
|
audit_shape(matrix)?;
|
||||||
|
audit_cargo_features(root)?;
|
||||||
|
audit_workflows(root, matrix)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates the platform, toolchain, feature, and command shape of a matrix.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error when a required profile, prerequisite, or gate is absent.
|
||||||
|
pub fn audit_shape(matrix: &ReleaseMatrix) -> Result<()> {
|
||||||
|
if matrix.schema != 1 {
|
||||||
|
return Err(MatrixError::new("release matrix schema must be 1"));
|
||||||
|
}
|
||||||
|
if matrix.msrv != "1.96.0" || matrix.current != "stable" {
|
||||||
|
return Err(MatrixError::new(
|
||||||
|
"release matrix must cover Rust 1.96.0 and current stable",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let prerequisite_ids = unique(
|
||||||
|
matrix
|
||||||
|
.native_prerequisites
|
||||||
|
.iter()
|
||||||
|
.map(|prerequisite| prerequisite.id.as_str()),
|
||||||
|
"native prerequisite",
|
||||||
|
)?;
|
||||||
|
if matrix.native_prerequisites.iter().any(|prerequisite| {
|
||||||
|
prerequisite.version.trim().is_empty() || prerequisite.purpose.trim().is_empty()
|
||||||
|
}) {
|
||||||
|
return Err(MatrixError::new(
|
||||||
|
"native prerequisites require exact versions and purposes",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let manual = matrix.manual_gates.join(" ").to_ascii_lowercase();
|
||||||
|
for boundary in ["live grid", "physical audio", "proprietary"] {
|
||||||
|
if !manual.contains(boundary) {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"manual gate boundary is missing {boundary}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let profile_ids = unique(
|
||||||
|
matrix.profiles.iter().map(|profile| profile.id.as_str()),
|
||||||
|
"profile",
|
||||||
|
)?;
|
||||||
|
for required in REQUIRED_PROFILES {
|
||||||
|
if !profile_ids.contains(required) {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"required profile {required} is missing"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (platforms, toolchains, feature_sets) = audit_profiles(matrix, &prerequisite_ids)?;
|
||||||
|
audit_release_surface(matrix)?;
|
||||||
|
if platforms != BTreeSet::from(["linux", "macos", "windows"])
|
||||||
|
|| !toolchains.contains("stable")
|
||||||
|
|| !toolchains.contains("1.96.0")
|
||||||
|
{
|
||||||
|
return Err(MatrixError::new(
|
||||||
|
"matrix must cover Linux, macOS, Windows, MSRV, and stable",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for required in REQUIRED_FEATURE_SETS {
|
||||||
|
if !feature_sets.contains(required) {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"feature/test surface {required} is missing"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn audit_release_surface(matrix: &ReleaseMatrix) -> Result<()> {
|
||||||
|
let profile = matrix
|
||||||
|
.profiles
|
||||||
|
.iter()
|
||||||
|
.find(|profile| profile.id == "linux-stable-release-surface")
|
||||||
|
.ok_or_else(|| MatrixError::new("release-surface profile is missing"))?;
|
||||||
|
let builds_programs = profile.commands.iter().any(|invocation| {
|
||||||
|
invocation
|
||||||
|
.args
|
||||||
|
.first()
|
||||||
|
.is_some_and(|argument| argument == "build")
|
||||||
|
&& invocation
|
||||||
|
.args
|
||||||
|
.windows(2)
|
||||||
|
.any(|pair| pair == ["-p", "libremetaverse-programs"])
|
||||||
|
&& invocation.args.iter().any(|argument| argument == "--bins")
|
||||||
|
});
|
||||||
|
if !builds_programs {
|
||||||
|
return Err(MatrixError::new(
|
||||||
|
"release-surface profile must build the shipped example-program binaries",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn audit_profiles<'a>(
|
||||||
|
matrix: &'a ReleaseMatrix,
|
||||||
|
prerequisite_ids: &BTreeSet<&str>,
|
||||||
|
) -> Result<(BTreeSet<&'a str>, BTreeSet<&'a str>, BTreeSet<&'a str>)> {
|
||||||
|
let mut platforms = BTreeSet::new();
|
||||||
|
let mut toolchains = BTreeSet::new();
|
||||||
|
let mut feature_sets = BTreeSet::new();
|
||||||
|
for profile in &matrix.profiles {
|
||||||
|
if profile.host != "ubuntu-latest" {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"profile {} violates the ubuntu-latest Gitea constraint",
|
||||||
|
profile.id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if !matches!(profile.platform.as_str(), "linux" | "windows" | "macos") {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"profile {} has an unsupported platform",
|
||||||
|
profile.id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let expected_execution = if profile.platform == "linux" {
|
||||||
|
"native"
|
||||||
|
} else {
|
||||||
|
"cross"
|
||||||
|
};
|
||||||
|
if profile.execution != expected_execution {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"profile {} must use {expected_execution} execution",
|
||||||
|
profile.id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if profile.target.trim().is_empty() || profile.commands.is_empty() {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"profile {} needs a target and at least one command",
|
||||||
|
profile.id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
for prerequisite in &profile.prerequisites {
|
||||||
|
if !prerequisite_ids.contains(prerequisite.as_str()) {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"profile {} references unknown prerequisite {prerequisite}",
|
||||||
|
profile.id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for invocation in &profile.commands {
|
||||||
|
audit_invocation(profile, invocation)?;
|
||||||
|
}
|
||||||
|
platforms.insert(profile.platform.as_str());
|
||||||
|
toolchains.insert(profile.toolchain.as_str());
|
||||||
|
feature_sets.extend(profile.feature_sets.iter().map(String::as_str));
|
||||||
|
}
|
||||||
|
Ok((platforms, toolchains, feature_sets))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unique<'a>(values: impl Iterator<Item = &'a str>, kind: &str) -> Result<BTreeSet<&'a str>> {
|
||||||
|
let mut unique = BTreeSet::new();
|
||||||
|
for value in values {
|
||||||
|
if value.trim().is_empty() || !unique.insert(value) {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"{kind} identifiers must be nonempty and unique"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(unique)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn audit_invocation(profile: &Profile, invocation: &CargoInvocation) -> Result<()> {
|
||||||
|
let Some(subcommand) = invocation.args.first().map(String::as_str) else {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"profile {} contains an empty Cargo command",
|
||||||
|
profile.id
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
if !matches!(subcommand, "check" | "test" | "doc" | "build") {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"profile {} uses disallowed Cargo subcommand {subcommand}",
|
||||||
|
profile.id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if invocation.label.trim().is_empty()
|
||||||
|
|| !invocation
|
||||||
|
.args
|
||||||
|
.iter()
|
||||||
|
.any(|argument| argument == "--locked")
|
||||||
|
|| !invocation.args.windows(2).any(|pair| pair == ["-j", "1"])
|
||||||
|
{
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"profile {} commands need labels, --locked, and -j 1",
|
||||||
|
profile.id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if profile.execution == "cross"
|
||||||
|
&& !invocation
|
||||||
|
.args
|
||||||
|
.windows(2)
|
||||||
|
.any(|pair| pair == ["--target", profile.target.as_str()])
|
||||||
|
{
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"cross profile {} command does not select its target",
|
||||||
|
profile.id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn audit_cargo_features(root: &Path) -> Result<()> {
|
||||||
|
let output = Command::new(cargo_program())
|
||||||
|
.args(["metadata", "--no-deps", "--format-version", "1"])
|
||||||
|
.current_dir(root)
|
||||||
|
.output()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(MatrixError::new(
|
||||||
|
"cargo metadata failed during matrix audit",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let metadata: Value = serde_json::from_slice(&output.stdout)?;
|
||||||
|
let packages = metadata["packages"]
|
||||||
|
.as_array()
|
||||||
|
.ok_or_else(|| MatrixError::new("cargo metadata has no package array"))?;
|
||||||
|
let feature_map = packages
|
||||||
|
.iter()
|
||||||
|
.filter_map(|package| Some((package["name"].as_str()?, package["features"].as_object()?)))
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
for (package, feature) in REQUIRED_FEATURES {
|
||||||
|
if !feature_map
|
||||||
|
.get(package)
|
||||||
|
.is_some_and(|features| features.contains_key(feature))
|
||||||
|
{
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"Cargo feature {package}/{feature} is missing"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let defaults = feature_map
|
||||||
|
.get("libremetaverse")
|
||||||
|
.and_then(|features| features.get("default"))
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.ok_or_else(|| MatrixError::new("libremetaverse default feature set is missing"))?;
|
||||||
|
if defaults.iter().any(|feature| {
|
||||||
|
matches!(
|
||||||
|
feature.as_str(),
|
||||||
|
Some("jpeg2000" | "vorbis" | "dep:vorbis_rs")
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
return Err(MatrixError::new(
|
||||||
|
"default consumers must not enable native OpenJPEG or Vorbis adapters",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let default_tree = Command::new(cargo_program())
|
||||||
|
.args([
|
||||||
|
"tree",
|
||||||
|
"--locked",
|
||||||
|
"-p",
|
||||||
|
"libremetaverse",
|
||||||
|
"--edges",
|
||||||
|
"normal,build",
|
||||||
|
"--prefix",
|
||||||
|
"none",
|
||||||
|
])
|
||||||
|
.current_dir(root)
|
||||||
|
.output()?;
|
||||||
|
if !default_tree.status.success() {
|
||||||
|
return Err(MatrixError::new(
|
||||||
|
"cargo tree failed during default dependency audit",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let default_tree = String::from_utf8_lossy(&default_tree.stdout);
|
||||||
|
for forbidden in [
|
||||||
|
"libremetaverse-openjpeg ",
|
||||||
|
"vorbis_rs ",
|
||||||
|
"ogg_next_sys ",
|
||||||
|
"aotuv_lancer_vorbis_sys ",
|
||||||
|
] {
|
||||||
|
if default_tree.lines().any(|line| line.starts_with(forbidden)) {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"default consumer dependency graph unexpectedly contains {forbidden}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn audit_workflows(root: &Path, matrix: &ReleaseMatrix) -> Result<()> {
|
||||||
|
let workflow = fs::read_to_string(root.join(WORKFLOW_PATH))?;
|
||||||
|
for profile in &matrix.profiles {
|
||||||
|
if !workflow.contains(&format!("profile: {}", profile.id)) {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"workflow does not schedule profile {}",
|
||||||
|
profile.id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !workflow.contains("~/.cargo/registry")
|
||||||
|
|| !workflow.contains("~/.cargo/git")
|
||||||
|
|| workflow.lines().any(|line| {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
trimmed == "target" || trimmed.starts_with("target/") || trimmed.contains("/target/")
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err(MatrixError::new(
|
||||||
|
"workflow must cache Cargo downloads without caching build target directories",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let workflows = root.join(".gitea/workflows");
|
||||||
|
for entry in fs::read_dir(workflows)? {
|
||||||
|
let path = entry?.path();
|
||||||
|
if !matches!(
|
||||||
|
path.extension().and_then(OsStr::to_str),
|
||||||
|
Some("yml" | "yaml")
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let contents = fs::read_to_string(&path)?.to_ascii_lowercase();
|
||||||
|
if contents.contains("runs-on: windows") || contents.contains("runs-on: macos") {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"{} violates the ubuntu-only Gitea runner policy",
|
||||||
|
path.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes one profile from an empty target directory and writes its evidence record.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error when the profile is unknown, its toolchain is incorrect, the
|
||||||
|
/// target directory is not clean, a command fails, or evidence cannot be written.
|
||||||
|
pub fn run(root: &Path, matrix: &ReleaseMatrix, profile_id: &str, evidence: &Path) -> Result<()> {
|
||||||
|
audit(root, matrix)?;
|
||||||
|
let profile = matrix
|
||||||
|
.profiles
|
||||||
|
.iter()
|
||||||
|
.find(|profile| profile.id == profile_id)
|
||||||
|
.ok_or_else(|| MatrixError::new(format!("unknown CI profile {profile_id}")))?;
|
||||||
|
if evidence.exists() {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"{} already exists; preserve or remove it before running the profile",
|
||||||
|
evidence.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let rustc = version("rustc")?;
|
||||||
|
let cargo = version(cargo_program())?;
|
||||||
|
if profile.toolchain != "stable"
|
||||||
|
&& rustc
|
||||||
|
.split_whitespace()
|
||||||
|
.nth(1)
|
||||||
|
.is_none_or(|version| version != profile.toolchain)
|
||||||
|
{
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"profile {} requires rustc {}, found {rustc}",
|
||||||
|
profile.id, profile.toolchain
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let target_directory = root.join("target/ci").join(&profile.id);
|
||||||
|
if target_directory.exists() {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"{} already exists; clean it before rerunning so cache cannot mask the build",
|
||||||
|
target_directory.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let source_commit = command_line("git", ["rev-parse", "--verify", "HEAD"], root)?;
|
||||||
|
let prerequisites = profile
|
||||||
|
.prerequisites
|
||||||
|
.iter()
|
||||||
|
.map(|id| {
|
||||||
|
matrix
|
||||||
|
.native_prerequisites
|
||||||
|
.iter()
|
||||||
|
.find(|prerequisite| prerequisite.id == *id)
|
||||||
|
.ok_or_else(|| MatrixError::new(format!("unknown prerequisite {id}")))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
let mut completed = 0;
|
||||||
|
let mut failure = None;
|
||||||
|
for invocation in &profile.commands {
|
||||||
|
println!("ci-matrix {}: {}", profile.id, invocation.label);
|
||||||
|
let status = Command::new(cargo_program())
|
||||||
|
.args(&invocation.args)
|
||||||
|
.current_dir(root)
|
||||||
|
.env("CARGO_INCREMENTAL", "0")
|
||||||
|
.env("CARGO_TARGET_DIR", &target_directory)
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.status()?;
|
||||||
|
if status.success() {
|
||||||
|
completed += 1;
|
||||||
|
} else {
|
||||||
|
failure = Some(status);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let status = if failure.is_none() { "ok" } else { "failed" };
|
||||||
|
write_evidence(
|
||||||
|
evidence,
|
||||||
|
&Evidence {
|
||||||
|
schema: 1,
|
||||||
|
profile: &profile.id,
|
||||||
|
platform: &profile.platform,
|
||||||
|
host: &profile.host,
|
||||||
|
execution: &profile.execution,
|
||||||
|
target: &profile.target,
|
||||||
|
requested_toolchain: &profile.toolchain,
|
||||||
|
rustc,
|
||||||
|
cargo,
|
||||||
|
source_commit,
|
||||||
|
recorded_unix_seconds: SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map_err(|_| MatrixError::new("system clock predates Unix epoch"))?
|
||||||
|
.as_secs(),
|
||||||
|
feature_sets: &profile.feature_sets,
|
||||||
|
native_prerequisites: prerequisites,
|
||||||
|
commands: &profile.commands,
|
||||||
|
completed_commands: completed,
|
||||||
|
status,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
if let Some(status) = failure {
|
||||||
|
return Err(MatrixError::new(format!(
|
||||||
|
"profile {} failed with {}",
|
||||||
|
profile.id,
|
||||||
|
describe_status(status)
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn version(program: impl AsRef<OsStr>) -> Result<String> {
|
||||||
|
command_line(program, ["--version"], Path::new("."))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_line(
|
||||||
|
program: impl AsRef<OsStr>,
|
||||||
|
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
|
||||||
|
directory: &Path,
|
||||||
|
) -> Result<String> {
|
||||||
|
let output = Command::new(program)
|
||||||
|
.args(args)
|
||||||
|
.current_dir(directory)
|
||||||
|
.output()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(MatrixError::new("version/provenance command failed"));
|
||||||
|
}
|
||||||
|
String::from_utf8(output.stdout)
|
||||||
|
.map(|value| value.trim().to_owned())
|
||||||
|
.map_err(|_| MatrixError::new("version/provenance output was not UTF-8"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cargo_program() -> std::ffi::OsString {
|
||||||
|
std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_evidence(path: &Path, evidence: &Evidence<'_>) -> Result<()> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let mut file = OpenOptions::new().write(true).create_new(true).open(path)?;
|
||||||
|
serde_json::to_writer_pretty(&mut file, evidence)?;
|
||||||
|
file.write_all(b"\n")?;
|
||||||
|
file.sync_all()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn describe_status(status: ExitStatus) -> String {
|
||||||
|
status.code().map_or_else(
|
||||||
|
|| "termination by signal".to_owned(),
|
||||||
|
|code| format!("exit {code}"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn checked_in_matrix_has_complete_shape() {
|
||||||
|
let matrix: ReleaseMatrix =
|
||||||
|
serde_json::from_str(include_str!("../../../ci/release-matrix.json")).unwrap();
|
||||||
|
audit_shape(&matrix).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runner_rejects_unknown_profiles_before_execution() {
|
||||||
|
let root = workspace_root(Path::new(env!("CARGO_MANIFEST_DIR"))).unwrap();
|
||||||
|
let matrix = load(&root).unwrap();
|
||||||
|
let evidence = std::env::temp_dir().join("metacrate-ci-matrix-unknown.json");
|
||||||
|
assert!(run(&root, &matrix, "not-a-profile", &evidence).is_err());
|
||||||
|
assert!(!evidence.exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
47
tools/ci-matrix/src/main.rs
Normal file
47
tools/ci-matrix/src/main.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use metacrate_ci_matrix::{audit, load, run, workspace_root};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if let Err(error) = execute() {
|
||||||
|
eprintln!("ci-matrix: {error}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let current = std::env::current_dir()?;
|
||||||
|
let root = workspace_root(¤t).ok_or("could not locate the Cargo workspace")?;
|
||||||
|
let matrix = load(&root)?;
|
||||||
|
let mut arguments = std::env::args().skip(1);
|
||||||
|
match arguments.next().as_deref() {
|
||||||
|
Some("audit") if arguments.next().is_none() => {
|
||||||
|
audit(&root, &matrix)?;
|
||||||
|
println!(
|
||||||
|
"release CI matrix: {} validated profiles; Linux, Windows, macOS, MSRV, stable, default/minimal/optional features, tests, examples, and docs covered",
|
||||||
|
matrix.profiles.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some("run") => {
|
||||||
|
let profile = arguments.next().ok_or("run requires a profile")?;
|
||||||
|
let flag = arguments.next().ok_or("run requires --evidence FILE")?;
|
||||||
|
let evidence = arguments.next().ok_or("run requires --evidence FILE")?;
|
||||||
|
if flag != "--evidence" || arguments.next().is_some() {
|
||||||
|
return Err("usage: ci-matrix run PROFILE --evidence FILE".into());
|
||||||
|
}
|
||||||
|
let evidence = absolute_or_rooted(&root, &evidence);
|
||||||
|
run(&root, &matrix, &profile, &evidence)?;
|
||||||
|
println!("release CI profile {profile}: ok ({})", evidence.display());
|
||||||
|
}
|
||||||
|
_ => return Err("usage: ci-matrix audit | run PROFILE --evidence FILE".into()),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn absolute_or_rooted(root: &Path, value: &str) -> PathBuf {
|
||||||
|
let path = PathBuf::from(value);
|
||||||
|
if path.is_absolute() {
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
root.join(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user