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:
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");