commit 7538aebccf4f868fb6ac57f42e5e9a813624b11b
parent ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb
Author: triesap <tyson@radroots.org>
Date: Tue, 1 Sep 2026 02:50:46 +0000
xtask: recognize SwiftPM build output
- centralize exact build-output directory classification
- exclude SwiftPM .build from consumer manifest discovery
- retain traversal for lookalike source directory names
- cover mobile and service source-lock walkers
Diffstat:
4 files changed, 84 insertions(+), 17 deletions(-)
diff --git a/tools/xtask/src/build_control.rs b/tools/xtask/src/build_control.rs
@@ -10,6 +10,8 @@ use fs2::FileExt;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
+use crate::build_output::is_build_output_directory;
+
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.v2.toml";
@@ -741,19 +743,9 @@ fn collect_manifests(root: &Path, current: &Path, output: &mut Vec<PathBuf>) ->
}
let name = entry.file_name();
if file_type.is_dir() {
- if matches!(
- name.to_str(),
- Some(
- ".git"
- | ".gradle"
- | ".kotlin"
- | ".radroots"
- | "build"
- | "node_modules"
- | "out"
- | "target"
- )
- ) {
+ if matches!(name.to_str(), Some(".git" | ".radroots"))
+ || is_build_output_directory(name.as_os_str())
+ {
continue;
}
collect_manifests(root, &entry.path(), output)?;
@@ -1196,6 +1188,22 @@ mod tests {
}
#[test]
+ fn consumer_manifest_discovery_skips_swiftpm_build_output_only() {
+ let fixture = Fixture::new("mobile");
+ let swiftpm_checkout = fixture.consumer.join(".build/checkouts/dependency");
+ fs::create_dir_all(&swiftpm_checkout).expect("SwiftPM checkout directory");
+ fs::write(swiftpm_checkout.join("Cargo.toml"), "not valid TOML")
+ .expect("SwiftPM build manifest");
+ ConsumerRoot::open(&fixture.consumer).expect("SwiftPM build output is excluded");
+
+ let source_lookalike = fixture.consumer.join(".builder");
+ fs::create_dir(&source_lookalike).expect("source lookalike directory");
+ fs::write(source_lookalike.join("Cargo.toml"), "not valid TOML")
+ .expect("source lookalike 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");
diff --git a/tools/xtask/src/build_output.rs b/tools/xtask/src/build_output.rs
@@ -0,0 +1,38 @@
+use std::ffi::OsStr;
+
+pub(crate) fn is_build_output_directory(name: &OsStr) -> bool {
+ matches!(
+ name.to_str(),
+ Some(".build" | ".gradle" | ".kotlin" | "build" | "node_modules" | "out" | "target")
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn recognizes_only_exact_build_output_directory_names() {
+ for name in [
+ ".build",
+ ".gradle",
+ ".kotlin",
+ "build",
+ "node_modules",
+ "out",
+ "target",
+ ] {
+ assert!(is_build_output_directory(OsStr::new(name)), "{name}");
+ }
+ for name in [
+ ".builder",
+ ".build-output",
+ "builds",
+ "node_module",
+ "output",
+ "targets",
+ ] {
+ assert!(!is_build_output_directory(OsStr::new(name)), "{name}");
+ }
+ }
+}
diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs
@@ -13,6 +13,7 @@ mod api_qualification;
mod architecture;
#[cfg_attr(coverage_nightly, coverage(off))]
mod build_control;
+mod build_output;
#[cfg_attr(coverage_nightly, coverage(off))]
mod catalog;
#[cfg_attr(coverage_nightly, coverage(off))]
diff --git a/tools/xtask/src/service_source_lock_command.rs b/tools/xtask/src/service_source_lock_command.rs
@@ -12,6 +12,7 @@ use sha2::{Digest as _, Sha256};
use tempfile::{NamedTempFile, TempDir};
use walkdir::WalkDir;
+use crate::build_output::is_build_output_directory;
use crate::service_source_lock::{
ContractVersions, LIB_REPOSITORY, LOCK_FILENAME, NixMaterialParts, NixMaterialState,
PREDECESSOR_LOCK_FILENAME, ServiceSourceLockParts, ServiceSourceLockV2,
@@ -415,10 +416,7 @@ fn validate_cargo_manifests(
.follow_links(false)
.into_iter()
.filter_entry(|entry| {
- !matches!(
- entry.file_name().to_str(),
- Some(".git" | ".gradle" | ".kotlin" | "build" | "node_modules" | "out" | "target")
- )
+ entry.file_name() != OsStr::new(".git") && !is_build_output_directory(entry.file_name())
});
for entry in walker {
entries = entries
@@ -1821,6 +1819,28 @@ name = "radroots_service_host"
}
#[test]
+ fn service_manifest_discovery_skips_swiftpm_build_output_only() {
+ let fixture = Fixture::new();
+ let swiftpm_checkout = fixture.service.join(".build/checkouts/dependency");
+ fs::create_dir_all(&swiftpm_checkout).expect("SwiftPM checkout directory");
+ fs::write(swiftpm_checkout.join(CARGO_MANIFEST), "not valid TOML")
+ .expect("SwiftPM build manifest");
+ assert_eq!(
+ validate_cargo_manifests(&fixture.service, None),
+ Ok(fixture.revision)
+ );
+
+ let source_lookalike = fixture.service.join(".builder");
+ fs::create_dir(&source_lookalike).expect("source lookalike directory");
+ fs::write(source_lookalike.join(CARGO_MANIFEST), "not valid TOML")
+ .expect("source lookalike manifest");
+ assert_eq!(
+ validate_cargo_manifests(&fixture.service, None),
+ Err(CommandError::InvalidCargoManifest)
+ );
+ }
+
+ #[test]
fn source_lock_output_rejects_symlink_replacement() {
let fixture = Fixture::new();
let target = fixture.root.path().join("foreign");