commit 1e37955cda5c65691fffe3264c8b21aa11c150e1
parent 205ba4d6eb71100a28f4d8cf47ee613b42a9938d
Author: triesap <tyson@radroots.org>
Date: Thu, 6 Aug 2026 00:50:17 +0000
build: govern catalog-driven artifact controls
- Drive Cargo, Nix, coverage, and preflight from catalog groups.
- Add typed source locks, exact-revision caches, and offline verification.
- Add atomic artifact manifests and immutable source archive controls.
- Qualify deterministic generation, Nix source exports, and negative paths.
Diffstat:
13 files changed, 2087 insertions(+), 86 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -88,6 +88,56 @@ dependencies = [
]
[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -462,12 +512,58 @@ dependencies = [
]
[[package]]
+name = "clap"
+version = "4.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.2",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
name = "cmov"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1644,6 +1740,12 @@ dependencies = [
]
[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2187,6 +2289,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3668,6 +3776,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4117,6 +4231,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
name = "uuid"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4731,6 +4851,7 @@ dependencies = [
name = "xtask"
version = "0.1.0-alpha"
dependencies = [
+ "clap",
"dto_bindgen_core",
"fs2",
"hex",
diff --git a/Cargo.toml b/Cargo.toml
@@ -38,6 +38,26 @@ members = [
"crates/mesh_agent_proto",
"tools/xtask",
]
+default-members = [
+ "crates/core",
+ "crates/identity",
+ "crates/blossom",
+ "crates/protocol",
+ "crates/event",
+ "crates/event_codec",
+ "crates/trade",
+ "crates/signing",
+ "crates/transport",
+ "crates/nostr",
+ "crates/nostr_connect",
+ "crates/secrets",
+ "crates/storage",
+ "crates/storage_sqlite",
+ "crates/transport_nostr",
+ "crates/sync",
+ "crates/geonames",
+ "tools/xtask",
+]
resolver = "3"
[workspace.package]
diff --git a/build/nix/checks.nix b/build/nix/checks.nix
@@ -8,7 +8,8 @@ let
pname = "radroots-cargo-check";
doCheck = false;
buildPhaseCargoCommand = ''
- cargo check --workspace --all-targets
+ cargo check --locked --all-targets ${common.publicNativeCargoArgs}
+ cargo check --locked --all-targets ${common.previewCargoArgs}
'';
installPhaseCommand = "mkdir -p $out";
}
@@ -20,7 +21,10 @@ let
pname = "radroots-cargo-test";
doCheck = false;
buildPhaseCargoCommand = ''
- cargo test ${common.coreContractCargoArgs}
+ cargo test --locked ${common.coreContractCargoArgs}
+ cargo test --locked ${common.previewCargoArgs}
+ cargo clippy --locked --all-targets ${common.publicNativeCargoArgs} -- -D warnings
+ cargo clippy --locked --all-targets ${common.previewCargoArgs} -- -D warnings
'';
installPhaseCommand = "mkdir -p $out";
}
@@ -194,7 +198,7 @@ in
pname = "radroots-architecture";
doCheck = false;
buildPhaseCargoCommand = ''
- cargo run --locked -q -p xtask -- architecture-ci
+ cargo run --locked -q -p xtask -- architecture-source-export-ci
'';
installPhaseCommand = "mkdir -p $out";
}
diff --git a/build/nix/common.nix b/build/nix/common.nix
@@ -7,6 +7,18 @@
let
root = ../..;
cargoToml = builtins.fromTOML (builtins.readFile ../../Cargo.toml);
+ packageGroupProjection = builtins.fromTOML (
+ builtins.readFile ../../contracts/crates/generated/package_groups.v1.toml
+ );
+ packageGroups = builtins.listToAttrs (
+ map (group: {
+ name = group.id;
+ value = group.active_packages;
+ }) packageGroupProjection.group
+ );
+ cargoArgsFor = packages: lib.concatStringsSep " " (map (package: "-p ${package}") packages);
+ publicNativeCargoArgs = cargoArgsFor packageGroups.public_native;
+ previewCargoArgs = cargoArgsFor packageGroups.preview;
version = cargoToml.workspace.package.version;
darwinBuildInputs = lib.optionals pkgs.stdenv.isDarwin [
pkgs.libiconv
@@ -22,14 +34,16 @@ let
../../CHANGELOG.md
../../LICENSE-APACHE
../../LICENSE-MIT
- ../../README
+ ../../README.md
../../dto_bindgen.toml
../../rust-toolchain.toml
../../contracts
../../crates
+ ../../docs/api
../../docs/decisions
../../docs/implementation
../../docs/specs
+ ../../fuzz
../../tools
]
);
@@ -93,23 +107,7 @@ let
cargoLlvmCov
];
releaseRuntimeInputs = coverageRuntimeInputs;
- coreContractCrates = [
- "xtask"
- "radroots_blossom"
- "radroots_core"
- "radroots_event"
- "radroots_trade"
- "radroots_identity"
- "radroots_replica_schema"
- "radroots_event_codec"
- "radroots_event_store"
- "radroots_nostr"
- "radroots_nostr_connect"
- "radroots_nostr_signer"
- ];
- coreContractCargoArgs =
- lib.concatStringsSep " " (map (crate: "-p ${crate}") coreContractCrates)
- + " --features radroots_event_codec/json,radroots_nostr/blossom,radroots_nostr/client,radroots_nostr/codec,radroots_nostr/events";
+ coreContractCargoArgs = publicNativeCargoArgs;
craneLib = (crane.mkLib pkgs).overrideToolchain toolchains.stable;
commonCraneArgs = {
inherit version;
@@ -184,7 +182,8 @@ let
export RADROOTS_WORKSPACE_ROOT="$PWD"
'';
checkCommand = ''
- cargo check --workspace --all-targets
+ cargo check --locked --all-targets ${publicNativeCargoArgs}
+ cargo check --locked --all-targets ${previewCargoArgs}
'';
architectureCommand = ''
cargo run --locked -q -p xtask -- architecture-ci
@@ -196,7 +195,8 @@ let
cargo run -q -p xtask -- contract validate
'';
releasePreflightCommand = ''
- cargo check -q
+ cargo check -q --locked ${publicNativeCargoArgs}
+ cargo check -q --locked ${previewCargoArgs}
cargo test -q -p xtask
cargo run -q -p xtask -- contract validate
@@ -365,6 +365,7 @@ in
{
inherit
architectureCommand
+ cargoArgsFor
cargoLlvmCov
cargoArtifacts
checkCommand
@@ -377,6 +378,9 @@ in
mkRepoCheck
releasePreflightCommand
coreContractCargoArgs
+ packageGroups
+ previewCargoArgs
+ publicNativeCargoArgs
sharedEnv
version
xtaskPackage
diff --git a/contracts/crates/catalog.v1.toml b/contracts/crates/catalog.v1.toml
@@ -36,7 +36,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["any"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["core"]
permitted_dependency_tiers = ["foundation"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -56,7 +56,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["any"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["identity"]
permitted_dependency_tiers = ["foundation"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -76,7 +76,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["any"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["protocol"]
permitted_dependency_tiers = ["foundation"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -96,7 +96,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["any"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["protocol"]
permitted_dependency_tiers = ["foundation"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -116,7 +116,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["any"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["event"]
permitted_dependency_tiers = ["foundation", "domain"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -136,7 +136,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["any"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["event"]
permitted_dependency_tiers = ["foundation", "domain"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -156,7 +156,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["any"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["trade"]
permitted_dependency_tiers = ["foundation", "domain"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -176,7 +176,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["any"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["signing"]
permitted_dependency_tiers = ["foundation", "domain", "spi"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -196,7 +196,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["any"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["transport"]
permitted_dependency_tiers = ["foundation", "domain", "spi"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -216,7 +216,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["security"]
permitted_dependency_tiers = ["foundation", "domain", "spi"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -236,7 +236,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["storage"]
permitted_dependency_tiers = ["foundation", "domain", "spi"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -256,7 +256,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["any"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["nostr"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -276,7 +276,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["any"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["nostr"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -296,7 +296,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["storage"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -316,7 +316,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["transport"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -336,7 +336,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["geonames"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -356,7 +356,7 @@ publish = true
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["portable", "public_native"]
+groups = ["coverage_required", "portable", "public_native"]
owners = ["sync"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -376,7 +376,7 @@ publish = false
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["preview"]
+groups = ["coverage_required", "preview"]
owners = ["preview"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -396,7 +396,7 @@ publish = false
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["preview"]
+groups = ["coverage_required", "preview"]
owners = ["preview"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -536,7 +536,7 @@ publish = false
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["any"]
-groups = ["preview"]
+groups = ["coverage_required", "preview"]
owners = ["preview"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -556,7 +556,7 @@ publish = false
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["preview"]
+groups = ["coverage_required", "preview"]
owners = ["preview"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -576,7 +576,7 @@ publish = false
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["preview"]
+groups = ["coverage_required", "preview"]
owners = ["preview"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview", "fixture"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -596,7 +596,7 @@ publish = false
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["preview"]
+groups = ["coverage_required", "preview"]
owners = ["preview"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -616,7 +616,7 @@ publish = false
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["preview"]
+groups = ["coverage_required", "preview"]
owners = ["preview"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -636,7 +636,7 @@ publish = false
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["any"]
-groups = ["preview"]
+groups = ["coverage_required", "preview"]
owners = ["preview"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -656,7 +656,7 @@ publish = false
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["preview"]
+groups = ["coverage_required", "preview"]
owners = ["runtime"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview", "runtime"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -676,7 +676,7 @@ publish = false
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["preview"]
+groups = ["coverage_required", "preview"]
owners = ["runtime"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview", "runtime"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -696,7 +696,7 @@ publish = false
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["preview"]
+groups = ["coverage_required", "preview"]
owners = ["runtime"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview", "runtime"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -716,7 +716,7 @@ publish = false
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native", "wasm32"]
-groups = ["preview", "wasm"]
+groups = ["coverage_required", "preview", "wasm"]
owners = ["storage"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -736,7 +736,7 @@ publish = false
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["any"]
-groups = ["preview"]
+groups = ["coverage_required", "preview"]
owners = ["testing"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview", "runtime", "fixture"]
source_repository = "https://github.com/radrootslabs/lib"
@@ -756,7 +756,7 @@ publish = false
version = "0.1.0-alpha"
license = "MIT OR Apache-2.0"
platforms = ["native"]
-groups = ["tools"]
+groups = ["coverage_required", "tools"]
owners = ["architecture"]
permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview", "runtime", "fixture", "tool"]
source_repository = "https://github.com/radrootslabs/lib"
diff --git a/contracts/crates/generated/package_groups.v1.toml b/contracts/crates/generated/package_groups.v1.toml
@@ -1,38 +1,62 @@
schema = "radroots.workspace.package-groups.v1"
-catalog_sha256 = "1a244e2e7f14a20ee21917a246c03277920c927c138796440483bf7837555fcf"
+catalog_sha256 = "0b1d39e687f499a58c7d3a1ffee6b8c07210b61d3b94c7c2a18681fe3a005ace"
[[group]]
id = "boundaries"
packages = ["radroots_core_bindings", "radroots_event_bindings", "radroots_event_codec_wasm", "radroots_identity_bindings", "radroots_mobile_bindgen", "radroots_mobile_ffi", "radroots_mobile_wasm", "radroots_replica_schema_bindings", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk_ffi", "radroots_studio_ffi", "radroots_studio_uniffi_bindgen", "radroots_trade_bindings"]
+active_packages = []
+reserved_packages = ["radroots_core_bindings", "radroots_event_bindings", "radroots_event_codec_wasm", "radroots_identity_bindings", "radroots_mobile_bindgen", "radroots_mobile_ffi", "radroots_mobile_wasm", "radroots_replica_schema_bindings", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk_ffi", "radroots_studio_ffi", "radroots_studio_uniffi_bindgen", "radroots_trade_bindings"]
+
+[[group]]
+id = "coverage_required"
+packages = ["radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_mesh", "radroots_mesh_agent_client", "radroots_mesh_agent_proto", "radroots_nostr", "radroots_nostr_connect", "radroots_nostrdb", "radroots_protocol", "radroots_replica_schema", "radroots_replica_store", "radroots_replica_sync", "radroots_runtime_distribution", "radroots_runtime_manager", "radroots_runtime_paths", "radroots_secrets", "radroots_signing", "radroots_sql_core", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_test_fixtures", "radroots_trade", "radroots_transport", "radroots_transport_nostr", "radroots_transport_reticulum", "xtask"]
+active_packages = ["radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_mesh", "radroots_mesh_agent_client", "radroots_mesh_agent_proto", "radroots_nostr", "radroots_nostr_connect", "radroots_nostrdb", "radroots_protocol", "radroots_replica_schema", "radroots_replica_store", "radroots_replica_sync", "radroots_runtime_distribution", "radroots_runtime_manager", "radroots_runtime_paths", "radroots_secrets", "radroots_signing", "radroots_sql_core", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_test_fixtures", "radroots_trade", "radroots_transport", "radroots_transport_nostr", "radroots_transport_reticulum", "xtask"]
+reserved_packages = []
[[group]]
id = "mobile"
packages = ["radroots_mobile_bindgen", "radroots_mobile_core", "radroots_mobile_ffi", "radroots_mobile_wasm"]
+active_packages = []
+reserved_packages = ["radroots_mobile_bindgen", "radroots_mobile_core", "radroots_mobile_ffi", "radroots_mobile_wasm"]
[[group]]
id = "portable"
packages = ["radroots", "radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_nostr", "radroots_nostr_connect", "radroots_protocol", "radroots_sdk", "radroots_secrets", "radroots_signing", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_trade", "radroots_transport", "radroots_transport_nostr"]
+active_packages = ["radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_nostr", "radroots_nostr_connect", "radroots_protocol", "radroots_secrets", "radroots_signing", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_trade", "radroots_transport", "radroots_transport_nostr"]
+reserved_packages = ["radroots", "radroots_sdk"]
[[group]]
id = "preview"
packages = ["radroots_mesh", "radroots_mesh_agent_client", "radroots_mesh_agent_proto", "radroots_nostrdb", "radroots_replica_schema", "radroots_replica_store", "radroots_replica_sync", "radroots_runtime_distribution", "radroots_runtime_manager", "radroots_runtime_paths", "radroots_simplex_agent_proto", "radroots_simplex_app_store", "radroots_simplex_chat_proto", "radroots_simplex_smp_crypto", "radroots_simplex_smp_proto", "radroots_simplex_smp_transport", "radroots_sql_core", "radroots_test_fixtures", "radroots_transport_reticulum"]
+active_packages = ["radroots_mesh", "radroots_mesh_agent_client", "radroots_mesh_agent_proto", "radroots_nostrdb", "radroots_replica_schema", "radroots_replica_store", "radroots_replica_sync", "radroots_runtime_distribution", "radroots_runtime_manager", "radroots_runtime_paths", "radroots_simplex_agent_proto", "radroots_simplex_app_store", "radroots_simplex_chat_proto", "radroots_simplex_smp_crypto", "radroots_simplex_smp_proto", "radroots_simplex_smp_transport", "radroots_sql_core", "radroots_test_fixtures", "radroots_transport_reticulum"]
+reserved_packages = []
[[group]]
id = "public_native"
packages = ["radroots", "radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_nostr", "radroots_nostr_connect", "radroots_protocol", "radroots_sdk", "radroots_secrets", "radroots_signing", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_trade", "radroots_transport", "radroots_transport_nostr"]
+active_packages = ["radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_nostr", "radroots_nostr_connect", "radroots_protocol", "radroots_secrets", "radroots_signing", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_trade", "radroots_transport", "radroots_transport_nostr"]
+reserved_packages = ["radroots", "radroots_sdk"]
[[group]]
id = "sdk"
packages = ["radroots", "radroots_core_bindings", "radroots_event_bindings", "radroots_event_codec_wasm", "radroots_identity_bindings", "radroots_replica_schema_bindings", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk", "radroots_sdk_ffi", "radroots_sdk_sql_wasm_runtime", "radroots_trade_bindings"]
+active_packages = []
+reserved_packages = ["radroots", "radroots_core_bindings", "radroots_event_bindings", "radroots_event_codec_wasm", "radroots_identity_bindings", "radroots_replica_schema_bindings", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk", "radroots_sdk_ffi", "radroots_sdk_sql_wasm_runtime", "radroots_trade_bindings"]
[[group]]
id = "studio"
packages = ["radroots_studio_application", "radroots_studio_domain", "radroots_studio_ffi", "radroots_studio_nostr", "radroots_studio_runtime", "radroots_studio_storage", "radroots_studio_uniffi_bindgen"]
+active_packages = []
+reserved_packages = ["radroots_studio_application", "radroots_studio_domain", "radroots_studio_ffi", "radroots_studio_nostr", "radroots_studio_runtime", "radroots_studio_storage", "radroots_studio_uniffi_bindgen"]
[[group]]
id = "tools"
packages = ["xtask"]
+active_packages = ["xtask"]
+reserved_packages = []
[[group]]
id = "wasm"
packages = ["radroots_event_codec_wasm", "radroots_mobile_wasm", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk_sql_wasm_runtime", "radroots_sql_core"]
+active_packages = ["radroots_sql_core"]
+reserved_packages = ["radroots_event_codec_wasm", "radroots_mobile_wasm", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk_sql_wasm_runtime"]
diff --git a/contracts/crates/generated/platform_inventory.v1.toml b/contracts/crates/generated/platform_inventory.v1.toml
@@ -1,5 +1,5 @@
schema = "radroots.workspace.platform-inventory.v1"
-catalog_sha256 = "1a244e2e7f14a20ee21917a246c03277920c927c138796440483bf7837555fcf"
+catalog_sha256 = "0b1d39e687f499a58c7d3a1ffee6b8c07210b61d3b94c7c2a18681fe3a005ace"
[[platform]]
id = "android"
diff --git a/contracts/crates/generated/release_inventory.v2.toml b/contracts/crates/generated/release_inventory.v2.toml
@@ -1,5 +1,5 @@
schema = "radroots.workspace.release-inventory.v2"
-catalog_sha256 = "1a244e2e7f14a20ee21917a246c03277920c927c138796440483bf7837555fcf"
+catalog_sha256 = "0b1d39e687f499a58c7d3a1ffee6b8c07210b61d3b94c7c2a18681fe3a005ace"
architecture = "radroots.crates.release.v2"
version = "0.1.0-alpha"
public_packages = ["radroots", "radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_nostr", "radroots_nostr_connect", "radroots_protocol", "radroots_sdk", "radroots_secrets", "radroots_signing", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_trade", "radroots_transport", "radroots_transport_nostr"]
diff --git a/tools/xtask/Cargo.toml b/tools/xtask/Cargo.toml
@@ -10,6 +10,7 @@ publish = false
authors = ["Tyson Lupul <tyson@radroots.org>"]
[dependencies]
+clap = { workspace = true, features = ["derive"] }
dto_bindgen_core = { workspace = true }
fs2 = { workspace = true }
hex = { workspace = true }
diff --git a/tools/xtask/src/build_control.rs b/tools/xtask/src/build_control.rs
@@ -0,0 +1,1322 @@
+use std::{
+ collections::BTreeSet,
+ fs,
+ io::Write,
+ path::{Component, Path, PathBuf},
+ process::Command,
+};
+
+use fs2::FileExt;
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+
+const SOURCE_LOCK_NAME: &str = "radroots.lib.source-lock.v1.toml";
+const CONSUMER_MARKER: &str = ".radroots-consumer-root";
+const CATALOG_RELATIVE: &str = "contracts/crates/catalog.v1.toml";
+const REPOSITORY: &str = "https://github.com/radrootslabs/lib";
+const ARCHITECTURE: &str = "radroots.crates.release.v2";
+const VERSION: &str = "0.1.0-alpha";
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum Operation {
+ Check,
+ Test,
+ Clippy,
+}
+
+impl Operation {
+ pub fn cargo_subcommand(self) -> &'static str {
+ match self {
+ Self::Check => "check",
+ Self::Test => "test",
+ Self::Clippy => "clippy",
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum Mode {
+ Check,
+ Write,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct PackageGroups {
+ schema: String,
+ catalog_sha256: String,
+ group: Vec<PackageGroup>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct PackageGroup {
+ id: String,
+ packages: Vec<String>,
+ active_packages: Vec<String>,
+ reserved_packages: Vec<String>,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct SourceLock {
+ pub schema: String,
+ pub repository: String,
+ pub revision: String,
+ pub architecture: String,
+ pub workspace_catalog_sha256: String,
+ pub version: String,
+ pub source_archive_sha256: Option<String>,
+ pub lockfile_sha256: String,
+}
+
+#[derive(Clone, Debug)]
+pub struct ConsumerRoot {
+ path: PathBuf,
+ product: String,
+ source_lock: SourceLock,
+}
+
+impl ConsumerRoot {
+ pub fn open(path: &Path) -> Result<Self, String> {
+ require_absolute_real_directory(path, "consumer root")?;
+ let canonical = fs::canonicalize(path)
+ .map_err(|error| format!("canonicalize consumer root {}: {error}", path.display()))?;
+ let marker = read_regular_no_follow(&canonical.join(CONSUMER_MARKER))?;
+ let product = String::from_utf8(marker)
+ .map_err(|error| format!("consumer marker is not UTF-8: {error}"))?
+ .trim()
+ .to_owned();
+ if !matches!(product.as_str(), "sdk" | "mobile" | "studio") {
+ return Err("consumer marker must contain sdk, mobile, or studio".to_owned());
+ }
+ let source_lock_path = canonical.join(SOURCE_LOCK_NAME);
+ let source_lock = parse_source_lock(&source_lock_path)?;
+ validate_source_lock(&source_lock)?;
+ validate_consumer_files(&canonical, &source_lock)?;
+ Ok(Self {
+ path: canonical,
+ product,
+ source_lock,
+ })
+ }
+
+ fn output(&self, relative: &Path) -> Result<PathBuf, String> {
+ validate_relative_path(relative, "artifact output")?;
+ let output = self.path.join(relative);
+ ensure_no_symlink_components(&self.path, relative)?;
+ Ok(output)
+ }
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct CacheManifest {
+ schema: String,
+ repository: String,
+ revision: String,
+ workspace_catalog_sha256: String,
+ source_archive_sha256: Option<String>,
+ tree: String,
+}
+
+#[derive(Debug, Deserialize)]
+struct CargoLock {
+ #[serde(default)]
+ package: Vec<CargoLockPackage>,
+}
+
+#[derive(Debug, Deserialize)]
+struct CargoLockPackage {
+ source: Option<String>,
+}
+
+#[derive(Debug, Serialize)]
+struct ArtifactManifest<'a> {
+ schema: &'static str,
+ product: &'a str,
+ target: &'a str,
+ language: &'a str,
+ external_names: Vec<&'a str>,
+ files: Vec<ArtifactFile>,
+ provenance: Provenance<'a>,
+}
+
+#[derive(Debug, Serialize)]
+struct ArtifactFile {
+ path: String,
+ bytes: u64,
+ sha256: String,
+}
+
+#[derive(Debug, Serialize)]
+struct Provenance<'a> {
+ repository: &'a str,
+ revision: &'a str,
+ architecture: &'a str,
+ catalog_sha256: &'a str,
+ lockfile_sha256: &'a str,
+ source_archive_sha256: Option<&'a str>,
+ source_date_epoch: u64,
+ builder_id: &'a str,
+ features: Vec<&'a str>,
+}
+
+pub fn group_plan(
+ workspace_root: &Path,
+ group: &str,
+ operation: Operation,
+ include_reserved: bool,
+) -> Result<Vec<String>, String> {
+ validate_identifier(group, "group")?;
+ let projection_path = workspace_root.join("contracts/crates/generated/package_groups.v1.toml");
+ let bytes = read_regular_no_follow(&projection_path)?;
+ let raw = std::str::from_utf8(&bytes)
+ .map_err(|error| format!("package group projection is not UTF-8: {error}"))?;
+ let projection = toml::from_str::<PackageGroups>(raw)
+ .map_err(|error| format!("parse package group projection: {error}"))?;
+ if projection.schema != "radroots.workspace.package-groups.v1" {
+ return Err("package group projection schema drifted".to_owned());
+ }
+ validate_sha256(&projection.catalog_sha256, "catalog digest")?;
+ let catalog = read_regular_no_follow(&workspace_root.join(CATALOG_RELATIVE))?;
+ if sha256(&catalog) != projection.catalog_sha256 {
+ return Err("package group projection is stale".to_owned());
+ }
+ let selected = projection
+ .group
+ .iter()
+ .find(|candidate| candidate.id == group)
+ .ok_or_else(|| format!("unknown catalog group {group}"))?;
+ let expected = selected
+ .active_packages
+ .iter()
+ .chain(selected.reserved_packages.iter())
+ .cloned()
+ .collect::<BTreeSet<_>>();
+ if selected.packages.iter().cloned().collect::<BTreeSet<_>>() != expected
+ || selected.active_packages.is_empty()
+ {
+ return Err(format!("catalog group {group} is malformed or not active"));
+ }
+ let packages = if include_reserved {
+ &selected.packages
+ } else {
+ &selected.active_packages
+ };
+ let mut plan = vec![
+ operation.cargo_subcommand().to_owned(),
+ "--locked".to_owned(),
+ ];
+ if operation != Operation::Test {
+ plan.push("--all-targets".to_owned());
+ }
+ for package in packages {
+ plan.push("-p".to_owned());
+ plan.push(package.clone());
+ }
+ if operation == Operation::Clippy {
+ plan.push("--".to_owned());
+ plan.push("-D".to_owned());
+ plan.push("warnings".to_owned());
+ }
+ Ok(plan)
+}
+
+pub fn execute_group_plan(workspace_root: &Path, plan: &[String]) -> Result<(), String> {
+ let output = Command::new("cargo")
+ .args(plan)
+ .current_dir(workspace_root)
+ .status()
+ .map_err(|error| format!("run catalog group plan: {error}"))?;
+ if output.success() {
+ Ok(())
+ } else {
+ Err(format!("catalog group plan failed with {output}"))
+ }
+}
+
+pub fn print_plan(plan: &[String]) {
+ println!("cargo {}", plan.join(" "));
+}
+
+pub fn validate_consumer(path: &Path) -> Result<ConsumerRoot, String> {
+ ConsumerRoot::open(path)
+}
+
+pub fn materialize(
+ consumer_path: &Path,
+ cache_root: &Path,
+ offline: bool,
+) -> Result<PathBuf, String> {
+ let consumer = ConsumerRoot::open(consumer_path)?;
+ materialize_from(
+ &consumer,
+ cache_root,
+ offline,
+ &consumer.source_lock.repository,
+ )
+}
+
+fn materialize_from(
+ consumer: &ConsumerRoot,
+ cache_root: &Path,
+ offline: bool,
+ fetch_url: &str,
+) -> Result<PathBuf, String> {
+ require_absolute_real_directory(cache_root, "cache root")?;
+ let key = format!(
+ "{}-{}",
+ consumer.source_lock.revision, consumer.source_lock.workspace_catalog_sha256
+ );
+ let destination = cache_root.join(key);
+ let lock_path = cache_root.join(".radroots-source-cache.lock");
+ let lock = fs::OpenOptions::new()
+ .create(true)
+ .truncate(false)
+ .read(true)
+ .write(true)
+ .open(&lock_path)
+ .map_err(|error| format!("open source cache lock: {error}"))?;
+ lock.lock_exclusive()
+ .map_err(|error| format!("lock source cache: {error}"))?;
+ let result = if destination.exists() {
+ verify_cache(&destination, &consumer.source_lock).map(|()| destination.clone())
+ } else if offline {
+ Err("offline source materialization requires a verified cache entry".to_owned())
+ } else {
+ prefetch_cache(cache_root, &destination, &consumer.source_lock, fetch_url)?;
+ verify_cache(&destination, &consumer.source_lock)?;
+ Ok(destination.clone())
+ };
+ FileExt::unlock(&lock).map_err(|error| format!("unlock source cache: {error}"))?;
+ result
+}
+
+fn prefetch_cache(
+ cache_root: &Path,
+ destination: &Path,
+ source_lock: &SourceLock,
+ fetch_url: &str,
+) -> Result<(), String> {
+ let staging = tempfile::Builder::new()
+ .prefix(".radroots-source-stage-")
+ .tempdir_in(cache_root)
+ .map_err(|error| format!("create source cache staging directory: {error}"))?;
+ git(staging.path(), &["init", "--quiet"])?;
+ git(staging.path(), &["remote", "add", "origin", fetch_url])?;
+ git(
+ staging.path(),
+ &[
+ "fetch",
+ "--quiet",
+ "--depth=1",
+ "origin",
+ &source_lock.revision,
+ ],
+ )?;
+ git(
+ staging.path(),
+ &["checkout", "--quiet", "--detach", "FETCH_HEAD"],
+ )?;
+ let tree = git_stdout(staging.path(), &["rev-parse", "HEAD^{tree}"])?;
+ let manifest = CacheManifest {
+ schema: "radroots.source-cache.v1".to_owned(),
+ repository: source_lock.repository.clone(),
+ revision: source_lock.revision.clone(),
+ workspace_catalog_sha256: source_lock.workspace_catalog_sha256.clone(),
+ source_archive_sha256: source_lock.source_archive_sha256.clone(),
+ tree: tree.trim().to_owned(),
+ };
+ let raw = toml::to_string(&manifest)
+ .map_err(|error| format!("serialize source cache manifest: {error}"))?;
+ atomic_write(
+ &staging.path().join(".radroots-source-cache.v1.toml"),
+ raw.as_bytes(),
+ )?;
+ let staging_path = staging.keep();
+ fs::rename(&staging_path, destination).map_err(|error| {
+ format!(
+ "install source cache {} -> {}: {error}",
+ staging_path.display(),
+ destination.display()
+ )
+ })?;
+ set_readonly_tree(destination)
+}
+
+fn verify_cache(path: &Path, source_lock: &SourceLock) -> Result<(), String> {
+ require_absolute_real_directory(path, "cache entry")?;
+ let head = git_stdout(path, &["rev-parse", "HEAD"])?;
+ let tree = git_stdout(path, &["rev-parse", "HEAD^{tree}"])?;
+ if head.trim() != source_lock.revision {
+ return Err("source cache revision drifted".to_owned());
+ }
+ git(path, &["diff", "--quiet", "--no-ext-diff"])?;
+ git(path, &["diff", "--cached", "--quiet", "--no-ext-diff"])?;
+ verify_untracked_cache_paths(path)?;
+ let manifest_path = path.join(".radroots-source-cache.v1.toml");
+ let bytes = read_regular_no_follow(&manifest_path)?;
+ let raw = std::str::from_utf8(&bytes)
+ .map_err(|error| format!("source cache manifest is not UTF-8: {error}"))?;
+ let manifest = toml::from_str::<CacheManifest>(raw)
+ .map_err(|error| format!("parse source cache manifest: {error}"))?;
+ if manifest.schema != "radroots.source-cache.v1"
+ || manifest.repository != source_lock.repository
+ || manifest.revision != source_lock.revision
+ || manifest.workspace_catalog_sha256 != source_lock.workspace_catalog_sha256
+ || manifest.source_archive_sha256 != source_lock.source_archive_sha256
+ || manifest.tree != tree.trim()
+ {
+ return Err("source cache manifest drifted".to_owned());
+ }
+ let catalog = read_regular_no_follow(&path.join(CATALOG_RELATIVE))?;
+ if sha256(&catalog) != source_lock.workspace_catalog_sha256 {
+ return Err("source cache catalog digest drifted".to_owned());
+ }
+ Ok(())
+}
+
+pub fn verify_source_archive(path: &Path, expected_sha256: &str) -> Result<(), String> {
+ if !path.is_absolute() {
+ return Err("source archive path must be absolute".to_owned());
+ }
+ validate_sha256(expected_sha256, "source archive digest")?;
+ let bytes = read_regular_no_follow(path)?;
+ if sha256(&bytes) != expected_sha256 {
+ return Err("source archive digest drifted".to_owned());
+ }
+ verify_bundle(path)
+}
+
+pub fn create_source_archive(
+ source_root: &Path,
+ revision: &str,
+ output_path: &Path,
+) -> Result<String, String> {
+ require_absolute_real_directory(source_root, "archive source root")?;
+ validate_oid(revision, "archive revision")?;
+ if !output_path.is_absolute() {
+ return Err("source archive output must be absolute".to_owned());
+ }
+ git(
+ source_root,
+ &["cat-file", "-e", &format!("{revision}^{{commit}}")],
+ )?;
+ let parent = output_path
+ .parent()
+ .ok_or_else(|| "source archive output has no parent".to_owned())?;
+ create_directories_no_follow(parent)?;
+ if let Ok(metadata) = fs::symlink_metadata(output_path)
+ && (metadata.file_type().is_symlink() || !metadata.is_file())
+ {
+ return Err("source archive output must be a regular file".to_owned());
+ }
+ let temporary = tempfile::Builder::new()
+ .prefix(".radroots-source-archive-")
+ .tempfile_in(parent)
+ .map_err(|error| format!("stage source archive: {error}"))?;
+ let temporary_path = temporary.path().to_path_buf();
+ temporary
+ .close()
+ .map_err(|error| format!("prepare source archive staging path: {error}"))?;
+ let archive_repo = tempfile::TempDir::new_in(parent)
+ .map_err(|error| format!("create archive staging repository: {error}"))?;
+ git(archive_repo.path(), &["init", "--bare", "--quiet"])?;
+ let fetch = Command::new("git")
+ .args(["fetch", "--quiet", "--no-tags"])
+ .arg(source_root)
+ .arg(format!("{revision}:refs/heads/archive"))
+ .current_dir(archive_repo.path())
+ .output()
+ .map_err(|error| format!("stage archive revision: {error}"))?;
+ if !fetch.status.success() {
+ return Err(format!(
+ "stage archive revision failed: {}",
+ String::from_utf8_lossy(&fetch.stderr).trim()
+ ));
+ }
+ let output = Command::new("git")
+ .args(["bundle", "create"])
+ .arg(&temporary_path)
+ .arg("refs/heads/archive")
+ .current_dir(archive_repo.path())
+ .output()
+ .map_err(|error| format!("create source archive: {error}"))?;
+ if !output.status.success() {
+ let _ = fs::remove_file(&temporary_path);
+ return Err(format!(
+ "create source archive failed: {}",
+ String::from_utf8_lossy(&output.stderr).trim()
+ ));
+ }
+ let bytes = read_regular_no_follow(&temporary_path)?;
+ let digest = sha256(&bytes);
+ verify_bundle(&temporary_path)?;
+ let listed = Command::new("git")
+ .args(["bundle", "list-heads"])
+ .arg(&temporary_path)
+ .output()
+ .map_err(|error| format!("list source archive heads: {error}"))?;
+ if !listed.status.success()
+ || String::from_utf8_lossy(&listed.stdout).trim()
+ != format!("{revision} refs/heads/archive")
+ {
+ let _ = fs::remove_file(&temporary_path);
+ return Err("source archive does not contain exactly the requested revision".to_owned());
+ }
+ if output_path.exists() {
+ let existing = read_regular_no_follow(output_path)?;
+ let _ = fs::remove_file(&temporary_path);
+ if existing == bytes {
+ return Ok(digest);
+ }
+ return Err(
+ "immutable source archive output already exists with different bytes".to_owned(),
+ );
+ }
+ fs::File::open(&temporary_path)
+ .and_then(|file| file.sync_all())
+ .map_err(|error| format!("sync source archive: {error}"))?;
+ fs::rename(&temporary_path, output_path)
+ .map_err(|error| format!("install source archive: {error}"))?;
+ Ok(digest)
+}
+
+fn verify_bundle(path: &Path) -> Result<(), String> {
+ let verification_repo = tempfile::TempDir::new()
+ .map_err(|error| format!("create bundle verification repository: {error}"))?;
+ git(verification_repo.path(), &["init", "--bare", "--quiet"])?;
+ let output = Command::new("git")
+ .args(["bundle", "verify"])
+ .arg(path)
+ .current_dir(verification_repo.path())
+ .output()
+ .map_err(|error| format!("run git bundle verify: {error}"))?;
+ if output.status.success() {
+ Ok(())
+ } else {
+ Err(format!(
+ "source archive is not a valid Git bundle: {}",
+ String::from_utf8_lossy(&output.stderr).trim()
+ ))
+ }
+}
+
+#[allow(clippy::too_many_arguments)]
+pub fn artifact(
+ product: &str,
+ target: &str,
+ language: &str,
+ mode: Mode,
+ consumer_path: &Path,
+ source_path: &Path,
+ output_relative: &Path,
+ source_date_epoch: u64,
+ builder_id: &str,
+ features: &[String],
+) -> Result<(), String> {
+ validate_identifier(product, "product")?;
+ validate_identifier(target, "target")?;
+ validate_identifier(language, "language")?;
+ validate_identifier(builder_id, "builder id")?;
+ validate_artifact_route(product, target, language)?;
+ let consumer = ConsumerRoot::open(consumer_path)?;
+ if consumer.product != product {
+ return Err(format!(
+ "consumer marker {} does not match artifact product {product}",
+ consumer.product
+ ));
+ }
+ verify_source_root(source_path, &consumer.source_lock)?;
+ let mut sorted_features = features.iter().map(String::as_str).collect::<Vec<_>>();
+ sorted_features.sort_unstable();
+ sorted_features.dedup();
+ for feature in &sorted_features {
+ validate_identifier(feature, "feature")?;
+ }
+ let external_names = match product {
+ "sdk" => vec!["radroots", "radroots_sdk"],
+ "mobile" => vec!["RadrootsFFI", "RadrootsKitBindings"],
+ "studio" => vec!["org.radroots.studio.ffi", "radroots_studio_ffi"],
+ _ => return Err("unsupported artifact product".to_owned()),
+ };
+ let manifest = ArtifactManifest {
+ schema: "radroots.artifact-manifest.v1",
+ product,
+ target,
+ language,
+ external_names,
+ files: Vec::new(),
+ provenance: Provenance {
+ repository: &consumer.source_lock.repository,
+ revision: &consumer.source_lock.revision,
+ architecture: &consumer.source_lock.architecture,
+ catalog_sha256: &consumer.source_lock.workspace_catalog_sha256,
+ lockfile_sha256: &consumer.source_lock.lockfile_sha256,
+ source_archive_sha256: consumer.source_lock.source_archive_sha256.as_deref(),
+ source_date_epoch,
+ builder_id,
+ features: sorted_features,
+ },
+ };
+ let mut bytes = serde_json::to_vec_pretty(&manifest)
+ .map_err(|error| format!("serialize artifact manifest: {error}"))?;
+ bytes.push(b'\n');
+ let output = consumer.output(output_relative)?;
+ match mode {
+ Mode::Check => {
+ let current = read_regular_no_follow(&output)?;
+ if current == bytes {
+ Ok(())
+ } else {
+ Err(format!("artifact manifest {} is stale", output.display()))
+ }
+ }
+ Mode::Write => atomic_write(&output, &bytes),
+ }
+}
+
+fn validate_artifact_route(product: &str, target: &str, language: &str) -> Result<(), String> {
+ let valid = match product {
+ "sdk" => matches!(target, "typescript" | "wasm" | "ffi") && language == "typescript",
+ "mobile" => matches!(
+ (target, language),
+ ("ios", "swift") | ("android", "kotlin") | ("wasm", "javascript")
+ ),
+ "studio" => matches!(target, "linux" | "macos" | "windows") && language == "kotlin",
+ _ => false,
+ };
+ if valid {
+ Ok(())
+ } else {
+ Err(format!(
+ "unsupported artifact route {product}/{target}/{language}"
+ ))
+ }
+}
+
+fn verify_source_root(path: &Path, source_lock: &SourceLock) -> Result<(), String> {
+ require_absolute_real_directory(path, "source root")?;
+ if git_stdout(path, &["rev-parse", "HEAD"])?.trim() != source_lock.revision {
+ return Err("source root revision does not match source lock".to_owned());
+ }
+ let catalog = read_regular_no_follow(&path.join(CATALOG_RELATIVE))?;
+ if sha256(&catalog) != source_lock.workspace_catalog_sha256 {
+ return Err("source root catalog does not match source lock".to_owned());
+ }
+ git(path, &["diff", "--quiet", "--no-ext-diff"])?;
+ git(path, &["diff", "--cached", "--quiet", "--no-ext-diff"])?;
+ verify_untracked_cache_paths(path)
+}
+
+fn verify_untracked_cache_paths(path: &Path) -> Result<(), String> {
+ let status = git_stdout(path, &["status", "--porcelain", "--untracked-files=all"])?;
+ for line in status.lines() {
+ if line != "?? .radroots-source-cache.v1.toml" {
+ return Err(format!("source tree contains ungoverned change {line}"));
+ }
+ }
+ Ok(())
+}
+
+fn parse_source_lock(path: &Path) -> Result<SourceLock, String> {
+ let bytes = read_regular_no_follow(path)?;
+ let raw = std::str::from_utf8(&bytes)
+ .map_err(|error| format!("source lock is not UTF-8: {error}"))?;
+ toml::from_str(raw).map_err(|error| format!("parse source lock {}: {error}", path.display()))
+}
+
+fn validate_source_lock(source_lock: &SourceLock) -> Result<(), String> {
+ if source_lock.schema != "radroots.lib.source-lock.v1"
+ || source_lock.repository != REPOSITORY
+ || source_lock.architecture != ARCHITECTURE
+ || source_lock.version != VERSION
+ {
+ return Err(
+ "source lock identity, repository, architecture, or version drifted".to_owned(),
+ );
+ }
+ validate_oid(&source_lock.revision, "source lock revision")?;
+ validate_sha256(
+ &source_lock.workspace_catalog_sha256,
+ "source lock catalog digest",
+ )?;
+ validate_sha256(&source_lock.lockfile_sha256, "source lock lockfile digest")?;
+ if let Some(digest) = &source_lock.source_archive_sha256 {
+ validate_sha256(digest, "source lock archive digest")?;
+ }
+ Ok(())
+}
+
+fn validate_consumer_files(root: &Path, source_lock: &SourceLock) -> Result<(), String> {
+ let lockfile = root.join("Cargo.lock");
+ let lockfile_bytes = read_regular_no_follow(&lockfile)?;
+ if sha256(&lockfile_bytes) != source_lock.lockfile_sha256 {
+ return Err("consumer Cargo.lock digest drifted".to_owned());
+ }
+ let lockfile_raw = std::str::from_utf8(&lockfile_bytes)
+ .map_err(|error| format!("consumer Cargo.lock is not UTF-8: {error}"))?;
+ let cargo_lock = toml::from_str::<CargoLock>(lockfile_raw)
+ .map_err(|error| format!("parse consumer Cargo.lock: {error}"))?;
+ let expected_source = format!(
+ "git+{}?rev={}#{}",
+ source_lock.repository, source_lock.revision, source_lock.revision
+ );
+ let mut lock_source_count = 0_usize;
+ for source in cargo_lock
+ .package
+ .iter()
+ .filter_map(|package| package.source.as_deref())
+ .filter(|source| source.contains("radrootslabs/lib"))
+ {
+ lock_source_count += 1;
+ if source != expected_source {
+ return Err("consumer Cargo.lock contains a mixed or floating lib source".to_owned());
+ }
+ }
+ if lock_source_count == 0 {
+ return Err("consumer Cargo.lock contains no canonical lib source".to_owned());
+ }
+ let mut manifests = Vec::new();
+ collect_manifests(root, root, &mut manifests)?;
+ if manifests.is_empty() {
+ return Err("consumer root contains no Cargo manifest".to_owned());
+ }
+ let mut dependency_count = 0_usize;
+ for manifest in manifests {
+ let bytes = read_regular_no_follow(&manifest)?;
+ let raw = std::str::from_utf8(&bytes)
+ .map_err(|error| format!("consumer manifest is not UTF-8: {error}"))?;
+ let value = toml::from_str::<toml::Value>(raw)
+ .map_err(|error| format!("parse consumer manifest {}: {error}", manifest.display()))?;
+ validate_manifest_value(&value, source_lock, &mut dependency_count)?;
+ }
+ if dependency_count == 0 {
+ return Err("consumer manifests contain no canonical lib dependency".to_owned());
+ }
+ Ok(())
+}
+
+fn collect_manifests(root: &Path, current: &Path, output: &mut Vec<PathBuf>) -> Result<(), String> {
+ for entry in fs::read_dir(current)
+ .map_err(|error| format!("read consumer directory {}: {error}", current.display()))?
+ {
+ let entry = entry.map_err(|error| format!("read consumer directory entry: {error}"))?;
+ let file_type = entry
+ .file_type()
+ .map_err(|error| format!("inspect {}: {error}", entry.path().display()))?;
+ if file_type.is_symlink() {
+ return Err(format!(
+ "consumer tree contains symlink {}",
+ entry.path().display()
+ ));
+ }
+ let name = entry.file_name();
+ if file_type.is_dir() {
+ if matches!(
+ name.to_str(),
+ Some(".git" | "target" | "node_modules" | ".radroots")
+ ) {
+ continue;
+ }
+ collect_manifests(root, &entry.path(), output)?;
+ } else if name == "Cargo.toml" {
+ entry
+ .path()
+ .strip_prefix(root)
+ .map_err(|_| "consumer manifest escaped root".to_owned())?;
+ output.push(entry.path());
+ }
+ }
+ output.sort();
+ Ok(())
+}
+
+fn validate_manifest_value(
+ value: &toml::Value,
+ source_lock: &SourceLock,
+ dependency_count: &mut usize,
+) -> Result<(), String> {
+ match value {
+ toml::Value::Table(table) => {
+ let structured_lib_url = table
+ .get("git")
+ .and_then(toml::Value::as_str)
+ .filter(|git| git.contains("radrootslabs/lib"));
+ if let Some(git) = structured_lib_url {
+ *dependency_count += 1;
+ if git != source_lock.repository
+ || table.get("rev").and_then(toml::Value::as_str)
+ != Some(source_lock.revision.as_str())
+ || table.get("version").and_then(toml::Value::as_str) != Some("=0.1.0-alpha")
+ || table.contains_key("branch")
+ || table.contains_key("tag")
+ || table.contains_key("path")
+ {
+ return Err(
+ "consumer manifest contains a mixed or floating lib dependency".to_owned(),
+ );
+ }
+ }
+ for (key, child) in table {
+ if structured_lib_url.is_some() && key == "git" {
+ continue;
+ }
+ if key == "patch" && format!("{child:?}").contains("radrootslabs/lib") {
+ return Err("consumer manifest contains a lib patch override".to_owned());
+ }
+ validate_manifest_value(child, source_lock, dependency_count)?;
+ }
+ }
+ toml::Value::Array(values) => {
+ for child in values {
+ validate_manifest_value(child, source_lock, dependency_count)?;
+ }
+ }
+ toml::Value::String(value) if value.contains("radrootslabs/lib") => {
+ return Err("consumer manifest contains an unstructured lib source".to_owned());
+ }
+ _ => {}
+ }
+ Ok(())
+}
+
+fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), String> {
+ if bytes.contains(&b'\r') || !bytes.ends_with(b"\n") {
+ return Err("generated text must use LF and end with one newline".to_owned());
+ }
+ let parent = path
+ .parent()
+ .ok_or_else(|| format!("output {} has no parent", path.display()))?;
+ create_directories_no_follow(parent)?;
+ if let Ok(metadata) = fs::symlink_metadata(path) {
+ if metadata.file_type().is_symlink() || !metadata.is_file() {
+ return Err(format!("output {} must be a regular file", path.display()));
+ }
+ if fs::read(path).map_err(|error| format!("read {}: {error}", path.display()))? == bytes {
+ return Ok(());
+ }
+ }
+ let mut temporary = tempfile::NamedTempFile::new_in(parent)
+ .map_err(|error| format!("stage output in {}: {error}", parent.display()))?;
+ temporary
+ .write_all(bytes)
+ .map_err(|error| format!("stage output {}: {error}", path.display()))?;
+ temporary
+ .as_file()
+ .sync_all()
+ .map_err(|error| format!("sync staged output {}: {error}", path.display()))?;
+ temporary
+ .persist(path)
+ .map_err(|error| format!("replace output {}: {}", path.display(), error.error))?;
+ Ok(())
+}
+
+fn create_directories_no_follow(path: &Path) -> Result<(), String> {
+ let mut current = PathBuf::new();
+ for component in path.components() {
+ current.push(component.as_os_str());
+ match fs::symlink_metadata(¤t) {
+ Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => {
+ return Err(format!(
+ "output parent {} is not a real directory",
+ current.display()
+ ));
+ }
+ Ok(_) => {}
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
+ fs::create_dir(¤t).map_err(|error| {
+ format!("create output directory {}: {error}", current.display())
+ })?;
+ }
+ Err(error) => {
+ return Err(format!(
+ "inspect output directory {}: {error}",
+ current.display()
+ ));
+ }
+ }
+ }
+ Ok(())
+}
+
+fn ensure_no_symlink_components(root: &Path, relative: &Path) -> Result<(), String> {
+ let mut current = root.to_path_buf();
+ for component in relative.components() {
+ current.push(component.as_os_str());
+ match fs::symlink_metadata(¤t) {
+ Ok(metadata) if metadata.file_type().is_symlink() => {
+ return Err(format!(
+ "artifact path contains symlink {}",
+ current.display()
+ ));
+ }
+ Ok(_) => {}
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => break,
+ Err(error) => {
+ return Err(format!(
+ "inspect artifact path {}: {error}",
+ current.display()
+ ));
+ }
+ }
+ }
+ Ok(())
+}
+
+fn read_regular_no_follow(path: &Path) -> Result<Vec<u8>, String> {
+ let metadata = fs::symlink_metadata(path)
+ .map_err(|error| format!("inspect {}: {error}", path.display()))?;
+ if metadata.file_type().is_symlink() || !metadata.is_file() {
+ return Err(format!(
+ "{} must be a regular non-symlink file",
+ path.display()
+ ));
+ }
+ fs::read(path).map_err(|error| format!("read {}: {error}", path.display()))
+}
+
+fn require_absolute_real_directory(path: &Path, label: &str) -> Result<(), String> {
+ if !path.is_absolute() {
+ return Err(format!("{label} must be absolute"));
+ }
+ let metadata = fs::symlink_metadata(path)
+ .map_err(|error| format!("inspect {label} {}: {error}", path.display()))?;
+ if metadata.file_type().is_symlink() || !metadata.is_dir() {
+ return Err(format!("{label} must be a real directory"));
+ }
+ Ok(())
+}
+
+fn validate_relative_path(path: &Path, label: &str) -> Result<(), String> {
+ if path.as_os_str().is_empty()
+ || path.is_absolute()
+ || path
+ .components()
+ .any(|component| !matches!(component, Component::Normal(_)))
+ {
+ return Err(format!("{label} must be a safe relative path"));
+ }
+ Ok(())
+}
+
+fn validate_identifier(value: &str, label: &str) -> Result<(), String> {
+ if value.is_empty()
+ || !value.bytes().all(|byte| {
+ byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-' | b'/')
+ })
+ {
+ return Err(format!("{label} contains unsupported characters"));
+ }
+ Ok(())
+}
+
+fn validate_oid(value: &str, label: &str) -> Result<(), String> {
+ if value.len() != 40
+ || !value
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ return Err(format!("{label} must be lowercase full 40-hex"));
+ }
+ Ok(())
+}
+
+fn validate_sha256(value: &str, label: &str) -> Result<(), String> {
+ if value.len() != 64
+ || !value
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ return Err(format!("{label} must be lowercase 64-hex"));
+ }
+ Ok(())
+}
+
+fn sha256(bytes: &[u8]) -> String {
+ format!("{:x}", Sha256::digest(bytes))
+}
+
+fn git(root: &Path, args: &[&str]) -> Result<(), String> {
+ let output = Command::new("git")
+ .args(args)
+ .current_dir(root)
+ .output()
+ .map_err(|error| format!("run git {}: {error}", args.join(" ")))?;
+ if output.status.success() {
+ Ok(())
+ } else {
+ Err(format!(
+ "git {} failed: {}",
+ args.join(" "),
+ String::from_utf8_lossy(&output.stderr).trim()
+ ))
+ }
+}
+
+fn git_stdout(root: &Path, args: &[&str]) -> Result<String, String> {
+ let output = Command::new("git")
+ .args(args)
+ .current_dir(root)
+ .output()
+ .map_err(|error| format!("run git {}: {error}", args.join(" ")))?;
+ if !output.status.success() {
+ return Err(format!(
+ "git {} failed: {}",
+ args.join(" "),
+ String::from_utf8_lossy(&output.stderr).trim()
+ ));
+ }
+ String::from_utf8(output.stdout)
+ .map_err(|error| format!("git {} output is not UTF-8: {error}", args.join(" ")))
+}
+
+fn set_readonly_tree(root: &Path) -> Result<(), String> {
+ for entry in walk(root)? {
+ let metadata = fs::symlink_metadata(&entry)
+ .map_err(|error| format!("inspect cache path {}: {error}", entry.display()))?;
+ if metadata.file_type().is_symlink() {
+ return Err(format!("source cache contains symlink {}", entry.display()));
+ }
+ let mut permissions = metadata.permissions();
+ permissions.set_readonly(true);
+ fs::set_permissions(&entry, permissions)
+ .map_err(|error| format!("protect cache path {}: {error}", entry.display()))?;
+ }
+ Ok(())
+}
+
+fn walk(root: &Path) -> Result<Vec<PathBuf>, String> {
+ let mut pending = vec![root.to_path_buf()];
+ let mut output = Vec::new();
+ while let Some(path) = pending.pop() {
+ output.push(path.clone());
+ if fs::symlink_metadata(&path)
+ .map_err(|error| format!("inspect {}: {error}", path.display()))?
+ .is_dir()
+ {
+ for entry in
+ fs::read_dir(&path).map_err(|error| format!("read {}: {error}", path.display()))?
+ {
+ pending.push(
+ entry
+ .map_err(|error| format!("read directory entry: {error}"))?
+ .path(),
+ );
+ }
+ }
+ }
+ output.sort_by_key(|path| std::cmp::Reverse(path.components().count()));
+ Ok(output)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ struct Fixture {
+ _root: tempfile::TempDir,
+ source: PathBuf,
+ consumer: PathBuf,
+ cache: PathBuf,
+ source_lock: SourceLock,
+ }
+
+ impl Fixture {
+ fn new(product: &str) -> Self {
+ let root = tempfile::TempDir::new().expect("fixture root");
+ let source = root.path().join("source");
+ let consumer = root.path().join("consumer");
+ let cache = root.path().join("cache");
+ fs::create_dir(&source).expect("source");
+ fs::create_dir(&consumer).expect("consumer");
+ fs::create_dir(&cache).expect("cache");
+ git(&source, &["init", "--initial-branch=master"]).expect("init");
+ git(&source, &["config", "user.name", "Build Control Fixture"]).expect("name");
+ git(
+ &source,
+ &["config", "user.email", "build-control@radroots.org"],
+ )
+ .expect("email");
+ fs::create_dir_all(source.join("contracts/crates")).expect("contracts");
+ fs::write(
+ source.join(CATALOG_RELATIVE),
+ "schema = \"radroots.workspace.catalog.v1\"\n",
+ )
+ .expect("catalog");
+ fs::create_dir_all(source.join("src")).expect("source crate");
+ fs::write(
+ source.join("Cargo.toml"),
+ "[package]\nname = \"source_fixture\"\nversion = \"0.1.0-alpha\"\nedition = \"2024\"\n",
+ )
+ .expect("source manifest");
+ fs::write(source.join("src/lib.rs"), "pub const READY: bool = true;\n")
+ .expect("source library");
+ fs::write(
+ source.join("Cargo.lock"),
+ "# This file is automatically @generated by Cargo.\n# It is not intended for manual editing.\nversion = 4\n\n[[package]]\nname = \"source_fixture\"\nversion = \"0.1.0-alpha\"\n",
+ )
+ .expect("source lockfile");
+ git(&source, &["add", "--all"]).expect("add");
+ git(&source, &["commit", "-m", "seed source fixture"]).expect("commit");
+ let revision = git_stdout(&source, &["rev-parse", "HEAD"])
+ .expect("revision")
+ .trim()
+ .to_owned();
+ let catalog_sha256 =
+ sha256(&fs::read(source.join(CATALOG_RELATIVE)).expect("read source catalog"));
+ fs::write(consumer.join(CONSUMER_MARKER), format!("{product}\n")).expect("marker");
+ fs::write(
+ consumer.join("Cargo.toml"),
+ format!(
+ "[package]\nname = \"consumer\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nradroots_core = {{ git = \"{REPOSITORY}\", rev = \"{revision}\", version = \"=0.1.0-alpha\" }}\n"
+ ),
+ )
+ .expect("manifest");
+ let consumer_lock = format!(
+ "version = 4\n\n[[package]]\nname = \"radroots_core\"\nversion = \"0.1.0-alpha\"\nsource = \"git+{REPOSITORY}?rev={revision}#{revision}\"\n"
+ );
+ fs::write(consumer.join("Cargo.lock"), &consumer_lock).expect("consumer lockfile");
+ let source_lock = SourceLock {
+ schema: "radroots.lib.source-lock.v1".to_owned(),
+ repository: REPOSITORY.to_owned(),
+ revision,
+ architecture: ARCHITECTURE.to_owned(),
+ workspace_catalog_sha256: catalog_sha256,
+ version: VERSION.to_owned(),
+ source_archive_sha256: None,
+ lockfile_sha256: sha256(consumer_lock.as_bytes()),
+ };
+ fs::write(
+ consumer.join(SOURCE_LOCK_NAME),
+ toml::to_string(&source_lock).expect("serialize source lock"),
+ )
+ .expect("source lock");
+ Self {
+ _root: root,
+ source,
+ consumer,
+ cache,
+ source_lock,
+ }
+ }
+ }
+
+ #[test]
+ fn source_lock_and_consumer_root_fail_closed() {
+ let fixture = Fixture::new("sdk");
+ ConsumerRoot::open(&fixture.consumer).expect("valid consumer");
+ let mut invalid = fixture.source_lock.clone();
+ invalid.revision = "short".to_owned();
+ assert!(validate_source_lock(&invalid).is_err());
+
+ fs::write(
+ fixture.consumer.join("Cargo.toml"),
+ "[package]\nname = \"consumer\"\nversion = \"0.1.0\"\n\n[dependencies]\nradroots_core = { git = \"https://github.com/radrootslabs/lib\", branch = \"master\", version = \"*\" }\n",
+ )
+ .expect("floating manifest");
+ assert!(ConsumerRoot::open(&fixture.consumer).is_err());
+ }
+
+ #[test]
+ fn materialization_reuses_verified_cache_and_rejects_tampering() {
+ let fixture = Fixture::new("sdk");
+ let consumer = ConsumerRoot::open(&fixture.consumer).expect("consumer");
+ let cached = materialize_from(
+ &consumer,
+ &fixture.cache,
+ false,
+ fixture.source.to_str().expect("source path"),
+ )
+ .expect("prefetch");
+ assert_eq!(
+ materialize_from(
+ &consumer,
+ &fixture.cache,
+ true,
+ fixture.source.to_str().expect("source path"),
+ )
+ .expect("offline reuse"),
+ cached
+ );
+ let frozen_target = fixture._root.path().join("frozen-target");
+ let frozen = Command::new("cargo")
+ .args(["check", "--offline", "--frozen", "--locked"])
+ .env("CARGO_TARGET_DIR", &frozen_target)
+ .current_dir(&cached)
+ .output()
+ .expect("run frozen offline source smoke");
+ assert!(
+ frozen.status.success(),
+ "frozen offline source smoke failed: {}",
+ String::from_utf8_lossy(&frozen.stderr)
+ );
+ let catalog = cached.join(CATALOG_RELATIVE);
+ make_path_writable(&catalog).expect("make catalog writable");
+ fs::write(&catalog, "tampered\n").expect("tamper");
+ assert!(verify_cache(&cached, &fixture.source_lock).is_err());
+ make_writable(&cached);
+ }
+
+ #[test]
+ fn artifact_manifests_are_deterministic_and_route_checked() {
+ let fixture = Fixture::new("sdk");
+ let output = Path::new("generated/artifact-manifest.json");
+ artifact(
+ "sdk",
+ "typescript",
+ "typescript",
+ Mode::Write,
+ &fixture.consumer,
+ &fixture.source,
+ output,
+ 1_700_000_000,
+ "fixture_builder",
+ &["zeta".to_owned(), "alpha".to_owned(), "alpha".to_owned()],
+ )
+ .expect("write artifact manifest");
+ artifact(
+ "sdk",
+ "typescript",
+ "typescript",
+ Mode::Check,
+ &fixture.consumer,
+ &fixture.source,
+ output,
+ 1_700_000_000,
+ "fixture_builder",
+ &["alpha".to_owned(), "zeta".to_owned()],
+ )
+ .expect("check artifact manifest");
+ assert!(
+ validate_artifact_route("mobile", "ios", "kotlin").is_err(),
+ "unsupported target/language must fail"
+ );
+ assert!(
+ artifact(
+ "sdk",
+ "typescript",
+ "typescript",
+ Mode::Write,
+ &fixture.consumer,
+ &fixture.source,
+ Path::new("../escape"),
+ 1,
+ "fixture_builder",
+ &[],
+ )
+ .is_err()
+ );
+ }
+
+ #[test]
+ fn atomic_outputs_reject_unsafe_text_and_path_collisions() {
+ let root = tempfile::TempDir::new().expect("output fixture");
+ let output = root.path().join("generated/output.json");
+ atomic_write(&output, b"prior\n").expect("initial output");
+ #[cfg(unix)]
+ let initial_inode = {
+ use std::os::unix::fs::MetadataExt;
+ fs::metadata(&output).expect("initial metadata").ino()
+ };
+ atomic_write(&output, b"prior\n").expect("identical no-op");
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::MetadataExt;
+ assert_eq!(
+ fs::metadata(&output).expect("no-op metadata").ino(),
+ initial_inode
+ );
+ }
+ assert!(atomic_write(&output, b"missing newline").is_err());
+ assert!(atomic_write(&output, b"windows\r\n").is_err());
+ assert_eq!(
+ fs::read(&output).expect("prior output retained"),
+ b"prior\n"
+ );
+ let directory_output = root.path().join("directory-output");
+ fs::create_dir_all(&directory_output).expect("directory collision");
+ assert!(atomic_write(&directory_output, b"{}\n").is_err());
+
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::symlink;
+
+ let symlink_root = tempfile::TempDir::new().expect("symlink fixture");
+ let outside = tempfile::TempDir::new().expect("outside fixture");
+ symlink(outside.path(), symlink_root.path().join("generated"))
+ .expect("output parent symlink");
+ assert!(
+ atomic_write(&symlink_root.path().join("generated/output.json"), b"{}\n").is_err()
+ );
+ }
+ }
+
+ #[test]
+ fn source_archive_round_trip_is_digest_bound_and_immutable() {
+ let fixture = Fixture::new("sdk");
+ let archive = fixture._root.path().join("archives/source.bundle");
+ let digest =
+ create_source_archive(&fixture.source, &fixture.source_lock.revision, &archive)
+ .expect("create archive");
+ verify_source_archive(&archive, &digest).expect("verify archive");
+ assert_eq!(
+ create_source_archive(&fixture.source, &fixture.source_lock.revision, &archive,)
+ .expect("identical archive no-op"),
+ digest
+ );
+ assert!(verify_source_archive(&archive, &"0".repeat(64)).is_err());
+ assert!(verify_source_archive(Path::new("relative.bundle"), &digest).is_err());
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn consumer_and_source_roots_reject_symlinks() {
+ use std::os::unix::fs::symlink;
+
+ let fixture = Fixture::new("sdk");
+ let consumer_link = fixture._root.path().join("consumer-link");
+ symlink(&fixture.consumer, &consumer_link).expect("consumer symlink");
+ assert!(ConsumerRoot::open(&consumer_link).is_err());
+
+ let source_link = fixture._root.path().join("source-link");
+ symlink(&fixture.source, &source_link).expect("source symlink");
+ assert!(verify_source_root(&source_link, &fixture.source_lock).is_err());
+ }
+
+ #[test]
+ fn checked_in_group_plan_is_explicit_and_active_only() {
+ let plan = group_plan(
+ &crate::workspace_root(),
+ "public_native",
+ Operation::Check,
+ false,
+ )
+ .expect("public native plan");
+ assert!(plan.iter().any(|arg| arg == "radroots_core"));
+ assert!(!plan.iter().any(|arg| arg == "radroots_sdk"));
+ assert!(!plan.iter().any(|arg| arg == "--workspace"));
+ assert!(group_plan(&crate::workspace_root(), "missing", Operation::Check, false).is_err());
+ }
+
+ fn make_writable(root: &Path) {
+ if let Ok(paths) = walk(root) {
+ for path in paths {
+ let _ = make_path_writable(&path);
+ }
+ }
+ }
+
+ fn make_path_writable(path: &Path) -> Result<(), std::io::Error> {
+ let mut permissions = fs::metadata(path)?.permissions();
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+ permissions.set_mode(permissions.mode() | 0o200);
+ }
+ #[cfg(not(unix))]
+ permissions.set_readonly(false);
+ fs::set_permissions(path, permissions)
+ }
+}
diff --git a/tools/xtask/src/catalog.rs b/tools/xtask/src/catalog.rs
@@ -18,6 +18,7 @@ const CONSOLIDATION_RELATIVE: &str = "contracts/consolidation/architecture.v1.to
const GROUPS_RELATIVE: &str = "contracts/crates/generated/package_groups.v1.toml";
const PLATFORMS_RELATIVE: &str = "contracts/crates/generated/platform_inventory.v1.toml";
const RELEASE_INVENTORY_RELATIVE: &str = "contracts/crates/generated/release_inventory.v2.toml";
+const COVERAGE_RELATIVE: &str = "contracts/coverage.toml";
const CATALOG_SCHEMA: &str = "radroots.workspace.catalog.v1";
const RELEASE_ID: &str = "radroots.crates.release.v2";
const CONSOLIDATION_ID: &str = "radroots.rust.consolidation.v1";
@@ -104,6 +105,16 @@ struct ConsolidationV1 {
}
#[derive(Debug, Deserialize)]
+struct CoveragePolicy {
+ required: CoverageRequired,
+}
+
+#[derive(Debug, Deserialize)]
+struct CoverageRequired {
+ crates: Vec<String>,
+}
+
+#[derive(Debug, Deserialize)]
struct CargoMetadata {
packages: Vec<CargoPackage>,
workspace_members: Vec<String>,
@@ -138,7 +149,45 @@ pub fn run(args: &[String], workspace_root: &Path) -> Result<(), String> {
}
pub(crate) fn check(workspace_root: &Path) -> Result<(), String> {
- let loaded = load_and_validate(workspace_root)?;
+ check_with_provenance(workspace_root, true)
+}
+
+pub(crate) fn check_source_export(workspace_root: &Path) -> Result<(), String> {
+ check_with_provenance(workspace_root, false)
+}
+
+pub(crate) fn active_group(workspace_root: &Path, group: &str) -> Result<Vec<String>, String> {
+ validate_identifier(group, "package group")?;
+ active_packages_matching(workspace_root, |package| {
+ package.groups.iter().any(|candidate| candidate == group)
+ })
+}
+
+pub(crate) fn active_packages(workspace_root: &Path) -> Result<Vec<String>, String> {
+ active_packages_matching(workspace_root, |_| true)
+}
+
+fn active_packages_matching(
+ workspace_root: &Path,
+ predicate: impl Fn(&CatalogPackage) -> bool,
+) -> Result<Vec<String>, String> {
+ let catalog = parse_file::<Catalog>(workspace_root, CATALOG_RELATIVE)?;
+ validate_catalog(&catalog)?;
+ let mut packages = catalog
+ .package
+ .iter()
+ .filter(|package| package.state == "active" && predicate(package))
+ .map(|package| package.name.clone())
+ .collect::<Vec<_>>();
+ packages.sort_unstable();
+ Ok(packages)
+}
+
+fn check_with_provenance(
+ workspace_root: &Path,
+ require_git_provenance: bool,
+) -> Result<(), String> {
+ let loaded = load_and_validate(workspace_root, require_git_provenance)?;
for artifact in render_projections(&loaded.catalog, &loaded.catalog_digest) {
let current = read_regular_file(workspace_root, artifact.relative)?;
if current != artifact.contents {
@@ -152,7 +201,7 @@ pub(crate) fn check(workspace_root: &Path) -> Result<(), String> {
}
fn write(workspace_root: &Path) -> Result<(), String> {
- let loaded = load_and_validate(workspace_root)?;
+ let loaded = load_and_validate(workspace_root, true)?;
let artifacts = render_projections(&loaded.catalog, &loaded.catalog_digest);
with_artifact_bundle_transaction(workspace_root, |transaction| transaction.write(artifacts))
}
@@ -162,18 +211,25 @@ struct LoadedCatalog {
catalog_digest: String,
}
-fn load_and_validate(workspace_root: &Path) -> Result<LoadedCatalog, String> {
+fn load_and_validate(
+ workspace_root: &Path,
+ require_git_provenance: bool,
+) -> Result<LoadedCatalog, String> {
let catalog_bytes = read_regular_file(workspace_root, CATALOG_RELATIVE)?;
let catalog = parse_toml::<Catalog>(CATALOG_RELATIVE, &catalog_bytes)?;
let release = parse_file::<ReleaseV2>(workspace_root, RELEASE_RELATIVE)?;
let consolidation = parse_file::<ConsolidationV1>(workspace_root, CONSOLIDATION_RELATIVE)?;
+ let coverage = parse_file::<CoveragePolicy>(workspace_root, COVERAGE_RELATIVE)?;
validate_catalog(&catalog)?;
+ validate_coverage_authority(&catalog, &coverage)?;
validate_release(&release, &catalog, workspace_root)?;
validate_consolidation(&consolidation)?;
validate_workspace_manifest(&catalog, workspace_root)?;
let metadata = cargo_metadata(workspace_root)?;
validate_metadata(&catalog, &metadata, workspace_root)?;
- validate_active_source_provenance(&catalog, workspace_root)?;
+ if require_git_provenance {
+ validate_active_source_provenance(&catalog, workspace_root)?;
+ }
Ok(LoadedCatalog {
catalog,
catalog_digest: sha256(&catalog_bytes),
@@ -342,6 +398,7 @@ fn validate_catalog(catalog: &Catalog) -> Result<(), String> {
}
let required_groups = BTreeSet::from([
"boundaries",
+ "coverage_required",
"mobile",
"portable",
"preview",
@@ -405,6 +462,37 @@ fn validate_catalog(catalog: &Catalog) -> Result<(), String> {
Ok(())
}
+fn validate_coverage_authority(catalog: &Catalog, coverage: &CoveragePolicy) -> Result<(), String> {
+ let catalog_required = catalog
+ .package
+ .iter()
+ .filter(|package| {
+ package.state == "active"
+ && package
+ .groups
+ .iter()
+ .any(|group| group == "coverage_required")
+ })
+ .map(|package| package.name.as_str())
+ .collect::<BTreeSet<_>>();
+ let policy_required = coverage
+ .required
+ .crates
+ .iter()
+ .map(String::as_str)
+ .collect::<BTreeSet<_>>();
+ if policy_required.len() != coverage.required.crates.len() {
+ return Err("coverage policy contains duplicate required packages".to_owned());
+ }
+ if catalog_required != policy_required {
+ return Err(
+ "coverage required packages must exactly match the catalog coverage_required group"
+ .to_owned(),
+ );
+ }
+ Ok(())
+}
+
fn validate_release(
release: &ReleaseV2,
catalog: &Catalog,
@@ -548,6 +636,32 @@ fn validate_workspace_manifest(catalog: &Catalog, workspace_root: &Path) -> Resu
"Cargo workspace members must exactly match explicit active catalog paths".to_owned(),
);
}
+ let default_members = workspace
+ .get("default-members")
+ .and_then(toml::Value::as_array)
+ .ok_or_else(|| "Cargo.toml lacks explicit default members".to_owned())?
+ .iter()
+ .map(|value| {
+ value
+ .as_str()
+ .ok_or_else(|| "default member must be a string".to_owned())
+ })
+ .collect::<Result<BTreeSet<_>, _>>()?;
+ let expected_defaults = catalog
+ .package
+ .iter()
+ .filter(|package| {
+ package.state == "active"
+ && (package.groups.iter().any(|group| group == "portable")
+ || package.name == "xtask")
+ })
+ .map(|package| package.path.as_str())
+ .collect::<BTreeSet<_>>();
+ if default_members != expected_defaults {
+ return Err(
+ "Cargo default members must match active portable packages plus xtask".to_owned(),
+ );
+ }
if workspace.get("resolver").and_then(toml::Value::as_str) != Some("3") {
return Err("Cargo workspace resolver must remain 3".to_owned());
}
@@ -722,6 +836,8 @@ fn validate_active_source_provenance(
fn render_projections(catalog: &Catalog, digest: &str) -> Vec<GeneratedArtifact> {
let mut groups = BTreeMap::<&str, Vec<&str>>::new();
+ let mut active_groups = BTreeMap::<&str, Vec<&str>>::new();
+ let mut reserved_groups = BTreeMap::<&str, Vec<&str>>::new();
let mut platforms = BTreeMap::<&str, Vec<&str>>::new();
let mut public = Vec::new();
let mut private = Vec::new();
@@ -729,6 +845,14 @@ fn render_projections(catalog: &Catalog, digest: &str) -> Vec<GeneratedArtifact>
for package in &catalog.package {
for group in &package.groups {
groups.entry(group).or_default().push(&package.name);
+ if package.state == "active" {
+ active_groups.entry(group).or_default().push(&package.name);
+ } else {
+ reserved_groups
+ .entry(group)
+ .or_default()
+ .push(&package.name);
+ }
}
for platform in &package.platforms {
platforms.entry(platform).or_default().push(&package.name);
@@ -743,6 +867,8 @@ fn render_projections(catalog: &Catalog, digest: &str) -> Vec<GeneratedArtifact>
}
}
sort_map_values(&mut groups);
+ sort_map_values(&mut active_groups);
+ sort_map_values(&mut reserved_groups);
sort_map_values(&mut platforms);
public.sort_unstable();
private.sort_unstable();
@@ -750,11 +876,11 @@ fn render_projections(catalog: &Catalog, digest: &str) -> Vec<GeneratedArtifact>
vec![
GeneratedArtifact {
relative: GROUPS_RELATIVE,
- contents: render_map_projection(
- "radroots.workspace.package-groups.v1",
+ contents: render_group_projection(
digest,
- "group",
&groups,
+ &active_groups,
+ &reserved_groups,
)
.into_bytes(),
},
@@ -781,6 +907,26 @@ fn render_projections(catalog: &Catalog, digest: &str) -> Vec<GeneratedArtifact>
]
}
+fn render_group_projection(
+ digest: &str,
+ groups: &BTreeMap<&str, Vec<&str>>,
+ active: &BTreeMap<&str, Vec<&str>>,
+ reserved: &BTreeMap<&str, Vec<&str>>,
+) -> String {
+ let mut output = format!(
+ "schema = \"radroots.workspace.package-groups.v1\"\ncatalog_sha256 = \"{digest}\"\n"
+ );
+ for (id, packages) in groups {
+ output.push_str(&format!(
+ "\n[[group]]\nid = \"{id}\"\npackages = {}\nactive_packages = {}\nreserved_packages = {}\n",
+ toml_array(packages),
+ toml_array(active.get(id).map(Vec::as_slice).unwrap_or_default()),
+ toml_array(reserved.get(id).map(Vec::as_slice).unwrap_or_default()),
+ ));
+ }
+ output
+}
+
fn render_map_projection(
schema: &str,
digest: &str,
diff --git a/tools/xtask/src/coverage.rs b/tools/xtask/src/coverage.rs
@@ -1108,6 +1108,7 @@ fn read_required_crates(path: &Path) -> Result<Vec<String>, String> {
read_coverage_policy(path)?.required_crates()
}
+#[cfg(test)]
fn read_workspace_crates(workspace_root: &Path) -> Result<Vec<String>, String> {
let packages = read_workspace_packages(workspace_root)?;
Ok(packages.into_iter().map(|(name, _)| name).collect())
@@ -2049,12 +2050,16 @@ fn read_gate_report(path: &Path) -> Result<CoverageGateReport, String> {
fn list_required_crates_with_root(root: &Path, writer: &mut dyn Write) -> Result<(), String> {
let required_path = coverage_policy_path(root);
- let crates = read_required_crates(&required_path)?;
+ let policy_crates = read_required_crates(&required_path)?;
+ let crates = crate::catalog::active_group(root, "coverage_required")?;
+ if crates.iter().collect::<BTreeSet<_>>() != policy_crates.iter().collect::<BTreeSet<_>>() {
+ return Err("coverage policy required crates drifted from the catalog".to_owned());
+ }
write_crate_names_output(writer, crates, "required crates")
}
fn list_workspace_crates_with_root(root: &Path, writer: &mut dyn Write) -> Result<(), String> {
- let crates = read_workspace_crates(root)?;
+ let crates = crate::catalog::active_packages(root)?;
write_crate_names_output(writer, crates, "workspace crates")
}
@@ -5528,7 +5533,7 @@ test_threads = 0
let workspace_err = list_workspace_crates_with_root(&root, &mut output)
.expect_err("missing workspace manifest should fail");
- assert!(workspace_err.contains("failed to read"));
+ assert!(workspace_err.contains("inspect artifact path"));
fs::remove_dir_all(root).expect("remove list helper root");
}
diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs
@@ -12,6 +12,8 @@ mod api_qualification;
#[cfg_attr(coverage_nightly, coverage(off))]
mod architecture;
#[cfg_attr(coverage_nightly, coverage(off))]
+mod build_control;
+#[cfg_attr(coverage_nightly, coverage(off))]
mod catalog;
#[cfg_attr(coverage_nightly, coverage(off))]
mod consolidation;
@@ -36,16 +38,228 @@ mod supply_chain_qualification;
#[cfg_attr(coverage_nightly, coverage(off))]
mod target_qualification;
+use clap::{Parser, Subcommand, ValueEnum};
use std::env;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
+#[derive(Debug, Parser)]
+#[command(name = "xtask", disable_help_subcommand = true)]
+struct Cli {
+ #[command(subcommand)]
+ command: XtaskCommand,
+}
+
+#[derive(Debug, Subcommand)]
+enum XtaskCommand {
+ Architecture,
+ ArchitectureCi,
+ ArchitectureSourceExportCi,
+ CheckApiBoundaries,
+ CheckDependencyBoundaries,
+ Check {
+ #[arg(long)]
+ group: String,
+ #[arg(long, value_enum, default_value_t = GroupOperation::Check)]
+ operation: GroupOperation,
+ #[arg(long)]
+ execute: bool,
+ #[arg(long)]
+ include_reserved: bool,
+ },
+ Catalog {
+ #[command(subcommand)]
+ command: CatalogCommand,
+ },
+ #[command(trailing_var_arg = true)]
+ Contract {
+ #[arg(allow_hyphen_values = true)]
+ args: Vec<String>,
+ },
+ #[command(trailing_var_arg = true)]
+ Consolidation {
+ #[arg(allow_hyphen_values = true)]
+ args: Vec<String>,
+ },
+ #[command(trailing_var_arg = true)]
+ Coverage {
+ #[arg(allow_hyphen_values = true)]
+ args: Vec<String>,
+ },
+ #[command(name = "dto-roots", trailing_var_arg = true)]
+ DtoRoots {
+ #[arg(allow_hyphen_values = true)]
+ args: Vec<String>,
+ },
+ #[command(trailing_var_arg = true)]
+ Generate {
+ #[arg(allow_hyphen_values = true)]
+ args: Vec<String>,
+ },
+ #[command(trailing_var_arg = true)]
+ Hygiene {
+ #[arg(allow_hyphen_values = true)]
+ args: Vec<String>,
+ },
+ #[command(trailing_var_arg = true)]
+ Release {
+ #[arg(allow_hyphen_values = true)]
+ args: Vec<String>,
+ },
+ SourceLock {
+ #[arg(long)]
+ consumer_root: PathBuf,
+ },
+ Source {
+ #[command(subcommand)]
+ command: SourceCommand,
+ },
+ Artifact {
+ #[arg(long, value_enum)]
+ product: ArtifactProduct,
+ #[arg(long, value_enum)]
+ target: ArtifactTarget,
+ #[arg(long, value_enum)]
+ language: ArtifactLanguage,
+ #[arg(long, value_enum)]
+ mode: ArtifactMode,
+ #[arg(long)]
+ consumer_root: PathBuf,
+ #[arg(long)]
+ source_root: PathBuf,
+ #[arg(long)]
+ output: PathBuf,
+ #[arg(long)]
+ source_date_epoch: u64,
+ #[arg(long)]
+ builder_id: String,
+ #[arg(long, value_delimiter = ',')]
+ features: Vec<String>,
+ },
+}
+
+#[derive(Clone, Copy, Debug, Subcommand)]
+enum CatalogCommand {
+ Check,
+ Write,
+}
+
+#[derive(Clone, Copy, Debug, ValueEnum)]
+enum GroupOperation {
+ Check,
+ Test,
+ Clippy,
+}
+
+#[derive(Clone, Copy, Debug, ValueEnum)]
+enum ArtifactMode {
+ Check,
+ Write,
+}
+
+#[derive(Clone, Copy, Debug, ValueEnum)]
+enum SourceMode {
+ Prefetch,
+ Offline,
+}
+
+#[derive(Clone, Copy, Debug, ValueEnum)]
+enum ArtifactProduct {
+ Sdk,
+ Mobile,
+ Studio,
+}
+
+impl ArtifactProduct {
+ fn as_str(self) -> &'static str {
+ match self {
+ Self::Sdk => "sdk",
+ Self::Mobile => "mobile",
+ Self::Studio => "studio",
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, ValueEnum)]
+enum ArtifactTarget {
+ Typescript,
+ Wasm,
+ Ffi,
+ Ios,
+ Android,
+ Linux,
+ Macos,
+ Windows,
+}
+
+impl ArtifactTarget {
+ fn as_str(self) -> &'static str {
+ match self {
+ Self::Typescript => "typescript",
+ Self::Wasm => "wasm",
+ Self::Ffi => "ffi",
+ Self::Ios => "ios",
+ Self::Android => "android",
+ Self::Linux => "linux",
+ Self::Macos => "macos",
+ Self::Windows => "windows",
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, ValueEnum)]
+enum ArtifactLanguage {
+ Typescript,
+ Swift,
+ Kotlin,
+ Javascript,
+}
+
+impl ArtifactLanguage {
+ fn as_str(self) -> &'static str {
+ match self {
+ Self::Typescript => "typescript",
+ Self::Swift => "swift",
+ Self::Kotlin => "kotlin",
+ Self::Javascript => "javascript",
+ }
+ }
+}
+
+#[derive(Debug, Subcommand)]
+enum SourceCommand {
+ Materialize {
+ #[arg(long)]
+ consumer_root: PathBuf,
+ #[arg(long)]
+ cache_root: PathBuf,
+ #[arg(long, value_enum)]
+ mode: SourceMode,
+ },
+ ArchiveVerify {
+ #[arg(long)]
+ archive: PathBuf,
+ #[arg(long)]
+ sha256: String,
+ },
+ ArchiveCreate {
+ #[arg(long)]
+ source_root: PathBuf,
+ #[arg(long)]
+ revision: String,
+ #[arg(long)]
+ output: PathBuf,
+ },
+}
+
fn usage() {
eprintln!("usage:");
eprintln!(" cargo xtask architecture");
eprintln!(" cargo xtask architecture-ci");
+ eprintln!(" cargo xtask architecture-source-export-ci");
eprintln!(" cargo xtask check-api-boundaries");
eprintln!(" cargo xtask check-dependency-boundaries");
+ eprintln!(" cargo xtask check --group <group> [--operation check|test|clippy] [--execute]");
eprintln!(" cargo xtask catalog check|write");
eprintln!(" cargo xtask contract validate");
eprintln!(" cargo xtask contract event-contract-registry-v7 [--write]");
@@ -76,6 +290,17 @@ fn usage() {
" cargo xtask coverage refresh-summary [--reports-root <dir>] [--out <file>] [--status-out <file>]"
);
eprintln!(" cargo xtask hygiene forbidden-identifiers");
+ eprintln!(" cargo xtask source-lock --consumer-root <absolute-directory>");
+ eprintln!(
+ " cargo xtask source materialize --consumer-root <absolute-directory> --cache-root <absolute-directory> --mode <prefetch|offline>"
+ );
+ eprintln!(" cargo xtask source archive-verify --archive <bundle> --sha256 <digest>");
+ eprintln!(
+ " cargo xtask source archive-create --source-root <absolute-directory> --revision <full-sha> --output <absolute-bundle>"
+ );
+ eprintln!(
+ " cargo xtask artifact --product <sdk|mobile|studio> --target <target> --language <language> --mode <check|write> --consumer-root <absolute-directory> --source-root <absolute-directory> --output <relative-path> --source-date-epoch <seconds> --builder-id <id>"
+ );
}
fn workspace_root_with_override(override_root: Option<&str>) -> PathBuf {
@@ -127,6 +352,10 @@ fn release_preflight() -> Result<(), String> {
}
fn release_preflight_at(root: &Path) -> Result<(), String> {
+ catalog::check(root)?;
+ for group in ["public_native", "preview", "tools"] {
+ build_control::group_plan(root, group, build_control::Operation::Check, false)?;
+ }
dto_roots::check(root)?;
generate::protocol::check(root)?;
contract::validate_artifact_contracts(root)?;
@@ -172,28 +401,112 @@ fn run_contract(args: &[String]) -> Result<(), String> {
}
fn run(args: &[String]) -> Result<(), String> {
- match args.first().map(String::as_str) {
- Some("architecture") if args.len() == 1 => architecture::validate(&workspace_root()),
- Some("architecture-ci") if args.len() == 1 => {
+ let cli = Cli::try_parse_from(std::iter::once("xtask").chain(args.iter().map(String::as_str)))
+ .map_err(|error| error.to_string())?;
+ match cli.command {
+ XtaskCommand::Architecture => architecture::validate(&workspace_root()),
+ XtaskCommand::ArchitectureCi => {
catalog::check(&workspace_root())?;
architecture::validate_ci(&workspace_root())?;
validate_contract()
}
- Some("check-api-boundaries") if args.len() == 1 => {
+ XtaskCommand::ArchitectureSourceExportCi => {
+ catalog::check_source_export(&workspace_root())?;
+ architecture::validate_ci(&workspace_root())?;
+ validate_contract()
+ }
+ XtaskCommand::CheckApiBoundaries => {
architecture::validate_api_boundaries(&workspace_root())
}
- Some("check-dependency-boundaries") if args.len() == 1 => {
+ XtaskCommand::CheckDependencyBoundaries => {
architecture::validate_dependency_boundaries(&workspace_root())
}
- Some("catalog") => catalog::run(&args[1..], &workspace_root()),
- Some("contract") => run_contract(&args[1..]),
- Some("consolidation") => consolidation::run(&args[1..], &workspace_root()),
- Some("coverage") => coverage::run(&args[1..]),
- Some("dto-roots") => dto_roots::run(&args[1..], &workspace_root()),
- Some("generate") => generate::run(&args[1..], &workspace_root()),
- Some("hygiene") => hygiene::run(&args[1..], &workspace_root()),
- Some("release") => run_release(&args[1..]),
- _ => Err("unknown command".to_string()),
+ XtaskCommand::Check {
+ group,
+ operation,
+ execute,
+ include_reserved,
+ } => {
+ let operation = match operation {
+ GroupOperation::Check => build_control::Operation::Check,
+ GroupOperation::Test => build_control::Operation::Test,
+ GroupOperation::Clippy => build_control::Operation::Clippy,
+ };
+ let root = workspace_root();
+ let plan = build_control::group_plan(&root, &group, operation, include_reserved)?;
+ if execute {
+ build_control::execute_group_plan(&root, &plan)
+ } else {
+ build_control::print_plan(&plan);
+ Ok(())
+ }
+ }
+ XtaskCommand::Catalog { command } => match command {
+ CatalogCommand::Check => catalog::run(&["check".to_owned()], &workspace_root()),
+ CatalogCommand::Write => catalog::run(&["write".to_owned()], &workspace_root()),
+ },
+ XtaskCommand::Contract { args } => run_contract(&args),
+ XtaskCommand::Consolidation { args } => consolidation::run(&args, &workspace_root()),
+ XtaskCommand::Coverage { args } => coverage::run(&args),
+ XtaskCommand::DtoRoots { args } => dto_roots::run(&args, &workspace_root()),
+ XtaskCommand::Generate { args } => generate::run(&args, &workspace_root()),
+ XtaskCommand::Hygiene { args } => hygiene::run(&args, &workspace_root()),
+ XtaskCommand::Release { args } => run_release(&args),
+ XtaskCommand::SourceLock { consumer_root } => {
+ build_control::validate_consumer(&consumer_root).map(|_| ())
+ }
+ XtaskCommand::Source { command } => match command {
+ SourceCommand::Materialize {
+ consumer_root,
+ cache_root,
+ mode,
+ } => build_control::materialize(
+ &consumer_root,
+ &cache_root,
+ matches!(mode, SourceMode::Offline),
+ )
+ .map(|path| {
+ println!("{}", path.display());
+ }),
+ SourceCommand::ArchiveVerify { archive, sha256 } => {
+ build_control::verify_source_archive(&archive, &sha256)
+ }
+ SourceCommand::ArchiveCreate {
+ source_root,
+ revision,
+ output,
+ } => build_control::create_source_archive(&source_root, &revision, &output).map(
+ |digest| {
+ println!("{digest}");
+ },
+ ),
+ },
+ XtaskCommand::Artifact {
+ product,
+ target,
+ language,
+ mode,
+ consumer_root,
+ source_root,
+ output,
+ source_date_epoch,
+ builder_id,
+ features,
+ } => build_control::artifact(
+ product.as_str(),
+ target.as_str(),
+ language.as_str(),
+ match mode {
+ ArtifactMode::Check => build_control::Mode::Check,
+ ArtifactMode::Write => build_control::Mode::Write,
+ },
+ &consumer_root,
+ &source_root,
+ &output,
+ source_date_epoch,
+ &builder_id,
+ &features,
+ ),
}
}
@@ -263,6 +576,47 @@ mod tests {
}
#[test]
+ fn typed_build_control_cli_requires_explicit_modes_and_known_values() {
+ let source_args = [
+ "xtask",
+ "source",
+ "materialize",
+ "--consumer-root",
+ "/tmp/consumer",
+ "--cache-root",
+ "/tmp/cache",
+ ];
+ assert!(Cli::try_parse_from(source_args).is_err());
+ assert!(Cli::try_parse_from(source_args.into_iter().chain(["--mode", "prefetch"])).is_ok());
+
+ assert!(
+ Cli::try_parse_from([
+ "xtask",
+ "artifact",
+ "--product",
+ "unknown",
+ "--target",
+ "wasm",
+ "--language",
+ "javascript",
+ "--mode",
+ "check",
+ "--consumer-root",
+ "/tmp/consumer",
+ "--source-root",
+ "/tmp/source",
+ "--output",
+ "generated/manifest.json",
+ "--source-date-epoch",
+ "1",
+ "--builder-id",
+ "fixture",
+ ])
+ .is_err()
+ );
+ }
+
+ #[test]
fn run_release_and_dispatchers_cover_error_paths() {
let unknown_release =
run_release(&["unknown".to_string()]).expect_err("unknown release subcommand");
@@ -278,7 +632,7 @@ mod tests {
.expect_err("invalid registry-v7 mode");
assert!(invalid_registry.contains("exactly --write"));
let unknown_root = run(&["unknown".to_string()]).expect_err("unknown command");
- assert!(unknown_root.contains("unknown command"));
+ assert!(unknown_root.contains("unrecognized subcommand"));
run(&["architecture".to_string()]).expect("architecture ledger validates");
@@ -291,15 +645,15 @@ mod tests {
let removed_sdk = run(&["sdk".to_string(), "validate".to_string()])
.expect_err("removed sdk command namespace");
- assert!(removed_sdk.contains("unknown command"));
+ assert!(removed_sdk.contains("unrecognized subcommand"));
}
#[test]
- fn release_preflight_checks_dto_root_authority_first() {
+ fn release_preflight_checks_catalog_authority_first() {
let workspace = tempfile::TempDir::new().expect("create empty workspace");
let error = release_preflight_at(workspace.path())
- .expect_err("missing DTO root authority must fail first");
- assert!(error.contains("DTO root authority"));
+ .expect_err("missing catalog authority must fail first");
+ assert!(error.contains("inspect artifact path") && error.contains("contracts"));
}
#[test]