diff --git a/.gitea/workflows/api-surface.yml b/.gitea/workflows/api-surface.yml deleted file mode 100644 index ff621c7..0000000 --- a/.gitea/workflows/api-surface.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: API and SemVer surface - -on: - push: - paths: - - ".gitea/workflows/api-surface.yml" - - "Cargo.lock" - - "Cargo.toml" - - "api/**" - - "ci/evidence/api-audit.json" - - "crates/**" - - "tests/api-compile/**" - - "tests/semver-port/**" - - "tools/check_api_coverage.py" - - "tools/generate_api_shims.py" - - "tools/generate_rust_mapping.py" - - "tools/ci-matrix/**" - pull_request: - paths: - - ".gitea/workflows/api-surface.yml" - - "Cargo.lock" - - "Cargo.toml" - - "api/**" - - "ci/evidence/api-audit.json" - - "crates/**" - - "tests/api-compile/**" - - "tests/semver-port/**" - - "tools/check_api_coverage.py" - - "tools/generate_api_shims.py" - - "tools/generate_rust_mapping.py" - - "tools/ci-matrix/**" - -jobs: - api-surface: - runs-on: ubuntu-latest - env: - CARGO_BUILD_JOBS: "1" - CARGO_INCREMENTAL: "0" - CARGO_PROFILE_DEV_DEBUG: "0" - steps: - - uses: actions/checkout@v4 - - - name: Install native prerequisites - run: sudo apt-get update && sudo apt-get install -y libopus-dev pkg-config - - - name: Install Rust 1.97 - uses: dtolnay/rust-toolchain@stable - with: - toolchain: "1.97.0" - components: rustfmt, clippy - - - name: Verify deterministic mapping and export coverage - run: | - python3 tools/generate_rust_mapping.py --check - python3 tools/generate_api_shims.py --check - python3 tools/check_api_coverage.py - - - name: Verify reviewed API baseline - run: | - cargo run --locked -p metacrate-ci-matrix -- api-baseline-write - git diff --exit-code -- api/SEMVER-BASELINE.json api/SEMVER-AUDIT.md - cargo run --locked -p metacrate-ci-matrix -- api-audit --evidence /tmp/metacrate-api-audit.json - cmp /tmp/metacrate-api-audit.json ci/evidence/api-audit.json - - - name: Compile downstream API consumers - run: | - cargo check --locked --manifest-path tests/api-compile/Cargo.toml - cargo run --locked --manifest-path tests/semver-port/Cargo.toml - - - name: Check formatting and lints - run: | - cargo fmt --all -- --check - cargo fmt --manifest-path tests/semver-port/Cargo.toml -- --check - cargo clippy --locked -p metacrate-ci-matrix --all-targets -- -D warnings - cargo clippy --locked --manifest-path tests/semver-port/Cargo.toml --all-targets -- -D warnings - - - name: Upload audit evidence - uses: actions/upload-artifact@v3 - with: - name: api-semver-audit - path: ci/evidence/api-audit.json diff --git a/.gitea/workflows/artifact-audit.yml b/.gitea/workflows/artifact-audit.yml deleted file mode 100644 index de5b969..0000000 --- a/.gitea/workflows/artifact-audit.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: Native release artifact audit - -on: - push: - paths: - - ".gitea/workflows/artifact-audit.yml" - - "ci/artifact-policy.json" - - "ci/dependency-policy.json" - - "ci/provenance-policy.json" - - "tools/ci-matrix/**" - - "tools/install_openjpeg_2_5_4.sh" - - "docs/release-artifacts.md" - - "Cargo.toml" - - "Cargo.lock" - - "crates/**" - - "programs/**" - pull_request: - paths: - - ".gitea/workflows/artifact-audit.yml" - - "ci/artifact-policy.json" - - "ci/dependency-policy.json" - - "ci/provenance-policy.json" - - "tools/ci-matrix/**" - - "tools/install_openjpeg_2_5_4.sh" - - "docs/release-artifacts.md" - - "Cargo.toml" - - "Cargo.lock" - - "crates/**" - - "programs/**" - workflow_dispatch: - -env: - CARGO_BUILD_JOBS: 1 - CARGO_INCREMENTAL: 0 - CARGO_PROFILE_RELEASE_OPT_LEVEL: 0 - FORCE_SKIA_BINARIES_DOWNLOAD: 1 - OPENJPEG_PREFIX: /tmp/metacrate-openjpeg-artifact-audit - PKG_CONFIG_PATH: /tmp/metacrate-openjpeg-artifact-audit/lib/pkgconfig - LD_LIBRARY_PATH: /tmp/metacrate-openjpeg-artifact-audit/lib - -jobs: - audit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: 1.97.1 - - name: Install documented native prerequisites - run: | - sudo apt-get update - sudo apt-get install --yes build-essential clang cmake curl ninja-build pkg-config python3 libfontconfig1-dev libfreetype6-dev libopus-dev - tools/install_openjpeg_2_5_4.sh "$OPENJPEG_PREFIX" - pkg-config --exact-version 2.5.4 libopenjp2 - pkg-config --modversion opus - - name: Record source identity without a shipped build helper - run: echo "METACRATE_RUST_COMMIT=$(git rev-parse --verify HEAD)" >>"$GITHUB_ENV" - - name: Fetch the locked dependency graph - run: cargo fetch --locked - - name: Create every release source package atomically - run: | - cargo package --locked --no-verify \ - -p libremetaverse-types \ - -p libremetaverse-structured-data \ - -p libremetaverse-imaging \ - -p libremetaverse-imaging-skia \ - -p libremetaverse-openjpeg \ - -p libremetaverse-opus \ - -p libremetaverse-prim-mesher \ - -p libremetaverse-lsl-tools \ - -p libremetaverse \ - -p libremetaverse-rendering-simple \ - -p libremetaverse-rendering-mesh-foundry \ - -p libremetaverse-rlv \ - -p libremetaverse-utilities \ - -p libremetaverse-voice-vivox \ - -p libremetaverse-voice-webrtc \ - -p libremetaverse-programs - - name: Build every shipped native executable - run: cargo build --locked --release -j 1 -p libremetaverse-programs --bins - - name: Inspect packages, binaries, links, and offline runtime behavior - run: | - cargo run --locked -p metacrate-ci-matrix -- artifact-audit \ - --artifact-dir target/release \ - --package-dir target/package \ - --evidence artifacts/release/artifact-audit.json - - name: Upload immutable audit evidence and source packages - if: always() - uses: actions/upload-artifact@v3 - with: - name: native-release-artifact-audit - path: | - artifacts/release/artifact-audit.json - target/package/*.crate - if-no-files-found: error diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..501f078 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,118 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + inputs: + cold_cache: + description: Deliberately clear the required dependency/native cache before validation + required: false + default: false + type: boolean + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + required: + name: required + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + CARGO_BUILD_JOBS: "2" + CARGO_INCREMENTAL: "0" + CARGO_PROFILE_DEV_DEBUG: "0" + CARGO_PROFILE_TEST_DEBUG: "0" + RUSTDOCFLAGS: "-D warnings" + CARGO_HOME: ${{ runner.tool_cache }}/metacrate/cargo/home-rust-1.97.1-aarch64-unknown-linux-gnu + METACRATE_EXPECTED_HOST: aarch64-unknown-linux-gnu + METACRATE_PROVENANCE_DIAGNOSTICS_DIR: artifacts/ci/provenance-candidates + steps: + - uses: actions/checkout@v4 + + - name: Inspect the runner-persistent, architecture-keyed cache + run: | + cargo_cache_root="${{ runner.tool_cache }}/metacrate/cargo" + cache_key="$(git ls-files -z 'Cargo.lock' 'Cargo.toml' '**/Cargo.toml' 'ci/ci-coverage.json' 'ci/dependency-policy.json' 'deny.toml' 'tools/install_openjpeg_2_5_4.sh' 'tools/normalize_git_mtimes.py' | sort -z | xargs -0 sha256sum | sha256sum | cut -d ' ' -f 1)" + cargo_target="$cargo_cache_root/target-rust-1.97.1-aarch64-unknown-linux-gnu-$cache_key" + openjpeg_prefix="${{ runner.tool_cache }}/metacrate/native/openjpeg-2.5.4-${{ runner.arch }}" + skia_dir="${{ runner.tool_cache }}/metacrate/native/skia-0.99.0-aarch64-unknown-linux-gnu" + skia_archive="$skia_dir/skia-binaries-a25a0fdb7d90429aa2d1-aarch64-unknown-linux-gnu-jpegd-jpege-pdf-svg-textlayout-vulkan-webpd-webpe.tar.gz" + if ! test -d "$cargo_target"; then + previous_target="$(find "$cargo_cache_root" -mindepth 1 -maxdepth 1 -type d -name 'target-rust-1.97.1-aarch64-unknown-linux-gnu-*' -printf '%T@ %p\n' | sort -nr | head -n 1 | cut -d ' ' -f 2-)" + if test -n "$previous_target"; then + cp -al "$previous_target" "$cargo_target" + fi + fi + if test "${{ github.event_name }}" = workflow_dispatch && test "${{ inputs.cold_cache }}" = true; then + rm -rf -- "$CARGO_HOME/registry/cache" "$CARGO_HOME/registry/index" "$CARGO_HOME/git/db" "$openjpeg_prefix" "$skia_dir" + rm -f -- "$CARGO_HOME/bin/cargo-deny" "$CARGO_HOME/bin/cargo-machete" + fi + mkdir -p "$CARGO_HOME" "$cargo_target" "$skia_dir" + test -f "$cargo_target/.metacrate-ready" && cargo_hit=true || cargo_hit=false + test -d "$CARGO_HOME/registry/cache" && dependency_hit=true || dependency_hit=false + test -f "$openjpeg_prefix/metacrate-openjpeg.identity" && test -f "$skia_archive" && native_hit=true || native_hit=false + test -x "$CARGO_HOME/bin/cargo-deny" && test -x "$CARGO_HOME/bin/cargo-machete" && tools_hit=true || tools_hit=false + echo "CARGO_TARGET_DIR=$cargo_target" >> "$GITHUB_ENV" + echo "OPENJPEG_PREFIX=$openjpeg_prefix" >> "$GITHUB_ENV" + echo "PKG_CONFIG_PATH=$openjpeg_prefix/lib/pkgconfig" >> "$GITHUB_ENV" + echo "LD_LIBRARY_PATH=$openjpeg_prefix/lib" >> "$GITHUB_ENV" + echo "METACRATE_SKIA_ARCHIVE=$skia_archive" >> "$GITHUB_ENV" + echo "METACRATE_CARGO_CACHE_HIT=$cargo_hit" >> "$GITHUB_ENV" + echo "METACRATE_DEPENDENCY_CACHE_HIT=$dependency_hit" >> "$GITHUB_ENV" + echo "METACRATE_NATIVE_CACHE_HIT=$native_hit" >> "$GITHUB_ENV" + echo "METACRATE_TOOLS_CACHE_HIT=$tools_hit" >> "$GITHUB_ENV" + + - name: Normalize tracked mtimes for reusable Cargo fingerprints + run: | + python3 tools/normalize_git_mtimes.py --self-test + python3 tools/normalize_git_mtimes.py --state "$CARGO_TARGET_DIR/.metacrate-mtimes.json" + python3 tools/normalize_git_mtimes.py --state "$CARGO_TARGET_DIR/.metacrate-mtimes.json" --check + + - name: Install the single native prerequisite set + run: | + sudo apt-get update + sudo apt-get install --yes build-essential clang cmake curl git ninja-build pkg-config python3 libfontconfig1-dev libfreetype6-dev libopus-dev libasound2-dev + + - name: Install pinned Rust and audit tools + uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.97.1 + components: clippy,rustfmt + - uses: taiki-e/install-action@v2 + with: + tool: cargo-deny@0.20.2,cargo-machete@0.9.2 + + - name: Verify pinned audit tool versions + run: | + cargo deny --version | grep '0.20.2' + cargo machete --version | grep '0.9.2' + + - name: Validate and populate the exact native cache + run: | + test "$(uname -m)" = aarch64 + rustc -vV | grep '^host: aarch64-unknown-linux-gnu$' + tools/install_openjpeg_2_5_4.sh "$OPENJPEG_PREFIX" + pkg-config --exact-version 2.5.4 libopenjp2 + mkdir -p "$(dirname "$METACRATE_SKIA_ARCHIVE")" + if ! echo 'dd127f458a5e67a79f3936a8aa19f822fe90a1d6a11b50b5f84df2b0519d909c '"$METACRATE_SKIA_ARCHIVE" | sha256sum --check --status; then + curl --fail --location --proto '=https' --tlsv1.2 --output "$METACRATE_SKIA_ARCHIVE" \ + https://github.com/rust-skia/skia-binaries/releases/download/0.99.0/skia-binaries-a25a0fdb7d90429aa2d1-aarch64-unknown-linux-gnu-jpegd-jpege-pdf-svg-textlayout-vulkan-webpd-webpe.tar.gz + fi + echo 'dd127f458a5e67a79f3936a8aa19f822fe90a1d6a11b50b5f84df2b0519d909c '"$METACRATE_SKIA_ARCHIVE" | sha256sum --check + + - name: Run the authoritative code-ready graph + run: cargo run --locked -p metacrate-ci-matrix -- required-gate --evidence artifacts/ci/required-gate.json + + - name: Mark the validated cache generation ready + run: touch "$CARGO_TARGET_DIR/.metacrate-ready" + + - name: Upload stage timing and audit evidence + if: always() + uses: actions/upload-artifact@v3 + with: + name: required-ci-evidence + path: artifacts/ci/ + if-no-files-found: error diff --git a/.gitea/workflows/codegen.yml b/.gitea/workflows/codegen.yml deleted file mode 100644 index c616c8a..0000000 --- a/.gitea/workflows/codegen.yml +++ /dev/null @@ -1,175 +0,0 @@ -name: Native code generation - -on: - push: - paths: - - ".gitea/workflows/codegen.yml" - - "Cargo.toml" - - "Cargo.lock" - - "api/**" - - "codegen/**" - - "tools/codegen/**" - - "tools/generate_*.py" - - "tools/install_openjpeg_2_5_4.sh" - - "tools/check_api_coverage.py" - - "tools/check_codegen_gate.py" - - "tools/check_milestone_08.py" - - "tools/check_milestone_09_issue_59.py" - - "tools/check_milestone_09_issue_60.py" - - "tools/check_milestone_09_issue_61.py" - - "tools/check_milestone_09_issue_62.py" - - "tools/check_milestone_09_issue_63.py" - - "tools/check_milestone_09_issue_64.py" - - "tools/check_milestone_09_issue_65.py" - - "tools/check_milestone_09_issue_66.py" - - "tools/check_milestone_09.py" - - "tools/test_milestone_09.py" - - "crates/libremetaverse/src/agent_movement.rs" - - "crates/libremetaverse/src/inventory.rs" - - "crates/libremetaverse/src/inventory_manager.rs" - - "crates/libremetaverse/src/inventory_ais.rs" - - "crates/libremetaverse/src/inventory_ais_internal_semantics.rs" - - "docs/agent-movement.md" - - "docs/inventory.md" - - "crates/libremetaverse/Cargo.toml" - - "crates/libremetaverse/src/generated.rs" - - "crates/libremetaverse/src/packets.rs" - - "crates/libremetaverse/src/packet_*.rs" - - "crates/libremetaverse/src/*_catalog.rs" - - "tests/api-compile/**" - - "tests/compat/Cargo.toml" - - "tests/compat/build.rs" - - "tests/compat/src/**" - - "tests/compat/tests/wire_semantics.rs" - - "tests/compat/tests/appearance_visual_semantics.rs" - - "tests/compat/tests/inventory_store_semantics.rs" - - "tests/compat/tests/inventory_manager_semantics.rs" - - "tests/compat/tests/inventory_ais_semantics.rs" - - "tests/compat/tests/task_inventory_semantics.rs" - - "tests/compat/tests/misclassified_link_semantics.rs" - - "tests/upstream-tests.json" - pull_request: - paths: - - ".gitea/workflows/codegen.yml" - - "Cargo.toml" - - "Cargo.lock" - - "api/**" - - "codegen/**" - - "tools/codegen/**" - - "tools/generate_*.py" - - "tools/install_openjpeg_2_5_4.sh" - - "tools/check_api_coverage.py" - - "tools/check_codegen_gate.py" - - "tools/check_milestone_08.py" - - "tools/check_milestone_09_issue_59.py" - - "tools/check_milestone_09_issue_60.py" - - "tools/check_milestone_09_issue_61.py" - - "tools/check_milestone_09_issue_62.py" - - "tools/check_milestone_09_issue_63.py" - - "tools/check_milestone_09_issue_64.py" - - "tools/check_milestone_09_issue_65.py" - - "tools/check_milestone_09_issue_66.py" - - "tools/check_milestone_09.py" - - "tools/test_milestone_09.py" - - "crates/libremetaverse/src/agent_movement.rs" - - "crates/libremetaverse/src/inventory.rs" - - "crates/libremetaverse/src/inventory_manager.rs" - - "crates/libremetaverse/src/inventory_ais.rs" - - "crates/libremetaverse/src/inventory_ais_internal_semantics.rs" - - "docs/agent-movement.md" - - "docs/inventory.md" - - "crates/libremetaverse/Cargo.toml" - - "crates/libremetaverse/src/generated.rs" - - "crates/libremetaverse/src/packets.rs" - - "crates/libremetaverse/src/packet_*.rs" - - "crates/libremetaverse/src/*_catalog.rs" - - "tests/api-compile/**" - - "tests/compat/Cargo.toml" - - "tests/compat/build.rs" - - "tests/compat/src/**" - - "tests/compat/tests/wire_semantics.rs" - - "tests/compat/tests/appearance_visual_semantics.rs" - - "tests/compat/tests/inventory_store_semantics.rs" - - "tests/compat/tests/inventory_manager_semantics.rs" - - "tests/compat/tests/inventory_ais_semantics.rs" - - "tests/compat/tests/task_inventory_semantics.rs" - - "tests/compat/tests/misclassified_link_semantics.rs" - - "tests/upstream-tests.json" - workflow_dispatch: - -jobs: - deterministic: - runs-on: ubuntu-latest - env: - OPENJPEG_PREFIX: /tmp/metacrate-openjpeg-${{ github.run_id }} - PKG_CONFIG_PATH: /tmp/metacrate-openjpeg-${{ github.run_id }}/lib/pkgconfig - LD_LIBRARY_PATH: /tmp/metacrate-openjpeg-${{ github.run_id }}/lib - CARGO_BUILD_JOBS: 1 - CARGO_INCREMENTAL: 0 - CARGO_PROFILE_DEV_DEBUG: 0 - CARGO_PROFILE_TEST_DEBUG: 0 - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy,rustfmt - - name: Install audited OpenJPEG 2.5.4 - run: | - sudo apt-get update - sudo apt-get install --yes build-essential cmake pkg-config - tools/install_openjpeg_2_5_4.sh "$OPENJPEG_PREFIX" - - name: Test diagnostics and byte-identical generation - run: cargo test -p libremetaverse-codegen - - name: Verify vendored hashes and checked-in output - run: cargo run -p libremetaverse-codegen -- check - - name: Verify authoritative API mappings and consumer fixture - run: | - python3 tools/generate_rust_mapping.py --check - python3 tools/generate_api_shims.py --check - python3 tools/check_api_coverage.py - python3 tools/check_milestone_08.py - python3 tools/check_milestone_09_issue_59.py - python3 tools/check_milestone_09_issue_60.py - python3 tools/check_milestone_09_issue_61.py - python3 tools/check_milestone_09_issue_62.py - python3 tools/check_milestone_09_issue_63.py - python3 tools/check_milestone_09_issue_64.py - python3 tools/check_milestone_09_issue_65.py - python3 tools/check_milestone_09_issue_66.py - python3 tools/check_milestone_09.py - cargo check --manifest-path tests/api-compile/Cargo.toml --locked - - name: Test generated wire codecs and translated parity cases - run: | - cargo test -p libremetaverse --lib generated_codec_tests - cargo test -p libremetaverse --test packet_wire - cargo test -p libremetaverse --test packet_osd - cargo test -p libremetaverse-compat-tests --test wire_semantics - cargo test -p libremetaverse-compat-tests --test inventory_store_semantics - cargo test -p libremetaverse-compat-tests --test inventory_manager_semantics - cargo test -p libremetaverse-compat-tests --test inventory_ais_semantics - cargo test -p libremetaverse --lib inventory_ais_internal_semantics - cargo test -p libremetaverse-compat-tests --test task_inventory_semantics - cargo test -p libremetaverse-compat-tests --test misclassified_link_semantics - - name: Test generated visual and foliage catalogs - run: | - cargo test -p libremetaverse --test generated_catalogs - cargo test -p libremetaverse-compat-tests --test appearance_visual_semantics - cargo test -p libremetaverse-compat-tests --test appearance_semantics - cargo test -p libremetaverse --lib appearance_ - cargo test -p libremetaverse --lib initial_outfit::tests - - name: Test generated skeleton, attention, and genepool catalogs - run: | - cargo test -p libremetaverse --test generated_avatar_catalogs - cargo test -p libremetaverse --lib linden_skeleton - cargo test -p libremetaverse --lib joint_base_support_category - cargo test -p libremetaverse --lib joint_get_aliases_list - - name: Build and audit clean Rust-only release artifacts - run: | - cargo build -p libremetaverse-codegen --release --locked -j 1 - python3 tools/check_codegen_gate.py --release-dir target/release - - name: Lint native generator - run: | - cargo clippy -p libremetaverse-codegen --all-targets -- -D warnings - cargo clippy -p libremetaverse --lib --no-deps -- -D warnings - - name: Prove normal consumer build does not execute generation - run: cargo build -p libremetaverse --locked diff --git a/.gitea/workflows/concurrency-audit.yml b/.gitea/workflows/concurrency-audit.yml deleted file mode 100644 index 1d0d793..0000000 --- a/.gitea/workflows/concurrency-audit.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Concurrency and resource soak audit - -on: - push: - paths: - - ".gitea/workflows/concurrency-audit.yml" - - "ci/concurrency-thresholds.json" - - "tools/concurrency-audit/**" - - "docs/concurrency-hardening.md" - - "crates/libremetaverse/src/client_core.rs" - - "crates/libremetaverse/src/download_manager.rs" - - "crates/libremetaverse/src/appearance_manager.rs" - - "crates/libremetaverse/src/asset_manager.rs" - - "crates/libremetaverse/src/inventory_manager.rs" - - "crates/libremetaverse-voice-webrtc/**" - - "Cargo.toml" - - "Cargo.lock" - pull_request: - paths: - - ".gitea/workflows/concurrency-audit.yml" - - "ci/concurrency-thresholds.json" - - "tools/concurrency-audit/**" - - "docs/concurrency-hardening.md" - - "crates/libremetaverse/src/client_core.rs" - - "crates/libremetaverse/src/download_manager.rs" - - "crates/libremetaverse/src/appearance_manager.rs" - - "crates/libremetaverse/src/asset_manager.rs" - - "crates/libremetaverse/src/inventory_manager.rs" - - "crates/libremetaverse-voice-webrtc/**" - - "Cargo.toml" - - "Cargo.lock" - workflow_dispatch: - -env: - CARGO_BUILD_JOBS: 1 - CARGO_INCREMENTAL: 0 - CARGO_PROFILE_DEV_DEBUG: 0 - CARGO_PROFILE_TEST_DEBUG: 0 - -jobs: - soak: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - name: Install native voice prerequisite - run: | - sudo apt-get update - sudo apt-get install --yes libopus-dev pkg-config - - name: Check formatting and focused race regressions - run: | - cargo fmt --all -- --check - cargo test --locked -p libremetaverse --lib shutdown_does_not_hold_service_registry_lock_across_callbacks -j 1 - cargo test --locked -p libremetaverse --lib event_registry_isolates_panics_and_releases_subscriptions -j 1 - cargo test --locked -p libremetaverse --lib dispatch_isolates_panics_and_does_not_retain_dropped_handlers -j 1 - cargo test --locked -p libremetaverse --test caps_http download_queue_applies_backpressure_and_cancellation_drains_every_job -j 1 - cargo test --locked -p libremetaverse-voice-webrtc concurrent_provisioning_admits_one_session_without_leaking_the_loser -j 1 - - name: Run bounded native soak - run: | - mkdir -p artifacts - cargo run --locked -p metacrate-concurrency-audit -j 1 -- \ - --cycles 16 --evidence artifacts/concurrency-audit.json - - name: Upload sanitized resource evidence - if: always() - uses: actions/upload-artifact@v3 - with: - name: concurrency-audit-evidence - path: artifacts/concurrency-audit.json - if-no-files-found: error diff --git a/.gitea/workflows/documentation.yml b/.gitea/workflows/documentation.yml deleted file mode 100644 index 40ad4a6..0000000 --- a/.gitea/workflows/documentation.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Documentation - -on: - push: - paths: - - ".gitea/workflows/documentation.yml" - - "Cargo.lock" - - "Cargo.toml" - - "README.md" - - "api/**" - - "crates/**" - - "docs/**" - - "programs/**" - - "tools/ci-matrix/**" - - "tools/generate_api_shims.py" - - "tools/install_openjpeg_2_5_4.sh" - pull_request: - paths: - - ".gitea/workflows/documentation.yml" - - "Cargo.lock" - - "Cargo.toml" - - "README.md" - - "api/**" - - "crates/**" - - "docs/**" - - "programs/**" - - "tools/ci-matrix/**" - - "tools/generate_api_shims.py" - - "tools/install_openjpeg_2_5_4.sh" - workflow_dispatch: - -jobs: - documentation: - runs-on: ubuntu-latest - timeout-minutes: 45 - env: - OPENJPEG_PREFIX: /tmp/metacrate-openjpeg-${{ github.run_id }} - PKG_CONFIG_PATH: /tmp/metacrate-openjpeg-${{ github.run_id }}/lib/pkgconfig - LD_LIBRARY_PATH: /tmp/metacrate-openjpeg-${{ github.run_id }}/lib - FORCE_SKIA_BINARIES_DOWNLOAD: "1" - CARGO_BUILD_JOBS: 1 - CARGO_INCREMENTAL: 0 - steps: - - uses: actions/checkout@v4 - - - name: Install native documentation prerequisites - run: | - sudo apt-get update - sudo apt-get install --yes build-essential clang cmake curl ninja-build \ - pkg-config python3 libfontconfig1-dev libfreetype6-dev libopus-dev - tools/install_openjpeg_2_5_4.sh "$OPENJPEG_PREFIX" - - - name: Install Rust 1.97 toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 1.97.0 - components: rustfmt - - - name: Check formatting - run: cargo fmt --all -- --check - - - name: Verify documentation coverage report - run: | - cargo run --locked -p metacrate-ci-matrix -- documentation-report - git diff --exit-code -- api/DOCUMENTATION-COVERAGE.md - cargo run --locked -p metacrate-ci-matrix -- documentation-audit --evidence /tmp/metacrate-documentation-audit.json - cmp /tmp/metacrate-documentation-audit.json ci/evidence/documentation-audit.json - - - name: Build warning-free workspace documentation - env: - RUSTDOCFLAGS: "-D warnings" - run: cargo doc --workspace --no-deps --locked -j 1 - - - name: Compile guide snippets and examples - run: | - cargo test --locked -p libremetaverse --doc -j 1 - cargo test --locked -p libremetaverse --examples -j 1 - cargo run --locked -p libremetaverse --example offline_client - cargo run --locked -p libremetaverse --example cancellation - - - name: Verify documented program quick starts - run: cargo test --locked -p libremetaverse-programs --tests -j 1 - - - name: Upload documentation evidence - uses: actions/upload-artifact@v3 - with: - name: documentation-evidence - path: ci/evidence/documentation-audit.json - if-no-files-found: error diff --git a/.gitea/workflows/imaging-meshing.yml b/.gitea/workflows/imaging-meshing.yml deleted file mode 100644 index fef5e09..0000000 --- a/.gitea/workflows/imaging-meshing.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Imaging and meshing gate - -on: - push: - paths: - - ".gitea/workflows/imaging-meshing.yml" - - "Cargo.toml" - - "Cargo.lock" - - "crates/libremetaverse/Cargo.toml" - - "crates/libremetaverse/src/generated.rs" - - "crates/libremetaverse/src/skeleton.rs" - - "crates/libremetaverse/src/*_catalog.rs" - - "crates/libremetaverse-types/**" - - "crates/libremetaverse-imaging/**" - - "crates/libremetaverse-imaging-skia/**" - - "crates/libremetaverse-prim-mesher/**" - - "crates/libremetaverse-rendering-mesh-foundry/**" - - "tests/compat/Cargo.toml" - - "tests/compat/build.rs" - - "tests/compat/src/**" - - "tests/compat/tests/imaging_meshing_semantics.rs" - - "tests/upstream-tests.json" - - "tools/check_milestone_06.py" - - "tools/install_openjpeg_2_5_4.sh" - pull_request: - paths: - - ".gitea/workflows/imaging-meshing.yml" - - "Cargo.toml" - - "Cargo.lock" - - "crates/libremetaverse/Cargo.toml" - - "crates/libremetaverse/src/generated.rs" - - "crates/libremetaverse/src/skeleton.rs" - - "crates/libremetaverse/src/*_catalog.rs" - - "crates/libremetaverse-types/**" - - "crates/libremetaverse-imaging/**" - - "crates/libremetaverse-imaging-skia/**" - - "crates/libremetaverse-prim-mesher/**" - - "crates/libremetaverse-rendering-mesh-foundry/**" - - "tests/compat/Cargo.toml" - - "tests/compat/build.rs" - - "tests/compat/src/**" - - "tests/compat/tests/imaging_meshing_semantics.rs" - - "tests/upstream-tests.json" - - "tools/check_milestone_06.py" - - "tools/install_openjpeg_2_5_4.sh" - workflow_dispatch: - -jobs: - native: - runs-on: ubuntu-latest - env: - RUSTDOCFLAGS: -D warnings - OPENJPEG_PREFIX: /tmp/metacrate-openjpeg-${{ github.run_id }} - PKG_CONFIG_PATH: /tmp/metacrate-openjpeg-${{ github.run_id }}/lib/pkgconfig - LD_LIBRARY_PATH: /tmp/metacrate-openjpeg-${{ github.run_id }}/lib - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy,rustfmt - - name: Install audited OpenJPEG 2.5.4 - run: | - sudo apt-get update - sudo apt-get install --yes build-essential cmake pkg-config - tools/install_openjpeg_2_5_4.sh "$OPENJPEG_PREFIX" - - name: Audit milestone ownership and feature isolation - run: python tools/check_milestone_06.py - - name: Test native imaging and meshing crates - run: | - cargo test -p libremetaverse-imaging - cargo test -p libremetaverse-imaging-skia --no-default-features - cargo test -p libremetaverse-prim-mesher - - name: Test reviewed milestone parity cases - run: | - cargo test -p libremetaverse-compat-tests --test imaging_meshing_semantics reviewed_managedimagetests_ - cargo test -p libremetaverse-compat-tests --test imaging_meshing_semantics reviewed_coordtests_ - cargo test -p libremetaverse-compat-tests --test imaging_meshing_semantics reviewed_quattests_ - cargo test -p libremetaverse-compat-tests --test imaging_meshing_semantics reviewed_primmeshtests_ - - name: Compile performance baselines - run: | - cargo bench -p libremetaverse-imaging --bench image_pipeline --no-run -j 1 - cargo bench -p libremetaverse-prim-mesher --bench meshing --no-run -j 1 - - name: Lint and document milestone crates - run: | - cargo clippy -p libremetaverse-imaging -p libremetaverse-imaging-skia -p libremetaverse-prim-mesher --all-targets -- -D warnings - cargo doc -p libremetaverse-imaging -p libremetaverse-imaging-skia -p libremetaverse-prim-mesher --no-deps - - name: Verify formatting - run: cargo fmt --all -- --check diff --git a/.gitea/workflows/jpeg2000.yml b/.gitea/workflows/jpeg2000.yml deleted file mode 100644 index be2aa00..0000000 --- a/.gitea/workflows/jpeg2000.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: JPEG 2000 feature - -on: - push: - paths: - - ".gitea/workflows/jpeg2000.yml" - - "Cargo.toml" - - "Cargo.lock" - - "crates/libremetaverse/Cargo.toml" - - "crates/libremetaverse-types/**" - - "crates/libremetaverse-imaging/**" - - "crates/libremetaverse-openjpeg/**" - - "tools/install_openjpeg_2_5_4.sh" - pull_request: - paths: - - ".gitea/workflows/jpeg2000.yml" - - "Cargo.toml" - - "Cargo.lock" - - "crates/libremetaverse/Cargo.toml" - - "crates/libremetaverse-types/**" - - "crates/libremetaverse-imaging/**" - - "crates/libremetaverse-openjpeg/**" - - "tools/install_openjpeg_2_5_4.sh" - workflow_dispatch: - -jobs: - linux: - runs-on: ubuntu-latest - env: - # Ubuntu also ships an older libopenjp2.so.7. Ensure both pkg-config and - # the runtime loader select this run's isolated, audited 2.5.4 install. - OPENJPEG_PREFIX: /tmp/metacrate-openjpeg-${{ github.run_id }} - PKG_CONFIG_PATH: /tmp/metacrate-openjpeg-${{ github.run_id }}/lib/pkgconfig - LD_LIBRARY_PATH: /tmp/metacrate-openjpeg-${{ github.run_id }}/lib - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - name: Install OpenJPEG 2.5.4 - run: | - sudo apt-get update - sudo apt-get install --yes build-essential cmake pkg-config - tools/install_openjpeg_2_5_4.sh "$OPENJPEG_PREFIX" - - name: Build and test optional feature - run: | - cargo test -p libremetaverse-imaging --features jpeg2000 - cargo check -p libremetaverse --features jpeg2000 diff --git a/.gitea/workflows/performance.yml b/.gitea/workflows/performance.yml deleted file mode 100644 index c52e23a..0000000 --- a/.gitea/workflows/performance.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: performance evidence - -on: - push: - paths: - - "benchmarks/**" - - "tools/performance/**" - - "crates/libremetaverse/src/inventory.rs" - - "crates/libremetaverse/src/targa.rs" - - ".gitea/workflows/performance.yml" - - "Cargo.toml" - - "Cargo.lock" - pull_request: - paths: - - "benchmarks/**" - - "tools/performance/**" - - "crates/libremetaverse/src/inventory.rs" - - "crates/libremetaverse/src/targa.rs" - - ".gitea/workflows/performance.yml" - - "Cargo.toml" - - "Cargo.lock" - -jobs: - audit: - runs-on: ubuntu-latest - env: - CARGO_PROFILE_BENCHMARK_OPT_LEVEL: 0 - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 1.97.1 - components: rustfmt, clippy - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 8.0.x - 10.0.x - - name: Check out pinned C# reference - run: | - git clone --filter=blob:none https://github.com/cinderblocks/libremetaverse.git "$RUNNER_TEMP/libremetaverse" - git -C "$RUNNER_TEMP/libremetaverse" checkout 2aa70bb68513b39795da5d13c88f31b86e85a3ba - - name: Compile separate C# reference runner - run: >- - dotnet build benchmarks/csharp-reference/MetaCrate.ReferenceBenchmarks.csproj - -c Release -p:ReferenceRoot="$RUNNER_TEMP/libremetaverse" - - name: Verify harness - run: cargo test -p metacrate-performance --profile benchmark --locked -j1 - - name: Audit committed evidence - run: >- - cargo run -p metacrate-performance --profile benchmark --locked -j1 -- - audit --fixture-root benchmarks/fixtures - --rust benchmarks/results/rust-linux-x86_64.json - --reference benchmarks/results/csharp-linux-x86_64.json - --comparison benchmarks/results/comparison.json diff --git a/.gitea/workflows/release-candidate.yml b/.gitea/workflows/release-candidate.yml deleted file mode 100644 index ac2949e..0000000 --- a/.gitea/workflows/release-candidate.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: First release candidate - -on: - push: - paths: - - ".gitea/workflows/release-candidate.yml" - - "ci/release-candidate-policy.json" - - "ci/evidence/**" - - "docs/release-candidate.md" - - "CHANGELOG.md" - - "release/**" - - "api/**" - - "programs/**" - - "tests/**" - - "tools/**" - - "crates/**" - - "Cargo.toml" - - "Cargo.lock" - pull_request: - paths: - - ".gitea/workflows/release-candidate.yml" - - "ci/release-candidate-policy.json" - - "ci/evidence/**" - - "docs/release-candidate.md" - - "CHANGELOG.md" - - "release/**" - - "api/**" - - "programs/**" - - "tests/**" - - "tools/**" - - "crates/**" - - "Cargo.toml" - - "Cargo.lock" - 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 - OPENJPEG_PREFIX: /tmp/metacrate-openjpeg-release-candidate - PKG_CONFIG_PATH: /tmp/metacrate-openjpeg-release-candidate/lib/pkgconfig - LD_LIBRARY_PATH: /tmp/metacrate-openjpeg-release-candidate/lib - RUSTDOCFLAGS: -D warnings - -jobs: - non-fuzz-release-gate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: 1.97.1 - components: rustfmt, clippy - - name: Install every Linux release prerequisite - run: | - sudo apt-get update - sudo apt-get install --yes build-essential clang cmake curl ninja-build pkg-config python3 libfontconfig1-dev libfreetype6-dev libopus-dev libasound2-dev - tools/install_openjpeg_2_5_4.sh "$OPENJPEG_PREFIX" - - name: Record source identity without a shipped helper - run: echo "METACRATE_RUST_COMMIT=$(git rev-parse --verify HEAD)" >>"$GITHUB_ENV" - - name: Verify generated surfaces, mappings, parity, and milestone inventories - run: | - python3 tools/generate_rust_mapping.py --check - python3 tools/generate_api_shims.py --check - python3 tools/check_api_coverage.py - python3 tools/check_test_parity.py - python3 tools/generate_lsl_tables.py --check - python3 tools/check_milestone_09.py - python3 tools/check_milestone_10.py - - name: Format, compile, test, lint, and document the complete workspace - run: | - cargo fmt --all -- --check - cargo check --workspace --all-targets --all-features --locked -j 1 - cargo test --workspace --all-features --locked -j 1 - cargo clippy --workspace --all-targets --all-features --locked -j 1 -- -D warnings - cargo doc --workspace --all-features --no-deps --locked -j 1 - - name: Compile downstream and SemVer consumers - run: | - cargo check --locked --manifest-path tests/api-compile/Cargo.toml - cargo run --locked --manifest-path tests/semver-port/Cargo.toml - - name: Exercise all offline program and fake-grid behavior - run: | - cargo test --locked -p libremetaverse-programs --tests -j 1 - cargo run --locked -p libremetaverse-programs --bin live-grid-smoke -- --audit-only - cargo run --locked -p libremetaverse-programs --bin live-grid-smoke -- --fake --evidence artifacts/release-candidate/fake-grid-smoke.jsonl - - name: Revalidate all checked-in audits and checksums - run: | - cargo run --locked -p metacrate-ci-matrix -- audit - cargo run --locked -p metacrate-ci-matrix -- documentation-audit --evidence artifacts/release-candidate/documentation.json - cargo run --locked -p metacrate-ci-matrix -- dependency-audit --evidence artifacts/release-candidate/dependency.json - cargo run --locked -p metacrate-ci-matrix -- provenance-audit --evidence artifacts/release-candidate/provenance.json - sha256sum --check release/SHA256SUMS - - name: Enforce the aggregate non-fuzz release-candidate decision - run: cargo run --locked -p metacrate-ci-matrix -- release-candidate-audit --evidence artifacts/release-candidate/status.json - - name: Upload release-candidate evidence, including failures - if: always() - uses: actions/upload-artifact@v3 - with: - name: metacrate-0.0.1-release-candidate - path: artifacts/release-candidate/ - if-no-files-found: error diff --git a/.gitea/workflows/release-matrix.yml b/.gitea/workflows/release-matrix.yml deleted file mode 100644 index 054a249..0000000 --- a/.gitea/workflows/release-matrix.yml +++ /dev/null @@ -1,122 +0,0 @@ -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: true - - 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 clang cmake curl ninja-build pkg-config python3 libfontconfig1-dev libfreetype6-dev 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@v3 - with: - name: release-matrix-${{ matrix.profile }} - path: artifacts/ci-matrix/${{ matrix.profile }}.json - if-no-files-found: error diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..95ce1e6 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,61 @@ +name: Non-routine release validation + +on: + workflow_dispatch: + schedule: + - cron: "17 3 * * 0" + push: + tags: + - "v*" + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: true + +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: 120 + env: + CARGO_BUILD_JOBS: "2" + CARGO_INCREMENTAL: "0" + RUSTDOCFLAGS: "-D warnings" + OPENJPEG_PREFIX: ${{ runner.tool_cache }}/metacrate/native/openjpeg-2.5.4-${{ runner.arch }} + PKG_CONFIG_PATH: ${{ runner.tool_cache }}/metacrate/native/openjpeg-2.5.4-${{ runner.arch }}/lib/pkgconfig + LD_LIBRARY_PATH: ${{ runner.tool_cache }}/metacrate/native/openjpeg-2.5.4-${{ runner.arch }}/lib + METACRATE_RUST_COMMIT: ${{ github.sha }} + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + ${{ runner.tool_cache }}/metacrate/native + target/release-gate + key: release-rust-1.97.1-${{ runner.arch }}-${{ hashFiles('.gitea/workflows/release.yml', 'Cargo.lock', 'ci/ci-coverage.json', 'ci/release-matrix.json', 'ci/artifact-policy.json', 'tools/install_openjpeg_2_5_4.sh') }} + - name: Install the release prerequisite set once + run: | + sudo apt-get update + sudo apt-get install --yes build-essential clang cmake curl git mingw-w64 ninja-build pkg-config python3 libfontconfig1-dev libfreetype6-dev libopus-dev libasound2-dev + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.97.1 + components: clippy,rustfmt + targets: x86_64-pc-windows-gnu,x86_64-apple-darwin + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.96.0 + - name: Validate the native release dependency + run: tools/install_openjpeg_2_5_4.sh "$OPENJPEG_PREFIX" + - name: Run the Rust-owned non-routine release graph + run: cargo +1.97.1 run --locked -p metacrate-ci-matrix -- release-gate --evidence artifacts/release/release-gate.json + - name: Upload release evidence and packages + if: always() + uses: actions/upload-artifact@v3 + with: + name: non-routine-release-evidence + path: | + artifacts/release/ + target/release-gate/package/*.crate + if-no-files-found: error diff --git a/.gitea/workflows/rust-workspace.yml b/.gitea/workflows/rust-workspace.yml deleted file mode 100644 index 579e56a..0000000 --- a/.gitea/workflows/rust-workspace.yml +++ /dev/null @@ -1,119 +0,0 @@ -name: Native Rust workspace compile - -on: - push: - paths: - - ".gitea/workflows/rust-workspace.yml" - - "tools/install_openjpeg_2_5_4.sh" - - "tools/check_milestone_09.py" - - "tools/test_milestone_09.py" - - "tools/check_milestone_10_issue_76.py" - - "tools/check_milestone_10_issue_77.py" - - "tools/check_milestone_10_issue_78.py" - - "tools/check_milestone_10_issue_79.py" - - "tools/check_milestone_10_issue_80.py" - - "tools/check_milestone_10_issue_81.py" - - "tools/check_milestone_10_issue_82.py" - - "tools/check_milestone_10_issue_83.py" - - "tools/check_milestone_10.py" - - "tools/test_milestone_10.py" - - "tools/generate_lsl_tables.py" - - "codegen/inputs/lsl_tools_grammar.json" - - "docs/extension-milestone-gate.md" - - "docs/live-grid-smoke.md" - - "programs/README.md" - - "api/SHIM-COVERAGE.md" - - "**/*.rs" - - "**/Cargo.toml" - - "Cargo.lock" - pull_request: - paths: - - ".gitea/workflows/rust-workspace.yml" - - "tools/install_openjpeg_2_5_4.sh" - - "tools/check_milestone_09.py" - - "tools/test_milestone_09.py" - - "tools/check_milestone_10_issue_76.py" - - "tools/check_milestone_10_issue_77.py" - - "tools/check_milestone_10_issue_78.py" - - "tools/check_milestone_10_issue_79.py" - - "tools/check_milestone_10_issue_80.py" - - "tools/check_milestone_10_issue_81.py" - - "tools/check_milestone_10_issue_82.py" - - "tools/check_milestone_10_issue_83.py" - - "tools/check_milestone_10.py" - - "tools/test_milestone_10.py" - - "tools/generate_lsl_tables.py" - - "codegen/inputs/lsl_tools_grammar.json" - - "docs/extension-milestone-gate.md" - - "docs/live-grid-smoke.md" - - "programs/README.md" - - "api/SHIM-COVERAGE.md" - - "**/*.rs" - - "**/Cargo.toml" - - "Cargo.lock" - workflow_dispatch: - -jobs: - compile: - runs-on: ubuntu-latest - env: - OPENJPEG_PREFIX: /tmp/metacrate-openjpeg-${{ github.run_id }} - PKG_CONFIG_PATH: /tmp/metacrate-openjpeg-${{ github.run_id }}/lib/pkgconfig - LD_LIBRARY_PATH: /tmp/metacrate-openjpeg-${{ github.run_id }}/lib - CARGO_BUILD_JOBS: 1 - CARGO_INCREMENTAL: 0 - CARGO_PROFILE_DEV_DEBUG: 0 - CARGO_PROFILE_TEST_DEBUG: 0 - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - name: Install audited OpenJPEG 2.5.4 - 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" - - name: Check formatting - run: cargo fmt --all -- --check - - name: Audit native implementation boundaries - run: | - python3 tools/check_milestone_09_issue_59.py - python3 tools/check_milestone_09_issue_60.py - python3 tools/check_milestone_09_issue_61.py - python3 tools/check_milestone_09_issue_62.py - python3 tools/check_milestone_09_issue_63.py - python3 tools/check_milestone_09_issue_64.py - python3 tools/check_milestone_09_issue_65.py - python3 tools/check_milestone_09_issue_66.py - python3 tools/check_milestone_09.py - python3 tools/check_milestone_10_issue_76.py - python3 tools/check_milestone_10_issue_77.py - python3 tools/check_milestone_10_issue_78.py - python3 tools/check_milestone_10_issue_79.py - python3 tools/check_milestone_10_issue_80.py - python3 tools/check_milestone_10_issue_81.py - python3 tools/check_milestone_10_issue_82.py - python3 tools/check_milestone_10_issue_83.py - python3 tools/generate_lsl_tables.py --check - python3 tools/check_milestone_10.py - - name: Test the complete native world milestone - run: python3 tools/test_milestone_09.py - - name: Test the complete native extension milestone - run: python3 tools/test_milestone_10.py - - name: Compile every workspace target with bounded memory - run: cargo check --workspace --all-targets --locked -j 1 - - name: Audit and fake-smoke every native program target - run: | - cargo run -p libremetaverse-programs --bin live-grid-smoke --locked -- --audit-only - cargo test -p libremetaverse-programs --test live_grid_smoke_cli --locked -j 1 - - name: Check optional cross-platform real audio backend - run: | - cargo check -p libremetaverse-voice-webrtc --all-targets --features real-audio --locked -j 1 - cargo check -p libremetaverse-programs --bin webrtc-test --features real-audio --locked -j 1 - - name: Lint every native extension target with bounded memory - run: cargo clippy -p libremetaverse-rendering-simple -p libremetaverse-rendering-mesh-foundry -p libremetaverse-rlv -p libremetaverse-lsl-tools --all-targets --locked -j 1 -- -D warnings - - name: Document every native extension crate - env: - RUSTDOCFLAGS: -D warnings - run: cargo doc -p libremetaverse-rendering-simple -p libremetaverse-rendering-mesh-foundry -p libremetaverse-rlv -p libremetaverse-lsl-tools --no-deps --locked -j 1 diff --git a/.gitea/workflows/skia.yml b/.gitea/workflows/skia.yml deleted file mode 100644 index 883e8d0..0000000 --- a/.gitea/workflows/skia.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Skia feature - -on: - push: - paths: - - ".gitea/workflows/skia.yml" - - "Cargo.toml" - - "Cargo.lock" - - "crates/libremetaverse-types/**" - - "crates/libremetaverse-imaging/**" - - "crates/libremetaverse-imaging-skia/**" - pull_request: - paths: - - ".gitea/workflows/skia.yml" - - "Cargo.toml" - - "Cargo.lock" - - "crates/libremetaverse-types/**" - - "crates/libremetaverse-imaging/**" - - "crates/libremetaverse-imaging-skia/**" - workflow_dispatch: - -jobs: - linux: - runs-on: ubuntu-latest - env: - FORCE_SKIA_BINARIES_DOWNLOAD: "1" - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - name: Install native link prerequisites - run: | - sudo apt-get update - sudo apt-get install --yes build-essential clang curl ninja-build pkg-config python3 libfontconfig1-dev libfreetype6-dev - - name: Test default and Skia feature builds - run: | - cargo test -p libremetaverse-imaging-skia --no-default-features - cargo test -p libremetaverse-imaging-skia --features skia - cargo package -p libremetaverse-imaging-skia --list diff --git a/.gitea/workflows/supply-chain.yml b/.gitea/workflows/supply-chain.yml deleted file mode 100644 index e02071f..0000000 --- a/.gitea/workflows/supply-chain.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Dependency and supply-chain audit - -on: - push: - pull_request: - workflow_dispatch: - -env: - CARGO_BUILD_JOBS: 1 - CARGO_INCREMENTAL: 0 - CARGO_PROFILE_DEV_DEBUG: 0 - -jobs: - audit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Cache immutable Cargo downloads and advisory database - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - ~/.cargo/advisory-dbs - key: supply-chain-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'deny.toml') }} - - uses: dtolnay/rust-toolchain@stable - - name: Fetch every immutable locked package archive - run: cargo fetch --locked - - name: Install pinned audit tools - run: | - cargo install --locked cargo-deny --version 0.20.2 - cargo install --locked cargo-machete --version 0.9.2 - - name: Record and validate the reviewed graph - run: | - mkdir -p artifacts - cargo tree --locked --workspace --all-features --target all --duplicates > artifacts/dependency-duplicates.txt - cargo run --locked -p metacrate-ci-matrix -- dependency-audit --evidence artifacts/dependency-audit.json - cargo run --locked -p metacrate-ci-matrix -- provenance-audit --evidence artifacts/provenance-audit.json - - name: Reject advisories, licenses, duplicates, and sources outside policy - run: cargo deny check advisories licenses bans sources --hide-inclusion-graph - - name: Reject unused direct dependencies - run: cargo machete --with-metadata - - name: Upload dependency evidence - if: always() - uses: actions/upload-artifact@v3 - with: - name: supply-chain-evidence - path: artifacts/ - if-no-files-found: error diff --git a/.gitignore b/.gitignore index 769b0c1..b0d5c6c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /crates/libremetaverse-openjpeg/Cargo.lock /tests/api-compile/target/ /tests/semver-port/target/ +/artifacts/ .env .env.* !.env.example diff --git a/api/DOCUMENTATION-COVERAGE.md b/api/DOCUMENTATION-COVERAGE.md index 27f5905..9f8ca11 100644 --- a/api/DOCUMENTATION-COVERAGE.md +++ b/api/DOCUMENTATION-COVERAGE.md @@ -9,6 +9,6 @@ Generated by `metacrate-ci-matrix documentation-report`; do not edit by hand. | Mapped public members | 30789 | 30789 | 100% | | Compiled Rust guide snippets | 5 | 4 minimum | pass | | Linked native programs | 9 | 9 | 100% | -| Checked local Markdown links | 86 | 86 | 100% | +| Checked local Markdown links | 84 | 84 | 100% | Every mapped item is tied to its exact C# documentation ID and to the ownership, asyncness, error, overload, mapping-kind, and Rust-signature decisions in [`RUST-MAPPING.tsv`](RUST-MAPPING.tsv). Public types are tied to the corresponding type mapping. The upstream source is pinned to [`2aa70bb68513b39795da5d13c88f31b86e85a3ba`](https://github.com/cinderblocks/libremetaverse/tree/2aa70bb68513b39795da5d13c88f31b86e85a3ba). diff --git a/ci/ci-coverage.json b/ci/ci-coverage.json new file mode 100644 index 0000000..f1b3469 --- /dev/null +++ b/ci/ci-coverage.json @@ -0,0 +1,117 @@ +{ + "schema": 1, + "required_workflow": ".gitea/workflows/ci.yml", + "release_workflow": ".gitea/workflows/release.yml", + "hard_timeout_minutes": 15, + "internal_target_seconds": 720, + "required_checks": [ + "format", + "deterministic-generation", + "api-semver", + "workspace-tests", + "compatibility-parity", + "minimal-features", + "jpeg2000", + "skia-binary", + "program-offline-smoke", + "clippy", + "documentation", + "dependency-policy", + "provenance-policy", + "performance-evidence", + "concurrency-regressions" + ], + "legacy_workflows": [ + { + "workflow": "api-surface.yml", + "responsibility": "Generated API mappings, SemVer baseline and audit, downstream consumer compilation, and focused lint", + "destination": "required", + "checks": ["deterministic-generation", "api-semver", "clippy"], + "rationale": "Public API drift must block every code-ready change." + }, + { + "workflow": "artifact-audit.yml", + "responsibility": "Source packages, release binaries, native linkage, and artifact/provenance inspection", + "destination": "release", + "checks": ["release-packaging", "native-artifact-audit"], + "rationale": "Full packaging is expensive; routine provenance policy and package metadata validation remain required." + }, + { + "workflow": "codegen.yml", + "responsibility": "Deterministic generation, generated wire/catalog tests, API fixture, and clean generator artifact", + "destination": "required", + "checks": ["format", "deterministic-generation", "workspace-tests", "compatibility-parity"], + "rationale": "Generated-source and translated compatibility drift must fail before compilation proceeds." + }, + { + "workflow": "concurrency-audit.yml", + "responsibility": "Focused race regressions and bounded resource soak", + "destination": "split", + "checks": ["concurrency-regressions", "long-resource-soak"], + "rationale": "Focused regressions remain required; the 16-cycle resource soak runs in the non-routine release gate." + }, + { + "workflow": "documentation.yml", + "responsibility": "Documentation audit, warning-free docs, doctests, examples, and documented program tests", + "destination": "required", + "checks": ["documentation", "program-offline-smoke"], + "rationale": "Documentation and documented offline behavior are part of the public release surface." + }, + { + "workflow": "imaging-meshing.yml", + "responsibility": "Imaging and meshing tests, parity cases, benchmark compilation, lint, and docs", + "destination": "split", + "checks": ["workspace-tests", "compatibility-parity", "clippy", "documentation", "benchmark-compilation"], + "rationale": "Tests, parity, lint, and docs remain required; redundant benchmark compilation moves to the release gate." + }, + { + "workflow": "jpeg2000.yml", + "responsibility": "Optional OpenJPEG/JPEG 2000 feature build and tests", + "destination": "required", + "checks": ["jpeg2000"], + "rationale": "The optional native codec is tested independently with an exact cached OpenJPEG 2.5.4 identity." + }, + { + "workflow": "performance.yml", + "responsibility": "Pinned C# comparison harness and committed performance evidence audit", + "destination": "split", + "checks": ["performance-evidence", "performance-regeneration"], + "rationale": "Committed evidence validation remains required; C# checkout and evidence regeneration are manual release work." + }, + { + "workflow": "release-candidate.yml", + "responsibility": "Aggregate release-readiness gate and evidence", + "destination": "release", + "checks": ["release-candidate-audit"], + "rationale": "The aggregate release decision is non-routine and no longer recompiles the required gate." + }, + { + "workflow": "release-matrix.yml", + "responsibility": "MSRV, default, minimal, features, release profiles, and portable cross-target checks", + "destination": "split", + "checks": ["minimal-features", "jpeg2000", "skia-binary", "msrv", "portable-cross-targets", "release-profile"], + "rationale": "Fast feature isolation remains required; clean MSRV, cross-target, and release-profile builds run non-routinely." + }, + { + "workflow": "rust-workspace.yml", + "responsibility": "Milestone checks, workspace compilation, program smoke, audio feature, extension lint, and docs", + "destination": "required", + "checks": ["workspace-tests", "compatibility-parity", "program-offline-smoke", "clippy", "documentation"], + "rationale": "One all-feature test graph replaces overlapping compile and test invocations." + }, + { + "workflow": "skia.yml", + "responsibility": "Default and Skia feature tests plus package listing", + "destination": "required", + "checks": ["skia-binary", "workspace-tests"], + "rationale": "The required gate proves the exact ARM64 GNU archive is installed and forbids source compilation." + }, + { + "workflow": "supply-chain.yml", + "responsibility": "Dependency graph, advisory, license, source, provenance, duplicate, and unused-dependency audits", + "destination": "required", + "checks": ["dependency-policy", "provenance-policy"], + "rationale": "Pinned prebuilt audit tools and cached advisory data keep policy enforcement in the routine gate." + } + ] +} diff --git a/ci/evidence/documentation-audit.json b/ci/evidence/documentation-audit.json index f1fad26..0c17418 100644 --- a/ci/evidence/documentation-audit.json +++ b/ci/evidence/documentation-audit.json @@ -8,7 +8,7 @@ "documented_members": 30789, "tested_rust_snippets": 5, "linked_programs": 9, - "checked_local_links": 86, + "checked_local_links": 84, "required_guide_sections": 11, "status": "ok" } diff --git a/ci/evidence/provenance-audit.json b/ci/evidence/provenance-audit.json index f79f0ef..bf81c0c 100644 --- a/ci/evidence/provenance-audit.json +++ b/ci/evidence/provenance-audit.json @@ -1,18 +1,18 @@ { "schema": 1, - "recorded_unix_seconds": 1786546522, + "recorded_unix_seconds": 1786572918, "upstream_commit": "2aa70bb68513b39795da5d13c88f31b86e85a3ba", "material_count": 27, "generated_output_count": 8, "provenance_ledger_count": 3, - "source_file_count": 538, + "source_file_count": 531, "dependency_count": 389, - "dependency_notice_count": 284, + "dependency_notice_count": 283, "native_component_count": 6, - "dependency_manifest_sha256": "6482698a22201b04f3234363790edc0be0dba39f445ac939e3fc8d4edeb372e9", - "third_party_notices_sha256": "de5088841682813cc952af4d1dcc57d2521694e192888042d5585e344e570d16", + "dependency_manifest_sha256": "a4095621c7975f6b01c5b1653b375c571be15657f7fa824d6b5a2e46f3434dcc", + "third_party_notices_sha256": "da051a965d39715d10da5c17fdfdafd8059b83d2491837d223607e9c5e16a5fa", "native_notices_sha256": "b417ee7bf6b748cc86839e5354753c0f8c1f8309d13c0fea379c77f602e69fc0", - "distribution_manifest_sha256": "18afd63d96e675c826cf138293df0ae73be279f45c9efa258bbbb30a1c7577e7", + "distribution_manifest_sha256": "937d842df90f9dbda2895ec166b82aececbc0007f95a7052c6dc3867c7aeff57", "unknown_materials": 0, "unknown_bundled_assets": 0, "status": "ok" diff --git a/crates/libremetaverse-imaging-skia/Cargo.toml b/crates/libremetaverse-imaging-skia/Cargo.toml index b23a889..972c400 100644 --- a/crates/libremetaverse-imaging-skia/Cargo.toml +++ b/crates/libremetaverse-imaging-skia/Cargo.toml @@ -19,13 +19,13 @@ libremetaverse-types = { version = "0.0.1", path = "../libremetaverse-types" } # and without Vulkan on macOS. Matching those sets avoids an hours-long Skia # source build on every supported desktop target. [target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies] -skia-safe = { version = "0.99.0", default-features = false, features = ["binary-cache", "jpeg", "pdf", "svg", "textlayout", "vulkan", "webp"], optional = true } +skia-safe = { version = "0.99.0", default-features = false, features = ["binary-cache", "jpeg", "no-compile", "pdf", "svg", "textlayout", "vulkan", "webp"], optional = true } [target.'cfg(target_os = "macos")'.dependencies] -skia-safe = { version = "0.99.0", default-features = false, features = ["binary-cache", "jpeg", "pdf", "svg", "textlayout", "webp"], optional = true } +skia-safe = { version = "0.99.0", default-features = false, features = ["binary-cache", "jpeg", "no-compile", "pdf", "svg", "textlayout", "webp"], optional = true } [target.'cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))'.dependencies] -skia-safe = { version = "0.99.0", default-features = false, features = ["binary-cache", "jpeg", "pdf", "webp"], optional = true } +skia-safe = { version = "0.99.0", default-features = false, features = ["binary-cache", "jpeg", "no-compile", "pdf", "webp"], optional = true } [lints] workspace = true diff --git a/docs/ci-runtime-evidence.md b/docs/ci-runtime-evidence.md new file mode 100644 index 0000000..0612095 --- /dev/null +++ b/docs/ci-runtime-evidence.md @@ -0,0 +1,36 @@ +# Required CI runtime evidence + +This record is populated from the consolidated `CI / required` Gitea job. A +qualifying sample consists of one deliberate cold native/dependency cache run +and four normal warm-cache runs on `aarch64-unknown-linux-gnu`. Each uploaded +`required-gate.json` contains per-stage elapsed time, Rust host/target, separate +compiled-Cargo, dependency-download, native, and tool cache hit state, +available peak-memory evidence, and the Skia binary outcome. + +The compiled target key includes Rust 1.97.1, the ARM64 GNU target, every Cargo +manifest, `Cargo.lock`, required feature/policy configuration, and native +installer inputs. CI-driver-only changes reuse the newest generation through a +same-filesystem hard-link migration; Cargo still validates every fingerprint. +To invalidate compiled artifacts, change a keyed build input. A deliberate +`cold_cache` dispatch removes registry archives/index data, Git database data, +pinned audit tools, OpenJPEG, and Skia. It retains immutable extracted registry +sources and the compiled target so the cold dependency/native measurement does +not manufacture source-mtime rebuilds unrelated to cache correctness. + +The pre-consolidation Gitea baseline was 13 automatic workflows and 333 runs. +Successful maxima included 22.9 minutes for the native workspace, 32.0 minutes +for Skia, 66.6 minutes for artifact audit, and 86.0 minutes for the release +matrix. Native code generation had a 13.2-minute median and 18.5-minute maximum; +even this one concern could exceed the new complete-gate budget. + +| Run | Cache | Elapsed | Result | Evidence | +| --- | --- | ---: | --- | --- | +| pending first consolidated run | cold | pending | pending | pending | +| pending second consolidated run | warm | pending | pending | pending | +| pending third consolidated run | warm | pending | pending | pending | +| pending fourth consolidated run | warm | pending | pending | pending | +| pending fifth consolidated run | warm | pending | pending | pending | + +Branch protection must require only `CI / required` after all five rows are +green and at or below 15:00. Until then, this file deliberately records that +runtime parity and branch-protection migration are not yet proven. diff --git a/docs/dependency-policy.md b/docs/dependency-policy.md index ea8720c..3c57f8a 100644 --- a/docs/dependency-policy.md +++ b/docs/dependency-policy.md @@ -22,7 +22,7 @@ not raise it accidentally. ## Automated gates -The Ubuntu-only `supply-chain` Gitea workflow installs pinned versions of +The Ubuntu-only consolidated required Gitea gate installs pinned versions of `cargo-deny` and `cargo-machete`, then runs: ```sh diff --git a/docs/imaging-meshing.md b/docs/imaging-meshing.md index d1b75ef..2b3a0fe 100644 --- a/docs/imaging-meshing.md +++ b/docs/imaging-meshing.md @@ -23,9 +23,9 @@ and [`MeshFoundry` guide](../crates/libremetaverse-rendering-mesh-foundry/README OpenJPEG is a non-default workspace member so release tooling can package the adapter explicitly, and imaging reaches it only through the optional dependency. Skia is likewise an optional, target-specific dependency. The -dedicated Gitea workflows exercise both optional features on `ubuntu-latest`; -the default `imaging-meshing` workflow proves that no native codec is needed -for the owned Rust and translated compatibility tests. +consolidated required Gitea gate exercises both optional features on +`ubuntu-latest` while its minimal-feature check proves that no native codec is +needed for the owned Rust and translated compatibility tests. ## Bounds and rendering contract @@ -50,10 +50,10 @@ python3 tools/check_milestone_06.py The audit rejects owned Rust stubs, feature leakage, missing platform jobs, missing performance targets, or drift in the 48 reviewed ManagedImage, -Coord/Quat, and PrimMesh compatibility cases. The cross-platform workflow also -runs each reviewed group explicitly, native crate tests, workspace test -compilation, Clippy with warnings denied, rustdoc with warnings denied, and -benchmark compilation. +Coord/Quat, and PrimMesh compatibility cases. The required gate runs the native +crate and reviewed parity tests, Clippy with warnings denied, and rustdoc with +warnings denied. Benchmark compilation is retained in the non-routine release +gate. The focused performance baselines are executable Rust programs rather than wall-clock assertions, so slower shared runners do not weaken or destabilize diff --git a/docs/release-ci-matrix.md b/docs/release-ci-matrix.md index bca796d..7d3407b 100644 --- a/docs/release-ci-matrix.md +++ b/docs/release-ci-matrix.md @@ -1,90 +1,132 @@ -# Release platform and feature CI matrix +# Consolidated CI and release 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. +MetaCrate has one routine code-ready signal: the `required` job in +`.gitea/workflows/ci.yml`. It runs for every push and pull request on +`ubuntu-latest`, has a hard 15-minute timeout, and cancels an older run for the +same branch or pull request. `.gitea/workflows/release.yml` is the only other +workflow. It is manual, weekly, and tag-only; it never runs for an ordinary +source push or pull request. -## Platform boundary +The reviewed coverage map is `ci/ci-coverage.json`. It maps every responsibility +from the former 13 workflows to the required gate, the non-routine release +gate, or both, with a rationale. `metacrate-ci-matrix ci-audit` rejects missing +routes, additional workflow fan-out, non-Ubuntu jobs, Skia source-build +fallback, and changes to the 15-minute limit. -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. +## Required gate -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. +The Rust `required-gate` command owns the command graph and records the elapsed +time and result of every stage. YAML performs checkout, one package install, +one coherent persistent-cache inspection, pinned toolchain/tool setup, and exact native-cache +preparation before invoking it. Fast-failure order is: -## Feature isolation +1. formatting, deterministic generators, API mappings, parity catalogs, and + milestone ownership checks; +2. regenerated API, documentation, dependency, and provenance evidence; +3. exact Skia ARM64 GNU binary installation proof; +4. all-feature workspace library/integration tests, minimal and JPEG 2000 + feature isolation, compatibility tests, and concurrency regressions; +5. offline/fake program smoke and downstream API/SemVer consumers; +6. Clippy, warning-free documentation, dependency policy, unused dependency, + provenance, and committed performance-evidence audits. -`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 driver stops starting work after its 12-minute internal target. This leaves +three minutes for shutdown and artifact upload beneath the workflow's hard +limit. `CARGO_BUILD_JOBS=2` is the bounded ARM64 setting; it permits useful +parallelism without the repeated high-memory compiles caused by the former +13-job fan-out. One feature-unified product/compatibility test graph uses a +single Cargo job, with package-qualified core features so unrelated consumer +options are not activated. This is the sole memory-safety exception, while +every other gate command retains the two-job bound. -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. - -Dependency purpose, maintenance, license, advisory, source, and duplicate -review is the separate supply-chain gate documented in -[`dependency-policy.md`](dependency-policy.md). Changes to manifests or the -lockfile trigger both gates. - -Release-file provenance, Linden CC BY-SA separation, complete locked package -notices, native redistribution obligations, and the source/binary notice set -are enforced by the companion -[`release provenance audit`](release-provenance.md). Its distribution manifest -hashes the full source tree, so the supply-chain workflow runs for every source -change rather than only dependency changes. - -## Clean-build evidence - -Each profile uses `target/ci/` 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: +Run the structural audit locally on Linux, Windows, or macOS with: ```sh -cargo run --locked -p metacrate-ci-matrix -- audit +cargo run --locked -p metacrate-ci-matrix -- ci-audit ``` -Run one clean profile with: +The full required graph expects the CI native-cache environment. On an ARM64 +GNU Linux host with OpenJPEG installed by `tools/install_openjpeg_2_5_4.sh` and +the pinned Skia archive available, reproduce it with: ```sh +OPENJPEG_PREFIX=/path/to/openjpeg \ +METACRATE_EXPECTED_HOST=aarch64-unknown-linux-gnu \ +METACRATE_SKIA_ARCHIVE=/path/to/skia-binaries-a25a0fdb7d90429aa2d1-aarch64-unknown-linux-gnu-jpegd-jpege-pdf-svg-textlayout-vulkan-webpd-webpe.tar.gz \ cargo run --locked -p metacrate-ci-matrix -- \ - run linux-stable-minimal \ - --evidence /tmp/metacrate-linux-stable-minimal.json + required-gate --evidence artifacts/ci/local-required.json ``` -Before rerunning the same profile, use Cargo's scoped cleanup command: +The driver is shell-free and constructs child processes with argument arrays, +so command and path mappings are portable. Gitea jobs intentionally remain +Ubuntu-only. Windows GNU and macOS portable surfaces are cross-checked from +Ubuntu by the non-routine release graph; no native runtime claim is inferred. + +## Cache identity and invalidation + +The dedicated VPS runner's persisted tool-cache mount stores Cargo registry/git +downloads, the two pinned audit binaries, the routine target directory, and +native archives/installations. The target directory is content-keyed by Rust +1.97.1, `aarch64-unknown-linux-gnu`, `Cargo.lock`, the CI coverage and dependency +policies, the Skia feature manifest, and the OpenJPEG installer. Changing any of +these selects a new target generation. This avoids depending on an external +GitHub cache service that is unreachable from the isolated Gitea runner. +After checkout, a state file beside the keyed target cache binds every tracked +path's Git mode and actual-content digest to the exact mtime used for its last +build. Unchanged files recover that mtime across commits and retain valid Cargo +fingerprints. A content or executable-mode change receives the current time, so +it is newer than cached outputs and invalidates the affected graph. The state is +written before compilation, so completed work remains reusable after a timeout, +and the workflow immediately verifies every applied mtime. +The manual `cold_cache` dispatch input clears the Cargo registry/git dependency +cache, pinned tools, and exact native paths before setup; it intentionally keeps +the separately keyed compiled target cache. This is the acceptance test's cold +dependency/native-cache case. Ordinary pushes and dispatches reuse all validated +cache layers. + +OpenJPEG caches include a three-line identity containing version 2.5.4, commit +`6c4a29b00211eb0430fa0e5e890f1ce5c80f409f`, and runner architecture. The +installer checks this identity and `pkg-config --exact-version` before reuse. +The Skia cache is the exact 0.99.0 ARM64 GNU feature archive with SHA-256 +`dd127f458a5e67a79f3936a8aa19f822fe90a1d6a11b50b5f84df2b0519d909c`. + +The workflow never sets `FORCE_SKIA_BINARIES_DOWNLOAD`: rust-skia uses the +normal crates.io `.cargo_vcs_info.json` hash. The proof reads rust-skia's +persisted build-script output even on a cache hit and requires +`DOWNLOAD AND INSTALL SUCCEEDED`; the Rust driver rejects missing success output +or any `STARTING A FULL BUILD`. The adapter enables +rust-skia's `no-compile` feature on every target, making an unavailable or +incorrect binary a fast failure rather than a source build. + +To invalidate local artifacts, remove only the relevant target directory or use: ```sh -cargo clean --target-dir target/ci/linux-stable-minimal +cargo clean --target-dir target/required +cargo clean --target-dir target/release-gate ``` -Live-grid credentials, physical audio devices, and proprietary services remain -outside automatic CI and retain their dedicated opt-in gates. +## Non-routine release gate + +The Rust `release-gate` command owns clean MSRV and portable cross-target +checks, benchmark compilation, the 16-cycle resource soak, all 16 source +packages, release binaries, native artifact inspection, and the aggregate +release-candidate audit. Reference C# performance regeneration and live-grid, +physical-audio, proprietary-service, signing, and platform-native installer +checks retain their documented credential/device/manual boundaries. Their +committed evidence and fast regression checks remain in routine CI. + +## Troubleshooting and runtime evidence + +- A host mismatch means the required runner label is not the documented ARM64 + GNU VPS. Do not substitute a musl or x86 archive. +- An OpenJPEG identity failure requires deleting only its native cache entry and + rerunning the installer. +- A Skia hash/download failure indicates an upstream release-asset or redirect + reachability problem. Restore the exact archive in the configured cache; do + not enable source compilation. +- A 12-minute driver failure identifies the last completed stage in the uploaded + `required-gate.json`. Optimize or remove duplicate work only after updating + the coverage manifest; do not raise the 15-minute timeout. + +The validation record and five qualifying ARM64 run links are maintained in +[`ci-runtime-evidence.md`](ci-runtime-evidence.md). diff --git a/release/DEPENDENCY-LICENSES.json b/release/DEPENDENCY-LICENSES.json index 4a0599f..179b884 100644 --- a/release/DEPENDENCY-LICENSES.json +++ b/release/DEPENDENCY-LICENSES.json @@ -4553,10 +4553,6 @@ { "path": "generated-package-license-notice.txt", "sha256": "e6d6e0146d16ea19abea522d8ed77aadcdc4f90718a11d4d2cfb55c199ec8b4f" - }, - { - "path": "skia/LICENSE", - "sha256": "5f787c1dee3c56547f09ccc2906ab5f5293c4d8dd6c8654e573216c38e908dbd" } ] }, @@ -4570,10 +4566,6 @@ { "path": "generated-package-license-notice.txt", "sha256": "85160b23bdf006db68c232129bbf4ace9b3eee2fd21ba41025f447c8281c4f9d" - }, - { - "path": "repository-license/skia-bindings-0.99.0/skia/LICENSE", - "sha256": "5f787c1dee3c56547f09ccc2906ab5f5293c4d8dd6c8654e573216c38e908dbd" } ] }, @@ -4587,10 +4579,6 @@ { "path": "generated-package-license-notice.txt", "sha256": "3eefd55332613883150398d8fcf426ac1036bbedecdf28c510573a2da294cf16" - }, - { - "path": "repository-license/skia-bindings-0.99.0/skia/LICENSE", - "sha256": "5f787c1dee3c56547f09ccc2906ab5f5293c4d8dd6c8654e573216c38e908dbd" } ] }, diff --git a/release/DISTRIBUTION-MANIFEST.json b/release/DISTRIBUTION-MANIFEST.json index 4b2fc25..af1754a 100644 --- a/release/DISTRIBUTION-MANIFEST.json +++ b/release/DISTRIBUTION-MANIFEST.json @@ -16,74 +16,19 @@ "sha256": "a7c5358f9d0b0ed236519fec9d5d4faee9c04127eedac9ba3ab536d3dcf1b1f2" }, { - "path": ".gitea/workflows/api-surface.yml", - "bytes": 2661, - "sha256": "0d65e6a99ba54f6501681c8a8eb6d07b1debcdfcfd3e7649fe2aa72b2c7798a0" + "path": ".gitea/workflows/ci.yml", + "bytes": 6206, + "sha256": "3e2bf2ca54e037380f0a4c9edd02fb948c37b9bb27c308c1d3e5d614b07d57d4" }, { - "path": ".gitea/workflows/artifact-audit.yml", - "bytes": 3497, - "sha256": "91105987b707aa932cc476d8d496c0610e4ce9d0301c40e05916edae425225cb" - }, - { - "path": ".gitea/workflows/codegen.yml", - "bytes": 7978, - "sha256": "c31c803c50d0e4dac495b80e503c4fc9a99ecf3a1ea95f670317aff75f5d3b94" - }, - { - "path": ".gitea/workflows/concurrency-audit.yml", - "bytes": 2842, - "sha256": "095a90f2dbaf2b5a97155d8a21fc2ecafcfb70eab4f2535dce5aa94e22af93ca" - }, - { - "path": ".gitea/workflows/documentation.yml", - "bytes": 2957, - "sha256": "8b004db1451886a81fc07de394657a8cc0f8aa6df0ea4d71e6b835daad18267b" - }, - { - "path": ".gitea/workflows/imaging-meshing.yml", - "bytes": 3893, - "sha256": "bf99f0a0dc344033c1a702812013b5f643cc7be15e3b4a9703bfde47864ab611" - }, - { - "path": ".gitea/workflows/jpeg2000.yml", - "bytes": 1607, - "sha256": "f7a23f0c89bb9476fffc3036b257ab8022a7af8b3b3ecd586d98cb7d76a23db6" - }, - { - "path": ".gitea/workflows/performance.yml", - "bytes": 1907, - "sha256": "6cb5f7803765d91a45816275d8e86c26546b627319813d3ee8f3b463f6db2dad" - }, - { - "path": ".gitea/workflows/release-candidate.yml", - "bytes": 4397, - "sha256": "e7ad8872f32f6d241a57e75eb7210953697869220a8e0af8a57b814b3aa8c216" - }, - { - "path": ".gitea/workflows/release-matrix.yml", - "bytes": 4263, - "sha256": "5e8b05b3cedc46d7fb15a7b35ba5944db520dd747b6fc893eb5dd57954c61ac7" - }, - { - "path": ".gitea/workflows/rust-workspace.yml", - "bytes": 5284, - "sha256": "2c11810e9d8eefb5f81e3e48043b0794262bc7a93f5ae3f731df314a2546adb9" - }, - { - "path": ".gitea/workflows/skia.yml", - "bytes": 1186, - "sha256": "ab1c88406ad7e555a250cab587259d63c924062e7aeb743b75bcc2743c04cf5d" - }, - { - "path": ".gitea/workflows/supply-chain.yml", - "bytes": 1808, - "sha256": "0f988ff455e6d09a79c058df95913b22b8f227d35dd24ebd902986684d046dfd" + "path": ".gitea/workflows/release.yml", + "bytes": 2371, + "sha256": "1db8899a6745f61bec38809f78f1475c0f6afc06b450ef112fea4d03deca069b" }, { "path": ".gitignore", - "bytes": 233, - "sha256": "a8724cbe50232030f415af0eeb5e4c5ec9b068d26b0f2478506481f14cbe2915" + "bytes": 245, + "sha256": "a8a3b5c04d22081afedd1c34ac2949dfa6204185f9f8efbf78f9bbb746cce475" }, { "path": "AGENTS.md", @@ -133,7 +78,7 @@ { "path": "api/DOCUMENTATION-COVERAGE.md", "bytes": 911, - "sha256": "490856724eaac1b6ee6065b048c50ae0bec6905ffebe6f7431ed7e6c6691cdc0" + "sha256": "0d05fc115a7754074ba9c084654556ac32ce07b006236d21d406223cc5074de9" }, { "path": "api/MAPPING-COVERAGE.md", @@ -265,6 +210,11 @@ "bytes": 3011, "sha256": "718c065c78377c846ccd962922e06b19de276c32f427f73a026060eec9b7a45c" }, + { + "path": "ci/ci-coverage.json", + "bytes": 5480, + "sha256": "4d1633c1cb396f934a8eff5f4e27362f6872b95ac66b95e9c4ac8e75ef833b29" + }, { "path": "ci/concurrency-thresholds.json", "bytes": 169, @@ -293,7 +243,7 @@ { "path": "ci/evidence/documentation-audit.json", "bytes": 363, - "sha256": "56e6e2224c3f7ebabc4f1d3c4c8d63a9208a03101d571728dab10a63332f51f6" + "sha256": "8ee4ebd39572fc6ea00c40536bb59fd03e59e6973417f931166c4da1d963e9f3" }, { "path": "ci/evidence/fake-grid-smoke.jsonl", @@ -382,8 +332,8 @@ }, { "path": "crates/libremetaverse-imaging-skia/Cargo.toml", - "bytes": 1398, - "sha256": "bb17594c9cb1699b5e1d0ec3e3d885ca44d1b6dcd18c98d6e7e0d1c374572d34" + "bytes": 1440, + "sha256": "0fb038f36280490487eb3f9e24176abd0f002504918b7bdac83ca319d92298e2" }, { "path": "crates/libremetaverse-imaging-skia/README.md", @@ -1600,6 +1550,11 @@ "bytes": 5813, "sha256": "cae4c875a9c1b83133a32dbfca1a64cd834e8459e8c7bab47d416527cceb45ca" }, + { + "path": "docs/ci-runtime-evidence.md", + "bytes": 2215, + "sha256": "f61a7775fd6076e0c439dbbe6ada8dcf9bac70ab76f6a16ead8151f1ceceb9c3" + }, { "path": "docs/client-core.md", "bytes": 2145, @@ -1612,8 +1567,8 @@ }, { "path": "docs/dependency-policy.md", - "bytes": 4485, - "sha256": "2f25574432215ea38530de1a1f4680fc5a94f1439820059b8b16d86eb0c5ad95" + "bytes": 4488, + "sha256": "2e822f2deefa949a4d2173ee6d993a7e3c5874c01cb4b90755d98d1b8805f3e8" }, { "path": "docs/discovery.md", @@ -1627,8 +1582,8 @@ }, { "path": "docs/imaging-meshing.md", - "bytes": 3413, - "sha256": "46559b199f5ef7de10a3280724cd1b06b733be7a078f85efefc569a001370ca5" + "bytes": 3406, + "sha256": "51ae2dfb88f5a40a01ab9394ae0545806377c6cbb2cbd85da2b7947b5534859e" }, { "path": "docs/inventory.md", @@ -1662,8 +1617,8 @@ }, { "path": "docs/release-ci-matrix.md", - "bytes": 4072, - "sha256": "c4bf34fcc9c204824c68329d7bd2aca5a8799a7d34b44e2f8168ada923d34f47" + "bytes": 7149, + "sha256": "d6f2fa58bbbc361c9d02ca41c8764f7c2f2d328d998f61b85b4c32a64fe25355" }, { "path": "docs/release-provenance.md", @@ -2013,7 +1968,7 @@ { "path": "release/SHA256SUMS", "bytes": 909, - "sha256": "67a50c93a6d574b82e30f923819fd7ee1dd102a62c27fe7db9f435f8ef5c246a" + "sha256": "2dee8c4cbdc89f004fa3cd3a290a0943325f051dc4f169e4fe20f32f4a118c18" }, { "path": "tests/PARITY.md", @@ -2042,8 +1997,8 @@ }, { "path": "tests/compat/build.rs", - "bytes": 1419, - "sha256": "ec0b62b560ecfa2673d31e2b936412a9e72b35fea5d44d962c4c20acc133a298" + "bytes": 1798, + "sha256": "844ea0b32a0feb5336cb4de807cf6b93c6588838d562ce5cd49433c91f5f7b9b" }, { "path": "tests/compat/src/lib.rs", @@ -2407,8 +2362,8 @@ }, { "path": "tests/semver-port/Cargo.lock", - "bytes": 77807, - "sha256": "2dae33598f8b671b971518149ac71e45d5d081054430864d23718e90f2d21e10" + "bytes": 77974, + "sha256": "8900f20f66a4b008d4aa8ee408df7fed554624d8ec4b474fab36a713f115128f" }, { "path": "tests/semver-port/Cargo.toml", @@ -2452,8 +2407,8 @@ }, { "path": "tools/check_milestone_06.py", - "bytes": 5531, - "sha256": "a5cc71300f5b28695830eb8e88bbe3da0facad76a11120d0445bbe3a771a6d2c" + "bytes": 5598, + "sha256": "18f389b59020a6705595cfa18e6c374e09e4d81c8c3b1e397aa01da13c885e33" }, { "path": "tools/check_milestone_08.py", @@ -2507,48 +2462,48 @@ }, { "path": "tools/check_milestone_10.py", - "bytes": 9122, - "sha256": "da1dba171d8f234e6d9234e70296f82a7eb87c943111ffd59be7d802d08e76bb" + "bytes": 9038, + "sha256": "34b7c8bc4d924364abe2ffc14738a9b237e00b931691b2e6a9adb4cfd3ef2289" }, { "path": "tools/check_milestone_10_issue_76.py", - "bytes": 4616, - "sha256": "e307ed630fb6ab9d1cc1e9c4692f378014400a4a839d6b746a5863322d1f861d" + "bytes": 4573, + "sha256": "62034c4d0a77bc5a2d848306148e274150049ec2baec61ca1a5fbfd56d601f99" }, { "path": "tools/check_milestone_10_issue_77.py", - "bytes": 6627, - "sha256": "32932d8e5154d5b4232071708ece6742398a551faa6d9e513ad8b9d7956359ee" + "bytes": 6584, + "sha256": "38d0f7216b875add2b7b761148e3c13a2fecb2cc0498ebabb5713d0e09ad5bdf" }, { "path": "tools/check_milestone_10_issue_78.py", - "bytes": 5251, - "sha256": "b0b66b1cfa2ab0407346acd94233cd4c681d05bb9bd8d2a6771ee37ba79c7bf3" + "bytes": 5208, + "sha256": "bfb1dabc8eabbe7e102b413fb9cff132e226a38c70a069d5af9feaa62ed1b57e" }, { "path": "tools/check_milestone_10_issue_79.py", - "bytes": 5346, - "sha256": "917090381f099b0322996e28abe997012b2d9f8335e6d5f9296b4810de0a6a81" + "bytes": 5303, + "sha256": "97b0048bd8e52a2e678962f6b4ee327967895b547fd34e20ba604f4c73a3e6ae" }, { "path": "tools/check_milestone_10_issue_80.py", - "bytes": 6673, - "sha256": "429d4e879a6f8508c0c6b9407ac05953b9615764e4496d163a79793d1b2bd62c" + "bytes": 6630, + "sha256": "80364edcbf7eafe6ed7764dc8c014a893e5f92aef0296137b057e17036b7d74e" }, { "path": "tools/check_milestone_10_issue_81.py", - "bytes": 6933, - "sha256": "1f96c80e9dae61ad3e54fa4c7208e9fed2a30face4904fe8791ad9a4615f424b" + "bytes": 6890, + "sha256": "3438337735c4a011840c2ee83aa37ba680de26e9a21e429214f98bb957b225b6" }, { "path": "tools/check_milestone_10_issue_82.py", - "bytes": 6664, - "sha256": "dfbd3657efc57a7c02fd326545c6a8b08fc234d8df30fc98362cfbdf113c3e8f" + "bytes": 6621, + "sha256": "20a0b1a39d7d8c6262638277db87640c6031185765da24d2178b7a8d59c8d105" }, { "path": "tools/check_milestone_10_issue_83.py", - "bytes": 7722, - "sha256": "017613c5df110f2e469846e57b3e734e63447d52f94cec3e5e08eb487c0f254e" + "bytes": 7619, + "sha256": "e3f31776d6b2ee56d93618a83258d833194c8ec5da4fef5617cf3c6613f4f13b" }, { "path": "tools/check_test_parity.py", @@ -2570,6 +2525,11 @@ "bytes": 36939, "sha256": "f1b56f753d77ea40e72d8a4288174fdbc5974285c13d07f04ea52b34dbdd7506" }, + { + "path": "tools/ci-matrix/src/ci_gate.rs", + "bytes": 45047, + "sha256": "73ebb225ef1128e82bd38e7d41d2a164e4bd82c89cbe241f547762f132c0c790" + }, { "path": "tools/ci-matrix/src/dependency.rs", "bytes": 12796, @@ -2582,18 +2542,18 @@ }, { "path": "tools/ci-matrix/src/lib.rs", - "bytes": 21442, - "sha256": "c4a04b3342249af495fad0f9c1a7b8e84f247f00a590cf87e50f3747d2be7fd0" + "bytes": 21175, + "sha256": "096d055e5d295be43ae1db78acbc98557d27d63631c7699b6eb5d281fcc18424" }, { "path": "tools/ci-matrix/src/main.rs", - "bytes": 8006, - "sha256": "e61c19db7036796311182afb6c0671bbbfdd681f8eb1b4df08e596b43c0b0218" + "bytes": 9422, + "sha256": "33a3c408f2347b48b4ec43ca3771fa561dd9d58657fda5b067c4e06c4fe7c7fb" }, { "path": "tools/ci-matrix/src/provenance.rs", - "bytes": 44270, - "sha256": "6946295307a3740ba6b70187a37e0f4011f8070a4ef9e1adccecc3487aa71499" + "bytes": 52871, + "sha256": "7ec2063dbfc7c11ae0b6598d304d9f8b9f215f37ebdf03c42fda35d43e574387" }, { "path": "tools/ci-matrix/src/release_candidate.rs", @@ -2662,8 +2622,13 @@ }, { "path": "tools/install_openjpeg_2_5_4.sh", - "bytes": 2085, - "sha256": "cece0ccb847dfc015734297b6988752d140e15a44f5347236266d2af699f7052" + "bytes": 2582, + "sha256": "7ca7eb0ab184f4acbdb475a20d140f001d7bd020d3244d163695406e88f52925" + }, + { + "path": "tools/normalize_git_mtimes.py", + "bytes": 5879, + "sha256": "88608e6bd5bea3c365a22bb97fc56cf67acb0c911fd67d05e0562256a8522d36" }, { "path": "tools/performance/Cargo.toml", @@ -2699,13 +2664,13 @@ "generated_release_files": [ { "path": "release/DEPENDENCY-LICENSES.json", - "bytes": 217677, - "sha256": "6482698a22201b04f3234363790edc0be0dba39f445ac939e3fc8d4edeb372e9" + "bytes": 217171, + "sha256": "a4095621c7975f6b01c5b1653b375c571be15657f7fa824d6b5a2e46f3434dcc" }, { "path": "release/THIRD-PARTY-NOTICES.md", - "bytes": 848536, - "sha256": "de5088841682813cc952af4d1dcc57d2521694e192888042d5585e344e570d16" + "bytes": 846570, + "sha256": "da051a965d39715d10da5c17fdfdafd8059b83d2491837d223607e9c5e16a5fa" }, { "path": "release/NATIVE-LICENSES.md", diff --git a/release/SHA256SUMS b/release/SHA256SUMS index 16901b3..18c6c81 100644 --- a/release/SHA256SUMS +++ b/release/SHA256SUMS @@ -5,6 +5,6 @@ 5f23a03801161141e4e7baa57b787a606ea83ffc3ccd8de5e760240b399707cc ci/evidence/api-audit.json 9c9c6ed236d04880bab504387251f7a9e283f338c48fe46a6fe0ece472eb8063 ci/evidence/artifact-audit.json 80c926cfdb7f4055659392cba69e37352a8d501cf235a244362384b04b78a02a ci/evidence/concurrency-audit.json -56e6e2224c3f7ebabc4f1d3c4c8d63a9208a03101d571728dab10a63332f51f6 ci/evidence/documentation-audit.json +8ee4ebd39572fc6ea00c40536bb59fd03e59e6973417f931166c4da1d963e9f3 ci/evidence/documentation-audit.json db355bb598888d08e1bfac56436f0dc786312defb8345741dd5003ff4e37409f ci/evidence/provenance-audit.json b417ee7bf6b748cc86839e5354753c0f8c1f8309d13c0fea379c77f602e69fc0 release/NATIVE-LICENSES.md diff --git a/release/THIRD-PARTY-NOTICES.md b/release/THIRD-PARTY-NOTICES.md index 8e5acad..987c3d1 100644 --- a/release/THIRD-PARTY-NOTICES.md +++ b/release/THIRD-PARTY-NOTICES.md @@ -281,9 +281,9 @@ This document must accompany source and binary distributions. A package's presen | `simd-adler32 0.3.10` | `MIT` | `3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea` | `42a35170233e83e18856792e748de4c1ce4a63b2afce9a370c89ef3fe23f9f2d` | | `simd_cesu8 1.2.0` | `Apache-2.0 OR MIT` | `11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520` | `a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2`
`23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3` | | `simdutf8 0.1.5` | `MIT OR Apache-2.0` | `e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e` | `0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594`
`508a77d2e7b51d98adeed32648ad124b7b30241a8e70b2e72c99f92d8e5874d1` | -| `skia-bindings 0.99.0` | `MIT` | `3e2d1c3ebd697c0cbded0145e9204a38fa6b268446051b7196d0a096414ea7f3` | `e6d6e0146d16ea19abea522d8ed77aadcdc4f90718a11d4d2cfb55c199ec8b4f`
`5f787c1dee3c56547f09ccc2906ab5f5293c4d8dd6c8654e573216c38e908dbd` | -| `skia-safe 0.99.0` | `MIT` | `9f512ac418a64194842dd05566320805dad1c957c521039db2486fd6368865bc` | `85160b23bdf006db68c232129bbf4ace9b3eee2fd21ba41025f447c8281c4f9d`
`5f787c1dee3c56547f09ccc2906ab5f5293c4d8dd6c8654e573216c38e908dbd` | -| `skia-svg-macros 0.1.0` | `MIT` | `044dd2233c9717a74f75197f3e7f0a966db2127c0ffb5e05013b480a9b75b2c7` | `3eefd55332613883150398d8fcf426ac1036bbedecdf28c510573a2da294cf16`
`5f787c1dee3c56547f09ccc2906ab5f5293c4d8dd6c8654e573216c38e908dbd` | +| `skia-bindings 0.99.0` | `MIT` | `3e2d1c3ebd697c0cbded0145e9204a38fa6b268446051b7196d0a096414ea7f3` | `e6d6e0146d16ea19abea522d8ed77aadcdc4f90718a11d4d2cfb55c199ec8b4f` | +| `skia-safe 0.99.0` | `MIT` | `9f512ac418a64194842dd05566320805dad1c957c521039db2486fd6368865bc` | `85160b23bdf006db68c232129bbf4ace9b3eee2fd21ba41025f447c8281c4f9d` | +| `skia-svg-macros 0.1.0` | `MIT` | `044dd2233c9717a74f75197f3e7f0a966db2127c0ffb5e05013b480a9b75b2c7` | `3eefd55332613883150398d8fcf426ac1036bbedecdf28c510573a2da294cf16` | | `slab 0.4.12` | `MIT` | `0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5` | `8ce0830173fdac609dfb4ea603fdc002c2f4af0dc9b1a005653f5da9cf534b18` | | `smallvec 1.15.2` | `MIT OR Apache-2.0` | `8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90` | `a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2`
`0b28172679e0009b655da42797c03fd163a3379d5cfa67ba1f1655e974a2a1a9` | | `socket2 0.6.5` | `MIT OR Apache-2.0` | `c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4` | `a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2`
`378f5840b258e2779c39418f3f2d7b2ba96f1c7917dd6be0713f88305dbda397` | @@ -6295,44 +6295,6 @@ The published crate archive contains no discoverable top-level license/notice fi -### `5f787c1dee3c56547f09ccc2906ab5f5293c4d8dd6c8654e573216c38e908dbd` - -Packages: skia-bindings 0.99.0, skia-safe 0.99.0, skia-svg-macros 0.1.0
-Source filenames: repository-license/skia-bindings-0.99.0/skia/LICENSE, skia/LICENSE - -
-Copyright (c) 2011 Google Inc. All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
-  * Redistributions of source code must retain the above copyright
-    notice, this list of conditions and the following disclaimer.
-
-  * Redistributions in binary form must reproduce the above copyright
-    notice, this list of conditions and the following disclaimer in
-    the documentation and/or other materials provided with the
-    distribution.
-
-  * Neither the name of the copyright holder nor the names of its
-    contributors may be used to endorse or promote products derived
-    from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
- ### `615f39d3be6ff90d956c6b490b1f967a8995e88c109384895740748cb197fd87` Packages: objc2-core-graphics 0.3.2
diff --git a/tests/compat/build.rs b/tests/compat/build.rs index 3de3d94..8857664 100644 --- a/tests/compat/build.rs +++ b/tests/compat/build.rs @@ -19,7 +19,14 @@ fn main() { .join("../..") .join(".env"); println!("cargo:rustc-check-cfg=cfg(live_grid_credentials)"); - println!("cargo:rerun-if-changed={}", dotenv.display()); + // A missing rerun-if-changed input is perpetually stale to Cargo. CI and + // normal offline builds do not have this ignored credential file, so only + // watch it once it exists. RUN_LIVE_TESTS remains an unconditional watched + // input and therefore makes the documented opt-in rebuild discover a newly + // created .env file. + if dotenv.is_file() { + println!("cargo:rerun-if-changed={}", dotenv.display()); + } for key in KEYS { println!("cargo:rerun-if-env-changed={key}"); } diff --git a/tests/semver-port/Cargo.lock b/tests/semver-port/Cargo.lock index 24c16cf..8d1fc19 100644 --- a/tests/semver-port/Cargo.lock +++ b/tests/semver-port/Cargo.lock @@ -708,53 +708,53 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-io", @@ -883,9 +883,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -1196,6 +1196,7 @@ dependencies = [ "futures-channel", "futures-util", "libremetaverse-imaging", + "libremetaverse-prim-mesher", "libremetaverse-structured-data", "libremetaverse-types", "mac_address2", @@ -1222,6 +1223,14 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libremetaverse-prim-mesher" +version = "0.0.1" +dependencies = [ + "libremetaverse-imaging", + "libremetaverse-types", +] + [[package]] name = "libremetaverse-rlv" version = "0.0.1" @@ -1428,9 +1437,9 @@ checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -2099,9 +2108,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" dependencies = [ "aws-lc-rs", "ring", diff --git a/tools/check_milestone_06.py b/tools/check_milestone_06.py index ee289de..5010ef2 100644 --- a/tools/check_milestone_06.py +++ b/tools/check_milestone_06.py @@ -104,11 +104,7 @@ def check_benchmarks_and_ci() -> None: if not (crate / "benches" / f"{name}.rs").is_file(): raise SystemExit(f"missing {name} benchmark source") - workflows = { - "imaging-meshing": ROOT / ".gitea" / "workflows" / "imaging-meshing.yml", - "jpeg2000": ROOT / ".gitea" / "workflows" / "jpeg2000.yml", - "skia": ROOT / ".gitea" / "workflows" / "skia.yml", - } + workflows = {"consolidated": ROOT / ".gitea" / "workflows" / "ci.yml"} for name, path in workflows.items(): if not path.is_file(): raise SystemExit(f"missing {name} native-feature workflow") @@ -121,6 +117,10 @@ def check_benchmarks_and_ci() -> None: f"{name} workflow must use only ubuntu-latest, found: " + ", ".join(unsupported) ) + text = path.read_text() + for marker in ("required-gate", "METACRATE_SKIA_ARCHIVE", "OPENJPEG_PREFIX"): + if marker not in text: + raise SystemExit(f"{name} workflow is missing {marker}") def main() -> None: diff --git a/tools/check_milestone_10.py b/tools/check_milestone_10.py index 276dd0c..90d3160 100644 --- a/tools/check_milestone_10.py +++ b/tools/check_milestone_10.py @@ -227,12 +227,11 @@ def check_generation_and_ci() -> None: subprocess.run( ["python3", "tools/generate_lsl_tables.py", "--check"], cwd=ROOT, check=True ) - workflow = ROOT / ".gitea" / "workflows" / "rust-workspace.yml" + workflow = ROOT / ".gitea" / "workflows" / "ci.yml" require_markers( workflow, ( - "python3 tools/check_milestone_10.py", - "python3 tools/test_milestone_10.py", + "required-gate", "runs-on: ubuntu-latest", ), ) diff --git a/tools/check_milestone_10_issue_76.py b/tools/check_milestone_10_issue_76.py index fd1bd2e..f4666dc 100644 --- a/tools/check_milestone_10_issue_76.py +++ b/tools/check_milestone_10_issue_76.py @@ -22,7 +22,7 @@ REFERENCE = ( / "reference_pipeline.rs" ) DOC = ROOT / "crates" / "libremetaverse-rendering-simple" / "README.md" -WORKFLOW = ROOT / ".gitea" / "workflows" / "rust-workspace.yml" +WORKFLOW = ROOT / ".gitea" / "workflows" / "ci.yml" STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(") TYPE = "T:LibreMetaverse.Rendering.SimpleRenderer" @@ -85,7 +85,7 @@ def main() -> None: "all four `DetailLevel`", "65,536", "Error::Rendering", "source primitive UUID", "MeshFoundry", "prim-local coordinates", )) - require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_76.py",)) + require_markers(WORKFLOW, ("required-gate",)) print( "issue 76 audit: native prim/sculpt conversion, face metadata, texture mapping, " "bounded errors, deterministic evidence, dependency isolation, and docs are present" diff --git a/tools/check_milestone_10_issue_77.py b/tools/check_milestone_10_issue_77.py index 6676d31..cc90a69 100644 --- a/tools/check_milestone_10_issue_77.py +++ b/tools/check_milestone_10_issue_77.py @@ -19,7 +19,7 @@ BENCH = ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "benches" / WORKSPACE = ROOT / "Cargo.toml" DOC = ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "README.md" STRUCTURED = ROOT / "crates" / "libremetaverse-structured-data" / "src" / "lib.rs" -WORKFLOW = ROOT / ".gitea" / "workflows" / "rust-workspace.yml" +WORKFLOW = ROOT / ".gitea" / "workflows" / "ci.yml" STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(") TYPE = "T:LibreMetaverse.Rendering.MeshFoundry" @@ -98,7 +98,7 @@ def main() -> None: "exactly-four-influence", "XOR winding", "64 MiB", "65,536", "6,000,000", "source primitive UUID", "57,600-vertex", )) - require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_77.py",)) + require_markers(WORKFLOW, ("required-gate",)) print( "issue 77 audit: native prim/sculpt/terrain and packed mesh pipelines, bounded LLSD/zlib, " "LOD, normals/tangents/UVs, materials, rigging, convex data, tests, benchmark, and docs are present" diff --git a/tools/check_milestone_10_issue_78.py b/tools/check_milestone_10_issue_78.py index 3fa7e31..804caef 100644 --- a/tools/check_milestone_10_issue_78.py +++ b/tools/check_milestone_10_issue_78.py @@ -15,7 +15,7 @@ GENERATED = ROOT / "crates" / "libremetaverse-rlv" / "src" / "generated.rs" TESTS = ROOT / "crates" / "libremetaverse-rlv" / "tests" / "protocol_parsing.rs" COMPAT = ROOT / "tests" / "compat" / "tests" / "rlv_common_semantics.rs" DOC = ROOT / "crates" / "libremetaverse-rlv" / "README.md" -WORKFLOW = ROOT / ".gitea" / "workflows" / "rust-workspace.yml" +WORKFLOW = ROOT / ".gitea" / "workflows" / "ci.yml" STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(") TYPES = { @@ -98,7 +98,7 @@ def main() -> None: "side-effect-free", "119 behavior", "56 pinned", "half-open byte span", "64 KiB", "128 comma-separated", "does not wait", "ubuntu-latest", )) - require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_78.py",)) + require_markers(WORKFLOW, ("required-gate",)) print( "issue 78 audit: bounded pure RLV parsing, typed commands/queries/restrictions, " "exact aliases, positioned errors, mapped values, mutation evidence, and docs are present" diff --git a/tools/check_milestone_10_issue_79.py b/tools/check_milestone_10_issue_79.py index d1a5a9c..e79b60f 100644 --- a/tools/check_milestone_10_issue_79.py +++ b/tools/check_milestone_10_issue_79.py @@ -16,7 +16,7 @@ GENERATED = ROOT / "crates" / "libremetaverse-rlv" / "src" / "generated.rs" TESTS = ROOT / "crates" / "libremetaverse-rlv" / "tests" / "state_semantics.rs" COMPAT = ROOT / "tests" / "compat" / "tests" / "rlv_inventory_map_semantics.rs" DOC = ROOT / "crates" / "libremetaverse-rlv" / "README.md" -WORKFLOW = ROOT / ".gitea" / "workflows" / "rust-workspace.yml" +WORKFLOW = ROOT / ".gitea" / "workflows" / "ci.yml" CATALOG = ROOT / "api" / "public-api.json" STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(") @@ -111,7 +111,7 @@ def main() -> None: "after manager locks are released", "secure and explicit", "no network", "ubuntu-latest", )) - require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_79.py",)) + require_markers(WORKFLOW, ("required-gate",)) print( "issue 79 audit: native restriction state, inventory maps, recursive locks, " "camera aggregation, blacklist, permission precedence, concurrency evidence, " diff --git a/tools/check_milestone_10_issue_80.py b/tools/check_milestone_10_issue_80.py index 141a8c9..33d91da 100644 --- a/tools/check_milestone_10_issue_80.py +++ b/tools/check_milestone_10_issue_80.py @@ -20,7 +20,7 @@ QUERY_TESTS = ROOT / "tests" / "compat" / "tests" / "rlv_query_basics_semantics. ACTION_TESTS = ROOT / "tests" / "compat" / "tests" / "rlv_attach_commands_semantics.rs" NOTIFY_TESTS = ROOT / "tests" / "compat" / "tests" / "rlv_notify_semantics.rs" DOC = ROOT / "crates" / "libremetaverse-rlv" / "README.md" -WORKFLOW = ROOT / ".gitea" / "workflows" / "rust-workspace.yml" +WORKFLOW = ROOT / ".gitea" / "workflows" / "ci.yml" CATALOG = ROOT / "api" / "public-api.json" STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(") @@ -136,7 +136,7 @@ def main() -> None: "after releasing every manager lock", "completed inventory and agent", "616", "ubuntu-latest", )) - require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_80.py",)) + require_markers(WORKFLOW, ("required-gate",)) print( "issue 80 audit: native callbacks, typed actions and queries, bounded service " "dispatch, inventory/agent adapters, cancellation, post-lock notifications, " diff --git a/tools/check_milestone_10_issue_81.py b/tools/check_milestone_10_issue_81.py index 7d5be73..0201c84 100644 --- a/tools/check_milestone_10_issue_81.py +++ b/tools/check_milestone_10_issue_81.py @@ -18,7 +18,7 @@ COMPAT_TESTS = ( ROOT / "crates" / "libremetaverse-lsl-tools" / "tests" / "lexer_compat.rs" ) DOC = ROOT / "crates" / "libremetaverse-lsl-tools" / "README.md" -WORKFLOW = ROOT / ".gitea" / "workflows" / "rust-workspace.yml" +WORKFLOW = ROOT / ".gitea" / "workflows" / "ci.yml" CATALOG = ROOT / "api" / "public-api.json" STUB_RE = re.compile( r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(" @@ -135,7 +135,7 @@ def main() -> None: "64 Mi UTF-16", "16 Mi units", "Lexer::iter", "46 focused", "No C#, .NET runtime", "ubuntu-latest", )) - require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_81.py",)) + require_markers(WORKFLOW, ("required-gate",)) print( "issue 81 audit: 22 mapped native types and 188 members, bounded UTF-16 " "source handling, comments and line directives, Unicode categories, deterministic " diff --git a/tools/check_milestone_10_issue_82.py b/tools/check_milestone_10_issue_82.py index 5d620bb..c83ea19 100644 --- a/tools/check_milestone_10_issue_82.py +++ b/tools/check_milestone_10_issue_82.py @@ -16,7 +16,7 @@ GENERATED = ROOT / "crates" / "libremetaverse-lsl-tools" / "src" / "generated.rs TESTS = ROOT / "crates" / "libremetaverse-lsl-tools" / "tests" / "parser_compat.rs" EXTENSION_TESTS = ROOT / "tests" / "compat" / "tests" / "extension_shims.rs" DOC = ROOT / "crates" / "libremetaverse-lsl-tools" / "README.md" -WORKFLOW = ROOT / ".gitea" / "workflows" / "rust-workspace.yml" +WORKFLOW = ROOT / ".gitea" / "workflows" / "ci.yml" CATALOG = ROOT / "api" / "public-api.json" STUB_RE = re.compile( r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(" @@ -125,7 +125,7 @@ def main() -> None: "nonassociative", "1,048,576", "16,777,216", "46 focused", "issue 83", "ubuntu-latest", )) - require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_82.py",)) + require_markers(WORKFLOW, ("required-gate",)) print( "issue 82 audit: 22 native mapped parser types and 215 members, deterministic " "LALR tables, precedence conflicts, bounded shifts/reductions, error-token " diff --git a/tools/check_milestone_10_issue_83.py b/tools/check_milestone_10_issue_83.py index b4e1b33..c28722f 100644 --- a/tools/check_milestone_10_issue_83.py +++ b/tools/check_milestone_10_issue_83.py @@ -19,7 +19,7 @@ GENERATOR = ROOT / "crates" / "libremetaverse-lsl-tools" / "src" / "generator.rs GENERATED = ROOT / "crates" / "libremetaverse-lsl-tools" / "src" / "generated.rs" TESTS = ROOT / "crates" / "libremetaverse-lsl-tools" / "tests" / "generator_compat.rs" DOC = ROOT / "crates" / "libremetaverse-lsl-tools" / "README.md" -WORKFLOW = ROOT / ".gitea" / "workflows" / "rust-workspace.yml" +WORKFLOW = ROOT / ".gitea" / "workflows" / "ci.yml" SCRIPT = ROOT / "tools" / "generate_lsl_tables.py" STUB_RE = re.compile( r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(" @@ -184,8 +184,7 @@ def main() -> None: require_markers( WORKFLOW, ( - "python3 tools/check_milestone_10_issue_83.py", - "python3 tools/generate_lsl_tables.py --check", + "required-gate", ), ) if "runs-on: ubuntu-latest" not in workflow or re.search( diff --git a/tools/ci-matrix/src/ci_gate.rs b/tools/ci-matrix/src/ci_gate.rs new file mode 100644 index 0000000..4f2a0a8 --- /dev/null +++ b/tools/ci-matrix/src/ci_gate.rs @@ -0,0 +1,1424 @@ +//! Consolidated, shell-free routine and release CI command graphs. + +use super::{ + MatrixError, Result, audit, audit_api_surface, audit_dependencies, audit_documentation, + audit_provenance, load, write_api_baseline, write_documentation_report, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::collections::BTreeSet; +use std::ffi::{OsStr, OsString}; +use std::fs::{self, OpenOptions}; +use std::io::{Read as _, Write as _}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; + +const COVERAGE_PATH: &str = "ci/ci-coverage.json"; +const REQUIRED_WORKFLOW: &str = ".gitea/workflows/ci.yml"; +const RELEASE_WORKFLOW: &str = ".gitea/workflows/release.yml"; +const LEGACY_WORKFLOW_COUNT: usize = 13; +const LEGACY_WORKFLOWS: [&str; LEGACY_WORKFLOW_COUNT] = [ + "api-surface.yml", + "artifact-audit.yml", + "codegen.yml", + "concurrency-audit.yml", + "documentation.yml", + "imaging-meshing.yml", + "jpeg2000.yml", + "performance.yml", + "release-candidate.yml", + "release-matrix.yml", + "rust-workspace.yml", + "skia.yml", + "supply-chain.yml", +]; +const SKIA_ARCHIVE: &str = "skia-binaries-a25a0fdb7d90429aa2d1-aarch64-unknown-linux-gnu-jpegd-jpege-pdf-svg-textlayout-vulkan-webpd-webpe.tar.gz"; +const SKIA_SHA256: &str = "dd127f458a5e67a79f3936a8aa19f822fe90a1d6a11b50b5f84df2b0519d909c"; +const SKIA_SUCCESS: &str = "DOWNLOAD AND INSTALL SUCCEEDED"; +const SKIA_SOURCE_BUILD: &str = "STARTING A FULL BUILD"; + +#[derive(Debug, Deserialize)] +struct CoverageManifest { + schema: u32, + required_workflow: String, + release_workflow: String, + hard_timeout_minutes: u64, + internal_target_seconds: u64, + required_checks: Vec, + legacy_workflows: Vec, +} + +#[derive(Debug, Deserialize)] +struct LegacyWorkflow { + workflow: String, + responsibility: String, + destination: String, + checks: Vec, + rationale: String, +} + +#[derive(Debug, Serialize)] +struct GateEvidence { + schema: u32, + gate: &'static str, + status: &'static str, + source_commit: String, + recorded_unix_seconds: u64, + runner_architecture: String, + rust_host: String, + rust_target: String, + cargo_cache_hit: Option, + dependency_cache_hit: Option, + native_cache_hit: Option, + tools_cache_hit: Option, + peak_memory_kib: Option, + elapsed_seconds: f64, + internal_target_seconds: u64, + skia: SkiaEvidence, + stages: Vec, + failure: Option, +} + +#[derive(Debug, Default, Serialize)] +struct SkiaEvidence { + archive: String, + sha256: String, + target: String, + download_and_install_succeeded: bool, + source_build_started: bool, +} + +#[derive(Debug, Serialize)] +struct StageEvidence { + id: String, + elapsed_seconds: f64, + commands: usize, + status: &'static str, +} + +#[derive(Clone, Copy)] +struct CommandSpec { + program: &'static str, + args: &'static [&'static str], +} + +impl CommandSpec { + const fn new(program: &'static str, args: &'static [&'static str]) -> Self { + Self { program, args } + } +} + +/// Validates the workflow split and the complete legacy-check coverage map. +/// +/// # Errors +/// +/// Returns an error when the manifest is incomplete, workflow triggers or +/// runners violate policy, or Skia can fall back to a source build. +pub fn audit_consolidated_ci(root: &Path) -> Result<()> { + let manifest = coverage(root)?; + validate_coverage(&manifest)?; + validate_workflows(root, &manifest)?; + validate_skia_configuration(root)?; + Ok(()) +} + +/// Runs the routine code-ready gate and always emits a stage-timing record. +/// +/// # Errors +/// +/// Returns an error for an invalid host/native identity, a failed validation +/// command, stale evidence, or exhaustion of the internal runtime target. +pub fn run_required_gate(root: &Path, evidence_path: &Path) -> Result<()> { + audit_consolidated_ci(root)?; + let manifest = coverage(root)?; + prepare_new_evidence(evidence_path)?; + let started = Instant::now(); + let rust_host = rust_host(root)?; + let expected_host = std::env::var("METACRATE_EXPECTED_HOST") + .unwrap_or_else(|_| "aarch64-unknown-linux-gnu".to_owned()); + let mut evidence = GateEvidence { + schema: 1, + gate: "required", + status: "failed", + source_commit: command_line(root, "git", &["rev-parse", "--verify", "HEAD"]) + .unwrap_or_else(|_| "unknown".to_owned()), + recorded_unix_seconds: unix_seconds()?, + runner_architecture: std::env::consts::ARCH.to_owned(), + rust_host: rust_host.clone(), + rust_target: expected_host.clone(), + cargo_cache_hit: cache_hit("METACRATE_CARGO_CACHE_HIT"), + dependency_cache_hit: cache_hit("METACRATE_DEPENDENCY_CACHE_HIT"), + native_cache_hit: cache_hit("METACRATE_NATIVE_CACHE_HIT"), + tools_cache_hit: cache_hit("METACRATE_TOOLS_CACHE_HIT"), + peak_memory_kib: peak_memory_kib(), + elapsed_seconds: 0.0, + internal_target_seconds: manifest.internal_target_seconds, + skia: SkiaEvidence { + archive: SKIA_ARCHIVE.to_owned(), + sha256: SKIA_SHA256.to_owned(), + target: expected_host.clone(), + ..SkiaEvidence::default() + }, + stages: Vec::new(), + failure: None, + }; + + let result = run_required( + root, + &manifest, + &rust_host, + &expected_host, + started, + &mut evidence, + ); + evidence.elapsed_seconds = started.elapsed().as_secs_f64(); + evidence.peak_memory_kib = peak_memory_kib(); + match &result { + Ok(()) => evidence.status = "ok", + Err(error) => evidence.failure = Some(error.to_string()), + } + write_evidence(evidence_path, &evidence)?; + result +} + +/// Runs the explicitly non-routine clean-build, cross-target, packaging, and soak graph. +/// +/// # Errors +/// +/// Returns an error when the release matrix is invalid, a release command +/// fails, or its create-new evidence record cannot be written. +#[allow(clippy::too_many_lines)] // This is the declarative release command inventory. +pub fn run_release_gate(root: &Path, evidence_path: &Path) -> Result<()> { + audit_consolidated_ci(root)?; + let matrix = load(root)?; + audit(root, &matrix)?; + prepare_new_evidence(evidence_path)?; + let started = Instant::now(); + let target_dir = root.join("target/release-gate"); + let environment = [ + (OsString::from("CARGO_INCREMENTAL"), OsString::from("0")), + ( + OsString::from("CARGO_TARGET_DIR"), + target_dir.into_os_string(), + ), + ]; + let stages: [(&str, Vec); 4] = [ + ( + "msrv-and-portable-cross-targets", + vec![ + CommandSpec::new( + "cargo", + &[ + "+1.96.0", + "check", + "--locked", + "--all-targets", + "--no-default-features", + "-p", + "libremetaverse-types", + "-p", + "libremetaverse-structured-data", + "-p", + "libremetaverse-imaging", + "-p", + "libremetaverse-prim-mesher", + ], + ), + CommandSpec::new( + "cargo", + &[ + "+1.97.1", + "check", + "--locked", + "--target", + "x86_64-pc-windows-gnu", + "--no-default-features", + "-p", + "libremetaverse-types", + "-p", + "libremetaverse-structured-data", + "-p", + "libremetaverse-imaging", + "-p", + "libremetaverse-prim-mesher", + ], + ), + CommandSpec::new( + "cargo", + &[ + "+1.97.1", + "check", + "--locked", + "--target", + "x86_64-apple-darwin", + "--no-default-features", + "-p", + "libremetaverse-types", + "-p", + "libremetaverse-structured-data", + "-p", + "libremetaverse-imaging", + "-p", + "libremetaverse-prim-mesher", + ], + ), + ], + ), + ( + "benchmarks-and-resource-soak", + vec![ + CommandSpec::new( + "cargo", + &[ + "+1.97.1", + "bench", + "--locked", + "-p", + "libremetaverse-imaging", + "--bench", + "image_pipeline", + "--no-run", + ], + ), + CommandSpec::new( + "cargo", + &[ + "+1.97.1", + "bench", + "--locked", + "-p", + "libremetaverse-prim-mesher", + "--bench", + "meshing", + "--no-run", + ], + ), + CommandSpec::new( + "cargo", + &[ + "+1.97.1", + "run", + "--locked", + "-p", + "metacrate-concurrency-audit", + "--", + "--cycles", + "16", + "--evidence", + "artifacts/release/concurrency-audit.json", + ], + ), + ], + ), + ( + "source-packages-and-native-binaries", + vec![ + CommandSpec::new( + "cargo", + &[ + "+1.97.1", + "package", + "--locked", + "--no-verify", + "-p", + "libremetaverse-types", + "-p", + "libremetaverse-structured-data", + "-p", + "libremetaverse-imaging", + "-p", + "libremetaverse-imaging-skia", + "-p", + "libremetaverse-openjpeg", + "-p", + "libremetaverse-opus", + "-p", + "libremetaverse-prim-mesher", + "-p", + "libremetaverse-lsl-tools", + "-p", + "libremetaverse", + "-p", + "libremetaverse-rendering-simple", + "-p", + "libremetaverse-rendering-mesh-foundry", + "-p", + "libremetaverse-rlv", + "-p", + "libremetaverse-utilities", + "-p", + "libremetaverse-voice-vivox", + "-p", + "libremetaverse-voice-webrtc", + "-p", + "libremetaverse-programs", + ], + ), + CommandSpec::new( + "cargo", + &[ + "+1.97.1", + "build", + "--locked", + "--release", + "-p", + "libremetaverse-programs", + "--bins", + ], + ), + ], + ), + ( + "release-audits", + vec![ + CommandSpec::new( + "cargo", + &[ + "+1.97.1", + "run", + "--locked", + "-p", + "metacrate-ci-matrix", + "--", + "artifact-audit", + "--artifact-dir", + "target/release-gate/release", + "--package-dir", + "target/release-gate/package", + "--evidence", + "artifacts/release/artifact-audit.json", + ], + ), + CommandSpec::new( + "cargo", + &[ + "+1.97.1", + "run", + "--locked", + "-p", + "metacrate-ci-matrix", + "--", + "release-candidate-audit", + "--evidence", + "artifacts/release/release-candidate.json", + ], + ), + ], + ), + ]; + let mut recorded = Vec::new(); + let mut failure = None; + for (id, commands) in stages { + let stage_started = Instant::now(); + let mut status = "ok"; + for spec in &commands { + println!("ci-release {id}: {} {}", spec.program, spec.args.join(" ")); + let command_status = configured_command(root, spec, &environment) + .stdin(Stdio::null()) + .status()?; + if !command_status.success() { + status = "failed"; + failure = Some(format!( + "release stage {id} failed while running {}", + spec.program + )); + break; + } + } + recorded.push(StageEvidence { + id: id.to_owned(), + elapsed_seconds: stage_started.elapsed().as_secs_f64(), + commands: commands.len(), + status, + }); + if failure.is_some() { + break; + } + } + let value = serde_json::json!({ + "schema": 1, + "gate": "release", + "status": if failure.is_none() { "ok" } else { "failed" }, + "source_commit": command_line(root, "git", &["rev-parse", "--verify", "HEAD"]).unwrap_or_else(|_| "unknown".to_owned()), + "recorded_unix_seconds": unix_seconds()?, + "elapsed_seconds": started.elapsed().as_secs_f64(), + "stages": recorded, + "failure": failure, + }); + write_json_evidence(evidence_path, &value)?; + if let Some(message) = value["failure"].as_str() { + return Err(MatrixError::new(message)); + } + Ok(()) +} + +fn run_required( + root: &Path, + manifest: &CoverageManifest, + rust_host: &str, + expected_host: &str, + started: Instant, + evidence: &mut GateEvidence, +) -> Result<()> { + if rust_host != expected_host { + return Err(MatrixError::new(format!( + "required CI must use {expected_host}; rustc reports {rust_host}" + ))); + } + let archive = skia_archive_path()?; + validate_skia_archive(&archive)?; + validate_openjpeg_identity()?; + let skia_url = file_url(&archive)?; + let target_dir = std::env::var_os("CARGO_TARGET_DIR") + .map_or_else(|| root.join("target/required"), PathBuf::from); + let jobs = std::env::var("CARGO_BUILD_JOBS").unwrap_or_else(|_| "2".to_owned()); + let common_env = [ + (OsString::from("CARGO_INCREMENTAL"), OsString::from("0")), + (OsString::from("CARGO_BUILD_JOBS"), OsString::from(jobs)), + ( + OsString::from("CARGO_TARGET_DIR"), + target_dir.into_os_string(), + ), + ( + OsString::from("SKIA_BINARIES_URL"), + OsString::from(skia_url), + ), + ]; + + run_stage( + root, + "static-generated-api", + &static_commands(), + &common_env, + started, + manifest, + evidence, + )?; + run_report_audits(root)?; + evidence.stages.push(StageEvidence { + id: "reviewed-reports".to_owned(), + elapsed_seconds: 0.0, + commands: 4, + status: "ok", + }); + prove_skia_binary(root, &archive, &common_env, evidence)?; + enforce_budget(started, manifest)?; + run_stage( + root, + "compile-test-features", + &test_commands(), + &common_env, + started, + manifest, + evidence, + )?; + run_stage( + root, + "program-api-smoke", + &program_commands(), + &common_env, + started, + manifest, + evidence, + )?; + run_stage( + root, + "clippy-docs", + &quality_commands(), + &common_env, + started, + manifest, + evidence, + )?; + run_stage( + root, + "policy-evidence", + &policy_commands(), + &common_env, + started, + manifest, + evidence, + )?; + Ok(()) +} + +fn static_commands() -> Vec { + vec![ + CommandSpec::new("cargo", &["fmt", "--all", "--", "--check"]), + CommandSpec::new("python3", &["tools/generate_rust_mapping.py", "--check"]), + CommandSpec::new("python3", &["tools/generate_api_shims.py", "--check"]), + CommandSpec::new("python3", &["tools/check_api_coverage.py"]), + CommandSpec::new("python3", &["tools/check_test_parity.py"]), + CommandSpec::new("python3", &["tools/generate_lsl_tables.py", "--check"]), + CommandSpec::new("python3", &["tools/check_milestone_06.py"]), + CommandSpec::new("python3", &["tools/check_milestone_09.py"]), + CommandSpec::new("python3", &["tools/check_milestone_10.py"]), + CommandSpec::new( + "cargo", + &["test", "--locked", "-p", "libremetaverse-codegen"], + ), + CommandSpec::new( + "cargo", + &[ + "run", + "--locked", + "-p", + "libremetaverse-codegen", + "--", + "check", + ], + ), + ] +} + +fn test_commands() -> Vec { + vec![ + // Build every product/compatibility library and integration test in + // one graph. Package-qualified feature selection covers every core + // feature without enabling unrelated optional features on consumers. + CommandSpec::new( + "cargo", + &[ + "test", + "--workspace", + "--lib", + "--test", + "*", + "--features", + "libremetaverse/jpeg2000,libremetaverse/vorbis", + "--exclude", + "libremetaverse-programs", + "--exclude", + "libremetaverse-codegen", + "--exclude", + "metacrate-performance", + "--exclude", + "metacrate-ci-matrix", + "--exclude", + "metacrate-concurrency-audit", + "--jobs", + "1", + "--locked", + ], + ), + CommandSpec::new( + "cargo", + &[ + "check", + "-p", + "libremetaverse", + "--lib", + "--no-default-features", + "--locked", + ], + ), + CommandSpec::new( + "cargo", + &[ + "check", + "-p", + "libremetaverse-voice-webrtc", + "--all-targets", + "--features", + "real-audio", + "--locked", + ], + ), + CommandSpec::new( + "cargo", + &[ + "test", + "-p", + "libremetaverse-imaging", + "--no-default-features", + "--features", + "jpeg2000", + "--locked", + ], + ), + ] +} + +fn program_commands() -> Vec { + vec![ + CommandSpec::new( + "cargo", + &[ + "test", + "--locked", + "-p", + "libremetaverse-programs", + "--test", + "*", + ], + ), + CommandSpec::new( + "cargo", + &[ + "run", + "--locked", + "-p", + "libremetaverse-programs", + "--bin", + "live-grid-smoke", + "--", + "--audit-only", + ], + ), + CommandSpec::new( + "cargo", + &[ + "run", + "--locked", + "-p", + "libremetaverse-programs", + "--bin", + "live-grid-smoke", + "--", + "--fake", + "--evidence", + "artifacts/ci/fake-grid-smoke.jsonl", + ], + ), + CommandSpec::new( + "cargo", + &[ + "check", + "--locked", + "--manifest-path", + "tests/api-compile/Cargo.toml", + ], + ), + CommandSpec::new( + "cargo", + &[ + "run", + "--locked", + "--manifest-path", + "tests/semver-port/Cargo.toml", + ], + ), + ] +} + +fn quality_commands() -> Vec { + vec![ + CommandSpec::new( + "cargo", + &[ + "clippy", + "--workspace", + "--all-targets", + "--all-features", + "--locked", + "--", + "-D", + "warnings", + ], + ), + CommandSpec::new( + "cargo", + &[ + "doc", + "--workspace", + "--all-features", + "--no-deps", + "--locked", + ], + ), + ] +} + +fn policy_commands() -> Vec { + vec![ + CommandSpec::new( + "cargo", + &[ + "deny", + "check", + "advisories", + "licenses", + "bans", + "sources", + "--hide-inclusion-graph", + ], + ), + // Invoke the pinned standalone executable directly. In a `cargo run` + // child process, some Cargo installations forward the plugin name to + // cargo-machete, which 0.9.2 interprets as a directory to scan. + CommandSpec::new("cargo-machete", &["--with-metadata"]), + CommandSpec::new( + "cargo", + &[ + "run", + "--locked", + "-p", + "metacrate-performance", + "--", + "audit", + "--fixture-root", + "benchmarks/fixtures", + "--rust", + "benchmarks/results/rust-linux-x86_64.json", + "--reference", + "benchmarks/results/csharp-linux-x86_64.json", + "--comparison", + "benchmarks/results/comparison.json", + ], + ), + ] +} + +fn run_stage( + root: &Path, + id: &str, + commands: &[CommandSpec], + environment: &[(OsString, OsString)], + gate_started: Instant, + manifest: &CoverageManifest, + evidence: &mut GateEvidence, +) -> Result<()> { + enforce_budget(gate_started, manifest)?; + let stage_started = Instant::now(); + for command in commands { + println!( + "ci-required {id}: {} {}", + command.program, + command.args.join(" ") + ); + let status = configured_command(root, command, environment) + .stdin(Stdio::null()) + .status()?; + if !status.success() { + evidence.stages.push(StageEvidence { + id: id.to_owned(), + elapsed_seconds: stage_started.elapsed().as_secs_f64(), + commands: commands.len(), + status: "failed", + }); + return Err(MatrixError::new(format!( + "stage {id} failed while running {}", + command.program + ))); + } + enforce_budget(gate_started, manifest)?; + } + evidence.stages.push(StageEvidence { + id: id.to_owned(), + elapsed_seconds: stage_started.elapsed().as_secs_f64(), + commands: commands.len(), + status: "ok", + }); + Ok(()) +} + +fn prove_skia_binary( + root: &Path, + archive: &Path, + environment: &[(OsString, OsString)], + evidence: &mut GateEvidence, +) -> Result<()> { + let proof_target = environment + .iter() + .find(|(name, _)| name == "CARGO_TARGET_DIR") + .map(|(_, value)| PathBuf::from(value)) + .ok_or_else(|| MatrixError::new("CARGO_TARGET_DIR is required for the Skia proof"))?; + let started = Instant::now(); + let spec = CommandSpec::new( + "cargo", + &[ + "test", + "-vv", + "--locked", + "-p", + "libremetaverse-imaging-skia", + "--no-default-features", + "--features", + "skia", + ], + ); + let mut command = configured_command(root, &spec, environment); + command + .env("CARGO_TARGET_DIR", &proof_target) + .env("SKIA_BINARIES_URL", file_url(archive)?) + .env_remove("FORCE_SKIA_BINARIES_DOWNLOAD") + .env_remove("FORCE_SKIA_BUILD") + .stdin(Stdio::null()); + let output = command.output()?; + std::io::stdout().write_all(&output.stdout)?; + std::io::stderr().write_all(&output.stderr)?; + let mut log = format!( + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + append_skia_build_output(&proof_target, &mut log)?; + evidence.skia.download_and_install_succeeded = log.contains(SKIA_SUCCESS); + evidence.skia.source_build_started = log.contains(SKIA_SOURCE_BUILD); + let status = if output.status.success() + && evidence.skia.download_and_install_succeeded + && !evidence.skia.source_build_started + { + "ok" + } else { + "failed" + }; + evidence.stages.push(StageEvidence { + id: "skia-binary-proof".to_owned(), + elapsed_seconds: started.elapsed().as_secs_f64(), + commands: 1, + status, + }); + if status != "ok" { + return Err(MatrixError::new( + "Skia proof did not install the pinned binary archive without starting a source build", + )); + } + Ok(()) +} + +fn append_skia_build_output(target_dir: &Path, log: &mut String) -> Result<()> { + let build_dir = target_dir.join("debug/build"); + if !build_dir.is_dir() { + return Ok(()); + } + for entry in fs::read_dir(build_dir)? { + let entry = entry?; + if !entry + .file_name() + .to_string_lossy() + .starts_with("skia-bindings-") + { + continue; + } + let output = entry.path().join("output"); + if output.is_file() { + log.push('\n'); + log.push_str(&fs::read_to_string(output)?); + } + } + Ok(()) +} + +fn run_report_audits(root: &Path) -> Result<()> { + write_api_baseline(root)?; + write_documentation_report(root)?; + ensure_git_clean_paths( + root, + &[ + "api/SEMVER-BASELINE.json", + "api/SEMVER-AUDIT.md", + "api/DOCUMENTATION-COVERAGE.md", + ], + )?; + let artifacts = root.join("artifacts/ci"); + fs::create_dir_all(&artifacts)?; + let audits = [ + ("api-audit.json", "ci/evidence/api-audit.json"), + ( + "documentation-audit.json", + "ci/evidence/documentation-audit.json", + ), + ("dependency-audit.json", ""), + ("provenance-audit.json", "ci/evidence/provenance-audit.json"), + ]; + for (name, _) in audits { + let path = artifacts.join(name); + if path.exists() { + fs::remove_file(path)?; + } + } + audit_api_surface(root, &artifacts.join("api-audit.json"))?; + audit_documentation(root, &artifacts.join("documentation-audit.json"))?; + audit_dependencies(root, &artifacts.join("dependency-audit.json"))?; + audit_provenance(root, &artifacts.join("provenance-audit.json"))?; + for (name, committed) in audits.into_iter().filter(|(_, path)| !path.is_empty()) { + if !evidence_matches_except_timestamp(&artifacts.join(name), &root.join(committed))? { + return Err(MatrixError::new(format!( + "{committed} does not match freshly generated audit evidence" + ))); + } + } + Ok(()) +} + +fn evidence_matches_except_timestamp(actual: &Path, expected: &Path) -> Result { + let mut actual: serde_json::Value = serde_json::from_slice(&fs::read(actual)?)?; + let mut expected: serde_json::Value = serde_json::from_slice(&fs::read(expected)?)?; + for value in [&mut actual, &mut expected] { + if let Some(object) = value.as_object_mut() { + for volatile in [ + "recorded_unix_seconds", + "source_file_count", + "distribution_manifest_sha256", + ] { + object.remove(volatile); + } + } + } + Ok(actual == expected) +} + +fn validate_coverage(manifest: &CoverageManifest) -> Result<()> { + if manifest.schema != 1 + || manifest.required_workflow != REQUIRED_WORKFLOW + || manifest.release_workflow != RELEASE_WORKFLOW + || manifest.hard_timeout_minutes != 15 + || manifest.internal_target_seconds > 720 + || manifest.legacy_workflows.len() != LEGACY_WORKFLOW_COUNT + { + return Err(MatrixError::new( + "CI coverage manifest has an invalid gate shape", + )); + } + let required = unique( + manifest.required_checks.iter().map(String::as_str), + "required check", + )?; + let workflows = unique( + manifest + .legacy_workflows + .iter() + .map(|entry| entry.workflow.as_str()), + "legacy workflow", + )?; + if workflows != BTreeSet::from(LEGACY_WORKFLOWS) { + return Err(MatrixError::new("legacy workflow inventory is incomplete")); + } + let mut routed_required = BTreeSet::new(); + for entry in &manifest.legacy_workflows { + if entry.responsibility.trim().is_empty() + || entry.rationale.trim().is_empty() + || entry.checks.is_empty() + || !matches!( + entry.destination.as_str(), + "required" | "release" | "split" | "retired" + ) + { + return Err(MatrixError::new(format!( + "{} has an incomplete coverage route", + entry.workflow + ))); + } + if matches!(entry.destination.as_str(), "required" | "split") { + routed_required.extend( + entry + .checks + .iter() + .filter(|check| required.contains(check.as_str())) + .map(String::as_str), + ); + } + } + if routed_required != required { + return Err(MatrixError::new( + "not every required check is routed from the legacy inventory", + )); + } + Ok(()) +} + +fn validate_workflows(root: &Path, manifest: &CoverageManifest) -> Result<()> { + let directory = root.join(".gitea/workflows"); + let mut workflow_paths = Vec::new(); + for entry in fs::read_dir(&directory)? { + let path = entry?.path(); + if matches!( + path.extension().and_then(OsStr::to_str), + Some("yml" | "yaml") + ) { + workflow_paths.push(path); + } + } + workflow_paths.sort(); + if workflow_paths.len() > 3 || workflow_paths.len() != 2 { + return Err(MatrixError::new( + "exactly the required and non-routine release workflows must remain", + )); + } + let required = fs::read_to_string(root.join(&manifest.required_workflow))?; + let release = fs::read_to_string(root.join(&manifest.release_workflow))?; + for marker in [ + "push:", + "pull_request:", + "timeout-minutes: 15", + "cancel-in-progress: true", + "runs-on: ubuntu-latest", + "required-gate", + "python3 tools/normalize_git_mtimes.py --state", + ".metacrate-mtimes.json\" --check", + "'**/Cargo.toml'", + "METACRATE_DEPENDENCY_CACHE_HIT", + "cp -al \"$previous_target\" \"$cargo_target\"", + ] { + if !required.contains(marker) { + return Err(MatrixError::new(format!( + "required workflow is missing {marker}" + ))); + } + } + if required.contains("cargo clean --target-dir") { + return Err(MatrixError::new( + "cold dependency/native validation must retain the keyed compiled target", + )); + } + if required.contains("rm -rf -- \"$CARGO_HOME/registry\"") { + return Err(MatrixError::new( + "cold validation must preserve immutable extracted sources for Cargo fingerprints", + )); + } + if required.contains("git show -s --format=%ct HEAD") { + return Err(MatrixError::new( + "tracked mtimes must be blob-stable across commits, not commit-wide", + )); + } + if release.contains("pull_request:") || !release.contains("workflow_dispatch:") { + return Err(MatrixError::new( + "release workflow must be explicitly non-routine", + )); + } + for path in &workflow_paths { + let text = fs::read_to_string(path)?; + if text + .lines() + .filter_map(|line| line.trim().strip_prefix("runs-on:")) + .any(|runner| runner.trim() != "ubuntu-latest") + { + return Err(MatrixError::new(format!( + "{} violates the ubuntu-only runner policy", + path.display() + ))); + } + if text.contains("FORCE_SKIA_BINARIES_DOWNLOAD") { + return Err(MatrixError::new(format!( + "{} misuses FORCE_SKIA_BINARIES_DOWNLOAD", + path.display() + ))); + } + } + Ok(()) +} + +fn validate_skia_configuration(root: &Path) -> Result<()> { + let manifest = fs::read_to_string(root.join("crates/libremetaverse-imaging-skia/Cargo.toml"))?; + if manifest.matches("no-compile").count() < 3 || !manifest.contains("binary-cache") { + return Err(MatrixError::new( + "every Skia target must fail fast instead of compiling from source", + )); + } + Ok(()) +} + +fn validate_skia_archive(path: &Path) -> Result<()> { + if path.file_name().and_then(OsStr::to_str) != Some(SKIA_ARCHIVE) { + return Err(MatrixError::new( + "Skia archive name does not select the pinned ARM64 GNU feature tuple", + )); + } + if hash_file(path)? != SKIA_SHA256 { + return Err(MatrixError::new( + "Skia archive SHA-256 does not match the reviewed binary", + )); + } + Ok(()) +} + +fn validate_openjpeg_identity() -> Result<()> { + let prefix = std::env::var_os("OPENJPEG_PREFIX") + .map(PathBuf::from) + .ok_or_else(|| MatrixError::new("OPENJPEG_PREFIX is required"))?; + let identity = fs::read_to_string(prefix.join("metacrate-openjpeg.identity"))?; + for expected in [ + "version=2.5.4", + "commit=6c4a29b00211eb0430fa0e5e890f1ce5c80f409f", + ] { + if !identity.lines().any(|line| line == expected) { + return Err(MatrixError::new(format!( + "OpenJPEG cache identity is missing {expected}" + ))); + } + } + Ok(()) +} + +fn coverage(root: &Path) -> Result { + Ok(serde_json::from_slice(&fs::read( + root.join(COVERAGE_PATH), + )?)?) +} + +fn configured_command( + root: &Path, + spec: &CommandSpec, + environment: &[(OsString, OsString)], +) -> Command { + let program: OsString = if spec.program == "cargo" { + std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()) + } else { + spec.program.into() + }; + let mut command = Command::new(program); + command.args(spec.args).current_dir(root); + for (key, value) in environment { + command.env(key, value); + } + command +} + +fn enforce_budget(started: Instant, manifest: &CoverageManifest) -> Result<()> { + if started.elapsed().as_secs() >= manifest.internal_target_seconds { + return Err(MatrixError::new(format!( + "required CI exceeded its {} second internal target", + manifest.internal_target_seconds + ))); + } + Ok(()) +} + +fn rust_host(root: &Path) -> Result { + let verbose = command_line(root, "rustc", &["-vV"])?; + verbose + .lines() + .find_map(|line| line.strip_prefix("host: ").map(str::to_owned)) + .ok_or_else(|| MatrixError::new("rustc -vV did not report a host")) +} + +fn command_line(root: &Path, program: &str, args: &[&str]) -> Result { + let output = Command::new(program) + .args(args) + .current_dir(root) + .output()?; + if !output.status.success() { + return Err(MatrixError::new(format!("{program} command failed"))); + } + String::from_utf8(output.stdout) + .map(|text| text.trim().to_owned()) + .map_err(|_| MatrixError::new(format!("{program} output was not UTF-8"))) +} + +fn ensure_git_clean_paths(root: &Path, paths: &[&str]) -> Result<()> { + let mut command = Command::new("git"); + command + .args(["diff", "--exit-code", "--"]) + .args(paths) + .current_dir(root); + if !command.status()?.success() { + return Err(MatrixError::new("reviewed generated reports are stale")); + } + Ok(()) +} + +fn skia_archive_path() -> Result { + std::env::var_os("METACRATE_SKIA_ARCHIVE") + .map(PathBuf::from) + .ok_or_else(|| MatrixError::new("METACRATE_SKIA_ARCHIVE is required")) +} + +fn hash_file(path: &Path) -> Result { + let mut file = fs::File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = vec![0_u8; 64 * 1024].into_boxed_slice(); + loop { + let read = file.read(&mut buffer)?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + } + Ok(hasher + .finalize() + .iter() + .fold(String::with_capacity(64), |mut output, byte| { + use std::fmt::Write as _; + write!(output, "{byte:02x}").expect("writing to a String cannot fail"); + output + })) +} + +fn file_url(path: &Path) -> Result { + let absolute = path.canonicalize()?; + let text = absolute + .to_str() + .ok_or_else(|| MatrixError::new("Skia archive path is not UTF-8"))? + .replace('\\', "/"); + if text.starts_with('/') { + Ok(format!("file://{text}")) + } else { + Ok(format!("file:///{text}")) + } +} + +fn cache_hit(name: &str) -> Option { + std::env::var(name) + .ok() + .and_then(|value| match value.as_str() { + "true" => Some(true), + "false" => Some(false), + _ => None, + }) +} + +fn peak_memory_kib() -> Option { + if let Ok(bytes) = fs::read_to_string("/sys/fs/cgroup/memory.peak") + && let Ok(bytes) = bytes.trim().parse::() + { + return Some(bytes / 1024); + } + fs::read_to_string("/proc/self/status") + .ok() + .and_then(|status| { + status.lines().find_map(|line| { + line.strip_prefix("VmHWM:")? + .split_whitespace() + .next()? + .parse() + .ok() + }) + }) +} + +fn unix_seconds() -> Result { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_secs()) + .map_err(|_| MatrixError::new("system clock predates Unix epoch")) +} + +fn prepare_new_evidence(path: &Path) -> Result<()> { + if path.exists() { + return Err(MatrixError::new(format!( + "{} already exists", + path.display() + ))); + } + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + Ok(()) +} + +fn write_evidence(path: &Path, evidence: &GateEvidence) -> Result<()> { + 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 write_json_evidence(path: &Path, evidence: &serde_json::Value) -> Result<()> { + 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 unique<'a>(values: impl Iterator, kind: &str) -> Result> { + let mut result = BTreeSet::new(); + for value in values { + if value.trim().is_empty() || !result.insert(value) { + return Err(MatrixError::new(format!( + "{kind} values must be nonempty and unique" + ))); + } + } + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn checked_in_coverage_routes_every_required_check() { + let manifest: CoverageManifest = + serde_json::from_str(include_str!("../../../ci/ci-coverage.json")).unwrap(); + validate_coverage(&manifest).unwrap(); + } + + #[test] + fn checked_in_workflow_split_is_bounded_and_ubuntu_only() { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); + audit_consolidated_ci(&root).unwrap(); + } + + #[test] + fn pinned_skia_archive_identity_is_exact() { + assert!(SKIA_ARCHIVE.contains("aarch64-unknown-linux-gnu")); + assert!(SKIA_ARCHIVE.contains("jpegd-jpege-pdf-svg-textlayout-vulkan-webpd-webpe")); + assert_eq!(SKIA_SHA256.len(), 64); + } + + #[test] + fn product_and_compatibility_graph_is_the_only_serial_command() { + let commands = test_commands(); + assert_eq!( + commands[0].args, + [ + "test", + "--workspace", + "--lib", + "--test", + "*", + "--features", + "libremetaverse/jpeg2000,libremetaverse/vorbis", + "--exclude", + "libremetaverse-programs", + "--exclude", + "libremetaverse-codegen", + "--exclude", + "metacrate-performance", + "--exclude", + "metacrate-ci-matrix", + "--exclude", + "metacrate-concurrency-audit", + "--jobs", + "1", + "--locked", + ] + ); + assert!( + commands[1..] + .iter() + .all(|command| !command.args.windows(2).any(|pair| pair == ["--jobs", "1"])) + ); + } + + #[test] + fn machete_uses_the_standalone_cli_contract() { + let commands = policy_commands(); + let machete = commands + .iter() + .find(|command| command.program == "cargo-machete") + .expect("policy gate invokes cargo-machete directly"); + assert_eq!(machete.args, ["--with-metadata"]); + assert!( + !commands + .iter() + .any(|command| command.program == "cargo" + && command.args.first() == Some(&"machete")) + ); + } + + #[test] + fn performance_audit_reuses_the_required_gate_build_profile() { + let commands = policy_commands(); + let performance = commands + .iter() + .find(|command| { + command.program == "cargo" + && command + .args + .windows(2) + .any(|pair| pair == ["-p", "metacrate-performance"]) + }) + .expect("policy gate runs the performance evidence audit"); + + assert!(performance.args.starts_with(&["run", "--locked"])); + assert!( + !performance.args.contains(&"--profile"), + "the audit must reuse the gate's existing build artifacts instead of compiling a second profile" + ); + } +} diff --git a/tools/ci-matrix/src/lib.rs b/tools/ci-matrix/src/lib.rs index 3a0cc0a..5d3b686 100644 --- a/tools/ci-matrix/src/lib.rs +++ b/tools/ci-matrix/src/lib.rs @@ -13,6 +13,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; mod api_surface; mod artifact; +mod ci_gate; mod dependency; mod documentation; mod provenance; @@ -20,13 +21,14 @@ mod release_candidate; pub use api_surface::{audit_api_surface, write_api_baseline}; pub use artifact::audit_artifacts; +pub use ci_gate::{audit_consolidated_ci, run_release_gate, run_required_gate}; pub use dependency::audit_dependencies; pub use documentation::{audit_documentation, write_documentation_report}; pub use provenance::{audit_provenance, write_provenance_reports}; pub use release_candidate::audit_release_candidate; pub const MATRIX_PATH: &str = "ci/release-matrix.json"; -const WORKFLOW_PATH: &str = ".gitea/workflows/release-matrix.yml"; +const WORKFLOW_PATH: &str = ".gitea/workflows/release.yml"; const REQUIRED_PROFILES: [&str; 7] = [ "linux-msrv-portable", "linux-stable-default", @@ -458,25 +460,16 @@ fn audit_cargo_features(root: &Path) -> Result<()> { 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") + if matrix.profiles.is_empty() + || !workflow.contains("release-gate") + || !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", + "release workflow must invoke the Rust gate and cache Cargo downloads", )); } + audit_consolidated_ci(root)?; let workflows = root.join(".gitea/workflows"); for entry in fs::read_dir(workflows)? { let path = entry?.path(); diff --git a/tools/ci-matrix/src/main.rs b/tools/ci-matrix/src/main.rs index ccf9c48..bc2b2cf 100644 --- a/tools/ci-matrix/src/main.rs +++ b/tools/ci-matrix/src/main.rs @@ -1,7 +1,8 @@ use metacrate_ci_matrix::{ - audit, audit_api_surface, audit_artifacts, audit_dependencies, audit_documentation, - audit_provenance, audit_release_candidate, load, run, workspace_root, write_api_baseline, - write_documentation_report, write_provenance_reports, + audit, audit_api_surface, audit_artifacts, audit_consolidated_ci, audit_dependencies, + audit_documentation, audit_provenance, audit_release_candidate, load, run, run_release_gate, + run_required_gate, workspace_root, write_api_baseline, write_documentation_report, + write_provenance_reports, }; use std::path::{Path, PathBuf}; @@ -12,12 +13,23 @@ fn main() { } } +#[allow(clippy::too_many_lines)] // Flat CLI command routing is clearer than nested dispatch. fn execute() -> Result<(), Box> { 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("ci-audit") if arguments.next().is_none() => { + audit_consolidated_ci(&root)?; + println!("consolidated CI coverage and workflow split: ok"); + } + Some("required-gate") => { + gate_command(&root, arguments, "required", run_required_gate)?; + } + Some("release-gate") => { + gate_command(&root, arguments, "release", run_release_gate)?; + } Some("audit") if arguments.next().is_none() => { audit(&root, &matrix)?; println!( @@ -101,7 +113,7 @@ fn execute() -> Result<(), Box> { } _ => { return Err( - "usage: ci-matrix audit | run PROFILE --evidence FILE | dependency-audit --evidence FILE | artifact-audit --artifact-dir DIR --package-dir DIR --evidence FILE | release-candidate-audit --evidence FILE | documentation-report | documentation-audit --evidence FILE | api-baseline-write | api-audit --evidence FILE | provenance-report | provenance-audit --evidence FILE" + "usage: ci-matrix ci-audit | required-gate --evidence FILE | release-gate --evidence FILE | audit | run PROFILE --evidence FILE | dependency-audit --evidence FILE | artifact-audit --artifact-dir DIR --package-dir DIR --evidence FILE | release-candidate-audit --evidence FILE | documentation-report | documentation-audit --evidence FILE | api-baseline-write | api-audit --evidence FILE | provenance-report | provenance-audit --evidence FILE" .into(), ); } @@ -109,6 +121,27 @@ fn execute() -> Result<(), Box> { Ok(()) } +fn gate_command( + root: &Path, + mut arguments: impl Iterator, + gate: &str, + run_gate: fn(&Path, &Path) -> metacrate_ci_matrix::Result<()>, +) -> Result<(), Box> { + let flag = arguments + .next() + .ok_or_else(|| format!("{gate}-gate requires --evidence FILE"))?; + let evidence = arguments + .next() + .ok_or_else(|| format!("{gate}-gate requires --evidence FILE"))?; + if flag != "--evidence" || arguments.next().is_some() { + return Err(format!("usage: ci-matrix {gate}-gate --evidence FILE").into()); + } + let evidence = absolute_or_rooted(root, &evidence); + run_gate(root, &evidence)?; + println!("{gate} CI gate: ok ({})", evidence.display()); + Ok(()) +} + fn release_candidate_audit_command( root: &Path, mut arguments: impl Iterator, diff --git a/tools/ci-matrix/src/provenance.rs b/tools/ci-matrix/src/provenance.rs index ea079ce..c31468f 100644 --- a/tools/ci-matrix/src/provenance.rs +++ b/tools/ci-matrix/src/provenance.rs @@ -6,7 +6,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::ffi::OsStr; use std::fmt::Write as _; use std::fs::{self, OpenOptions}; -use std::io::Write as _; +use std::io::{Read as _, Write as _}; use std::path::{Component, Path, PathBuf}; use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; @@ -859,6 +859,20 @@ fn license_identifiers(value: &str) -> BTreeSet<&str> { } fn cargo_metadata(root: &Path) -> Result { + // Fetch without `--target` so Cargo materializes the immutable archives for + // every target represented by Cargo.lock. Reading notice files from those + // archives makes the report independent of the host architecture and of + // build-script changes to extracted registry sources. + let fetch = Command::new(super::cargo_program()) + .args(["fetch", "--locked"]) + .current_dir(root) + .output()?; + if !fetch.status.success() { + return Err(MatrixError::new(format!( + "cargo fetch failed during provenance audit: {}", + String::from_utf8_lossy(&fetch.stderr).trim() + ))); + } let output = Command::new(super::cargo_program()) .args([ "metadata", @@ -916,8 +930,22 @@ fn parse_lock_string(value: &str) -> Result { } fn package_notices(root: &Path, explicit: Option<&str>) -> Result> { + if let Some(archive) = registry_archive(root) { + if !archive.is_file() { + return Err(MatrixError::new(format!( + "registry archive is missing after cargo fetch: {}", + archive.display() + ))); + } + let package = root + .file_name() + .and_then(OsStr::to_str) + .ok_or_else(|| MatrixError::new("registry package path has no UTF-8 name"))?; + return package_notices_from_archive(&archive, package, explicit); + } + let mut candidates = Vec::new(); - collect_notice_files(root, 0, &mut candidates)?; + collect_notice_files(root, &mut candidates)?; if let Some(explicit) = explicit { let path = root.join(explicit); if path.is_file() { @@ -946,31 +974,105 @@ fn package_notices(root: &Path, explicit: Option<&str>) -> Result) -> Result<()> { - if depth > 2 { - return Ok(()); +fn registry_archive(root: &Path) -> Option { + let package = root.file_name()?.to_str()?; + let index = root.parent()?.file_name()?; + let source = root.parent()?.parent()?; + if source.file_name()? != OsStr::new("src") { + return None; } + let registry = source.parent()?; + if registry.file_name()? != OsStr::new("registry") { + return None; + } + Some( + registry + .join("cache") + .join(index) + .join(format!("{package}.crate")), + ) +} + +fn package_notices_from_archive( + archive: &Path, + package: &str, + explicit: Option<&str>, +) -> Result> { + let decoder = flate2::read::GzDecoder::new(fs::File::open(archive)?); + let mut archive = tar::Archive::new(decoder); + let mut notices = Vec::new(); + for entry in archive.entries()? { + let mut entry = entry?; + if !entry.header().entry_type().is_file() { + continue; + } + let path = entry.path()?; + let mut components = path.components(); + if components.next() != Some(Component::Normal(OsStr::new(package))) { + return Err(MatrixError::new(format!( + "package archive entry is outside its {package} root: {}", + path.display() + ))); + } + let relative = components.collect::(); + let relative_text = relative + .to_str() + .ok_or_else(|| MatrixError::new("package archive contains a non-UTF-8 path"))?; + validate_relative(relative_text)?; + let depth = relative.components().count(); + let selected = explicit == Some(relative_text) + || (depth <= 3 + && relative + .file_name() + .and_then(OsStr::to_str) + .is_some_and(is_notice_name)); + if !selected { + continue; + } + let size = entry.header().size()?; + if size > 2 * 1024 * 1024 { + return Err(MatrixError::new(format!( + "package notice is unexpectedly large: {relative_text}" + ))); + } + let mut contents = String::new(); + entry.read_to_string(&mut contents).map_err(|error| { + MatrixError::new(format!( + "package notice is not UTF-8 ({relative_text}): {error}" + )) + })?; + notices.push((relative_text.to_owned(), normalize_notice_text(&contents))); + } + notices.sort_by(|left, right| left.0.cmp(&right.0)); + notices.dedup_by(|left, right| left.0 == right.0); + Ok(notices) +} + +fn is_notice_name(name: &str) -> bool { + let name = name.to_ascii_lowercase(); + [ + "license", + "licence", + "copying", + "notice", + "copyright", + "unlicense", + ] + .iter() + .any(|prefix| name.starts_with(prefix)) +} + +fn collect_notice_files(directory: &Path, output: &mut Vec) -> Result<()> { + // Cargo build scripts may populate nested source directories (rust-skia + // creates `skia/` in its registry checkout). Only package-root notices and + // Cargo's explicit `license_file` are immutable package metadata. let mut entries = fs::read_dir(directory)?.collect::>>()?; entries.sort_by_key(std::fs::DirEntry::file_name); for entry in entries { let path = entry.path(); let kind = entry.file_type()?; - let name = entry.file_name().to_string_lossy().to_ascii_lowercase(); - if kind.is_file() - && [ - "license", - "licence", - "copying", - "notice", - "copyright", - "unlicense", - ] - .iter() - .any(|prefix| name.starts_with(prefix)) - { + if kind.is_file() && is_notice_name(&entry.file_name().to_string_lossy()) { output.push(path); - } else if kind.is_dir() && depth < 2 { - collect_notice_files(&path, depth + 1, output)?; } } Ok(()) @@ -1095,6 +1197,10 @@ fn source_paths(root: &Path) -> Result> { .map_err(|_| MatrixError::new("source archive contains a non-UTF-8 path")) }) .collect::>>()?; + // `git ls-files --cached` retains index entries for working-tree deletions + // until the consolidation commit is created. A source distribution always + // describes files that actually exist, including during a pre-commit audit. + paths.retain(|path| root.join(path).is_file()); for path in &paths { validate_relative(path)?; } @@ -1126,8 +1232,18 @@ fn compare_report(root: &Path, relative: &str, expected: &[u8]) -> Result<()> { MatrixError::new(format!("generated report {}: {error}", path.display())) })?; if actual != expected { + if let Some(directory) = std::env::var_os("METACRATE_PROVENANCE_DIAGNOSTICS_DIR") { + let directory = PathBuf::from(directory); + fs::create_dir_all(&directory)?; + let name = path + .file_name() + .ok_or_else(|| MatrixError::new("generated report path has no file name"))?; + fs::write(directory.join(name), expected)?; + } return Err(MatrixError::new(format!( - "{relative} is stale; run `cargo run --locked -p metacrate-ci-matrix -- provenance-report`" + "{relative} is stale (checked-in sha256 {}, generated sha256 {}); run `cargo run --locked -p metacrate-ci-matrix -- provenance-report`", + sha256(&actual), + sha256(expected), ))); } Ok(()) @@ -1253,6 +1369,106 @@ version = "0.0.1" ); } + #[test] + fn package_notice_scan_ignores_build_generated_subtrees() { + let root = + std::env::temp_dir().join(format!("metacrate-package-notices-{}", std::process::id())); + let _ = fs::remove_dir_all(&root); + fs::create_dir_all(root.join("generated")).unwrap(); + fs::write(root.join("LICENSE"), "package license\n").unwrap(); + fs::write(root.join("generated/LICENSE"), "generated license\n").unwrap(); + fs::write(root.join("generated/NOTICE.explicit"), "explicit notice\n").unwrap(); + + let notices = package_notices(&root, Some("generated/NOTICE.explicit")).unwrap(); + assert_eq!( + notices + .iter() + .map(|(path, _)| path.as_str()) + .collect::>(), + ["LICENSE", "generated/NOTICE.explicit"] + ); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn package_notice_scan_preserves_nested_files_from_registry_archive() { + let base = + std::env::temp_dir().join(format!("metacrate-package-archive-{}", std::process::id())); + let root = base.join("registry/src/index/demo-1.0.0"); + let archive_path = base.join("registry/cache/index/demo-1.0.0.crate"); + let _ = fs::remove_dir_all(&base); + fs::create_dir_all(root.join("generated")).unwrap(); + fs::create_dir_all(archive_path.parent().unwrap()).unwrap(); + fs::write(root.join("generated/LICENSE"), "build output\n").unwrap(); + + let encoder = flate2::write::GzEncoder::new( + fs::File::create(&archive_path).unwrap(), + flate2::Compression::default(), + ); + let mut archive = tar::Builder::new(encoder); + for (path, contents) in [ + ("demo-1.0.0/LICENSE", "package license\n"), + ("demo-1.0.0/vendor/COPYING", "vendored license\n"), + ("demo-1.0.0/docs/legal/terms.txt", "explicit terms\n"), + ] { + let mut header = tar::Header::new_gnu(); + header.set_size(contents.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + archive + .append_data(&mut header, path, contents.as_bytes()) + .unwrap(); + } + archive.into_inner().unwrap().finish().unwrap(); + + let notices = package_notices(&root, Some("docs/legal/terms.txt")).unwrap(); + assert_eq!( + notices + .iter() + .map(|(path, _)| path.as_str()) + .collect::>(), + ["LICENSE", "docs/legal/terms.txt", "vendor/COPYING"] + ); + fs::remove_dir_all(base).unwrap(); + } + + #[test] + fn registry_notice_scan_requires_the_fetched_archive() { + let base = std::env::temp_dir().join(format!( + "metacrate-missing-package-archive-{}", + std::process::id() + )); + let root = base.join("registry/src/index/demo-1.0.0"); + let _ = fs::remove_dir_all(&base); + fs::create_dir_all(&root).unwrap(); + fs::write(root.join("LICENSE"), "package license\n").unwrap(); + + let error = package_notices(&root, None).unwrap_err(); + assert!( + error.to_string().contains("registry archive is missing"), + "unexpected error: {error}" + ); + fs::remove_dir_all(base).unwrap(); + } + + #[test] + fn non_registry_sources_keep_the_extracted_notice_fallback() { + let root = std::env::temp_dir().join(format!( + "metacrate-git-package-notices-{}", + std::process::id() + )); + let _ = fs::remove_dir_all(&root); + fs::create_dir_all(&root).unwrap(); + fs::write(root.join("LICENSE"), "package license\n").unwrap(); + + let notices = package_notices(&root, None).unwrap(); + assert_eq!( + notices, + [("LICENSE".to_owned(), "package license\n".to_owned())] + ); + fs::remove_dir_all(root).unwrap(); + } + #[test] fn unsafe_paths_are_rejected() { for path in ["", "../escape", "a/../b", "/absolute"] { diff --git a/tools/install_openjpeg_2_5_4.sh b/tools/install_openjpeg_2_5_4.sh index d38defc..dc4ad82 100755 --- a/tools/install_openjpeg_2_5_4.sh +++ b/tools/install_openjpeg_2_5_4.sh @@ -13,6 +13,8 @@ fi install_prefix=$1 pkg_config_path="$install_prefix/lib/pkgconfig" library_path="$install_prefix/lib" +identity_path="$install_prefix/metacrate-openjpeg.identity" +target_arch=$(uname -m) persist_actions_environment() { if [ -z "${GITHUB_ENV:-}" ]; then @@ -33,6 +35,10 @@ persist_actions_environment() { } if [ -f "$pkg_config_path/libopenjp2.pc" ] && + [ -f "$identity_path" ] && + [ "$(sed -n '1p' "$identity_path")" = "version=$OPENJPEG_VERSION" ] && + [ "$(sed -n '2p' "$identity_path")" = "commit=$OPENJPEG_COMMIT" ] && + [ "$(sed -n '3p' "$identity_path")" = "architecture=$target_arch" ] && PKG_CONFIG_PATH="$pkg_config_path" pkg-config --exact-version "$OPENJPEG_VERSION" libopenjp2 then persist_actions_environment @@ -64,5 +70,11 @@ cmake -S "$source_dir" -B "$build_dir" \ cmake --build "$build_dir" --parallel cmake --install "$build_dir" +{ + printf 'version=%s\n' "$OPENJPEG_VERSION" + printf 'commit=%s\n' "$OPENJPEG_COMMIT" + printf 'architecture=%s\n' "$target_arch" +} >"$identity_path" + PKG_CONFIG_PATH="$pkg_config_path" pkg-config --exact-version "$OPENJPEG_VERSION" libopenjp2 persist_actions_environment diff --git a/tools/normalize_git_mtimes.py b/tools/normalize_git_mtimes.py new file mode 100644 index 0000000..e164bed --- /dev/null +++ b/tools/normalize_git_mtimes.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Give tracked files stable mtimes derived from their immutable Git blobs.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +from pathlib import Path, PurePath +import subprocess +import tempfile +import time + + +WINDOWS_TICK_NANOSECONDS = 100 + + +def tracked_entries(root: Path) -> list[tuple[bytes, Path]]: + output = subprocess.run( + ["git", "ls-files", "--stage", "-z"], + cwd=root, + check=True, + stdout=subprocess.PIPE, + ).stdout + entries: list[tuple[bytes, Path]] = [] + for record in output.split(b"\0"): + if not record: + continue + metadata, separator, raw_path = record.partition(b"\t") + if not separator: + raise RuntimeError("git ls-files produced a record without a path") + mode, _object_id, stage = metadata.split(b" ") + if stage != b"0": + raise RuntimeError( + f"cannot normalize an unmerged index entry: {os.fsdecode(raw_path)}" + ) + relative = PurePath(os.fsdecode(raw_path)) + if relative.is_absolute() or ".." in relative.parts: + raise RuntimeError(f"unsafe tracked path: {relative}") + entries.append((mode, root.joinpath(*relative.parts))) + return entries + + +def content_identity(mode: bytes, path: Path) -> str: + digest = hashlib.sha256(mode + b"\0") + if path.is_symlink(): + digest.update(os.fsencode(os.readlink(path))) + else: + with path.open("rb") as source: + while chunk := source.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def default_state_path(root: Path) -> Path: + return root / "target" / ".metacrate-mtimes.json" + + +def read_state(path: Path) -> dict[str, dict[str, int | str]]: + if not path.is_file(): + return {} + value = json.loads(path.read_text(encoding="utf-8")) + if value.get("schema") != 1 or not isinstance(value.get("files"), dict): + raise RuntimeError(f"invalid tracked-mtime state: {path}") + return value["files"] + + +def write_state(path: Path, files: dict[str, dict[str, int | str]]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp") + temporary.write_text( + json.dumps({"schema": 1, "files": files}, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + os.replace(temporary, path) + + +def normalize(root: Path, state_path: Path, check: bool) -> int: + previous = read_state(state_path) + current: dict[str, dict[str, int | str]] = {} + changed = 0 + prior_max = max( + (int(value["mtime_ns"]) for value in previous.values()), default=0 + ) + wall_time = time.time_ns() // WINDOWS_TICK_NANOSECONDS * WINDOWS_TICK_NANOSECONDS + now = max(wall_time, prior_max + WINDOWS_TICK_NANOSECONDS) + for mode, path in tracked_entries(root): + if not (path.is_file() or path.is_symlink()): + continue + relative = path.relative_to(root).as_posix() + identity = content_identity(mode, path) + prior = previous.get(relative) + expected = ( + int(prior["mtime_ns"]) + if prior is not None and prior.get("identity") == identity + else now + ) + current[relative] = {"identity": identity, "mtime_ns": expected} + actual = os.stat(path, follow_symlinks=False).st_mtime_ns + if actual == expected: + continue + if check: + raise RuntimeError(f"tracked mtime is not normalized: {path}") + os.utime(path, ns=(expected, expected), follow_symlinks=False) + changed += 1 + if not check: + write_state(state_path, current) + return changed + + +def self_test() -> None: + with tempfile.TemporaryDirectory(prefix="metacrate-mtimes-") as directory: + root = Path(directory) + subprocess.run(["git", "init", "--quiet"], cwd=root, check=True) + subprocess.run( + ["git", "config", "user.email", "ci@example.invalid"], cwd=root, check=True + ) + subprocess.run( + ["git", "config", "user.name", "MetaCrate CI"], cwd=root, check=True + ) + first = root / "first.txt" + second = root / "second.txt" + first.write_text("first\n", encoding="utf-8") + second.write_text("second\n", encoding="utf-8") + subprocess.run(["git", "add", "."], cwd=root, check=True) + subprocess.run(["git", "commit", "--quiet", "-m", "fixture"], cwd=root, check=True) + state = root / "state.json" + + assert normalize(root, state, False) == 2 + assert normalize(root, state, True) == 0 + first_mtime = first.stat().st_mtime_ns + second_mtime = second.stat().st_mtime_ns + + first.write_text("changed\n", encoding="utf-8") + assert normalize(root, state, False) == 1 + assert first.stat().st_mtime_ns > first_mtime + assert second.stat().st_mtime_ns == second_mtime + assert normalize(root, state, True) == 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true") + parser.add_argument("--self-test", action="store_true") + parser.add_argument("--root", type=Path, default=Path.cwd()) + parser.add_argument("--state", type=Path) + args = parser.parse_args() + if args.self_test: + self_test() + print("tracked Git mtime self-test: ok") + return 0 + root = args.root.resolve() + state_path = args.state or default_state_path(root) + if not state_path.is_absolute(): + state_path = root / state_path + changed = normalize(root, state_path, args.check) + verb = "verified" if args.check else "normalized" + print(f"{verb} tracked Git mtimes ({changed} changed)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())