lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit da0bf7c5d3e2293f9e6e5a75a4609e58c4bb59c8
parent 5c13921aae0899ad2e33aa82dc6c2dcdf1aec84a
Author: triesap <tyson@radroots.org>
Date:   Thu,  6 Aug 2026 20:05:07 +0000

build: support nested consumer lockfiles

- allow source locks to declare one contained relative Cargo lockfile
- retain Cargo.lock as the default for existing consumers
- exclude generated build trees from manifest validation
- test nested paths and reject repository escape attempts

Diffstat:
Mtools/xtask/src/build_control.rs | 48++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 46 insertions(+), 2 deletions(-)

diff --git a/tools/xtask/src/build_control.rs b/tools/xtask/src/build_control.rs @@ -67,9 +67,15 @@ pub struct SourceLock { pub workspace_catalog_sha256: String, pub version: String, pub source_archive_sha256: Option<String>, + #[serde(default = "default_consumer_lockfile")] + pub lockfile: String, pub lockfile_sha256: String, } +fn default_consumer_lockfile() -> String { + "Cargo.lock".to_owned() +} + #[derive(Clone, Debug)] pub struct ConsumerRoot { path: PathBuf, @@ -663,6 +669,7 @@ fn validate_source_lock(source_lock: &SourceLock) -> Result<(), String> { "source lock catalog digest", )?; validate_sha256(&source_lock.lockfile_sha256, "source lock lockfile digest")?; + validate_relative_path(Path::new(&source_lock.lockfile), "source lock lockfile")?; if let Some(digest) = &source_lock.source_archive_sha256 { validate_sha256(digest, "source lock archive digest")?; } @@ -670,7 +677,9 @@ fn validate_source_lock(source_lock: &SourceLock) -> Result<(), String> { } fn validate_consumer_files(root: &Path, source_lock: &SourceLock) -> Result<(), String> { - let lockfile = root.join("Cargo.lock"); + let lockfile_relative = Path::new(&source_lock.lockfile); + ensure_no_symlink_components(root, lockfile_relative)?; + let lockfile = root.join(lockfile_relative); 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()); @@ -736,7 +745,16 @@ fn collect_manifests(root: &Path, current: &Path, output: &mut Vec<PathBuf>) -> if file_type.is_dir() { if matches!( name.to_str(), - Some(".git" | "target" | "node_modules" | ".radroots") + Some( + ".git" + | ".gradle" + | ".kotlin" + | ".radroots" + | "build" + | "node_modules" + | "out" + | "target" + ) ) { continue; } @@ -1110,6 +1128,7 @@ mod tests { workspace_catalog_sha256: catalog_sha256, version: VERSION.to_owned(), source_archive_sha256: None, + lockfile: default_consumer_lockfile(), lockfile_sha256: sha256(consumer_lock.as_bytes()), }; fs::write( @@ -1144,6 +1163,31 @@ mod tests { } #[test] + fn source_lock_supports_a_contained_nested_lockfile() { + let mut fixture = Fixture::new("studio"); + let core = fixture.consumer.join("core"); + fs::create_dir(&core).expect("create nested capsule"); + fs::rename(fixture.consumer.join("Cargo.lock"), core.join("Cargo.lock")) + .expect("move lockfile"); + fixture.source_lock.lockfile = "core/Cargo.lock".to_owned(); + fs::write( + fixture.consumer.join(SOURCE_LOCK_NAME), + toml::to_string(&fixture.source_lock).expect("serialize nested source lock"), + ) + .expect("write nested source lock"); + + ConsumerRoot::open(&fixture.consumer).expect("nested lockfile is valid"); + + fixture.source_lock.lockfile = "../Cargo.lock".to_owned(); + fs::write( + fixture.consumer.join(SOURCE_LOCK_NAME), + toml::to_string(&fixture.source_lock).expect("serialize escaping source lock"), + ) + .expect("write escaping source lock"); + 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");