use std::path::{Path, PathBuf};
use crate::core::builder_slot_pool::SlotPool;
use crate::core::builders::BuildersConfig;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct SharedDirs {
pub roots: Vec<PathBuf>,
}
impl SharedDirs {
#[must_use]
pub fn resolve(config: &BuildersConfig, home: &Path) -> Self {
let mut roots = vec![
config.effective_slot_pool_root(home),
home.join(trusty_common::crate_config::TRUSTY_TOOLS_DIR)
.join(crate::core::build_env::CARGO_TARGET_SUBDIR),
];
let configured = load_build_config(home)
.and_then(|b| b.cargo_target_dir)
.map(|t| trusty_common::workspace_layout::expand_tilde(&t, home));
roots.extend(configured);
Self { roots }
}
#[must_use]
pub fn contains(&self, dir: &Path) -> bool {
let canon = |p: &Path| std::fs::canonicalize(p).ok();
self.roots.iter().any(|root| {
dir.starts_with(root)
|| matches!((canon(dir), canon(root)), (Some(d), Some(r)) if d.starts_with(&r))
})
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum TargetPlan {
Keep(String),
Unset,
Slot {
pool: SlotPool,
clone_from: Option<PathBuf>,
},
Refuse(String),
}
#[must_use]
pub fn plan(
ambient: Option<&str>,
shared: &SharedDirs,
pool: Result<(SlotPool, Option<PathBuf>), String>,
) -> TargetPlan {
let Some(dir) = ambient.filter(|d| !d.trim().is_empty()) else {
return TargetPlan::Unset;
};
if !shared.contains(Path::new(dir)) {
return TargetPlan::Keep(dir.to_string());
}
match pool {
Ok((pool, clone_from)) => TargetPlan::Slot { pool, clone_from },
Err(why) => TargetPlan::Refuse(format!(
"CARGO_TARGET_DIR={dir} is a shared target directory that concurrent builds \
overwrite, and no private slot directory can replace it ({why}). Set a private \
CARGO_TARGET_DIR for this build, or add a git `origin` to the checkout"
)),
}
}
#[must_use]
pub fn explicit_target_dir_arg(argv: &[String]) -> Option<&str> {
let mut found = None;
let mut i = 0;
while i < argv.len() {
let tok = argv[i].as_str();
if tok == "--" {
break;
}
if tok == "--target-dir" {
if let Some(value) = argv.get(i + 1) {
found = Some(value.as_str());
}
i += 2;
continue;
}
if let Some(value) = tok.strip_prefix("--target-dir=") {
found = Some(value);
}
i += 1;
}
found
}
#[must_use]
pub fn rewrite_target_dir_arg(argv: &[String], new_dir: &str) -> Vec<String> {
let mut out = argv.to_vec();
let mut separated: Option<usize> = None;
let mut joined: Option<usize> = None;
let mut i = 0;
while i < out.len() {
if out[i] == "--" {
break;
}
if out[i] == "--target-dir" && i + 1 < out.len() {
separated = Some(i + 1);
joined = None;
i += 2;
continue;
}
if out[i].starts_with("--target-dir=") {
joined = Some(i);
separated = None;
}
i += 1;
}
if let Some(idx) = separated {
out[idx] = new_dir.to_string();
} else if let Some(idx) = joined {
out[idx] = format!("--target-dir={new_dir}");
}
out
}
fn load_build_config(home: &Path) -> Option<crate::core::build_env::BuildConfig> {
trusty_common::crate_config::load_at::<crate::core::trusty_tools_config::TrustyToolsConfig>(
&trusty_common::crate_config::crate_config_path_at(
home,
crate::core::trusty_tools_config::CRATE_NAME,
),
)
.ok()
.flatten()
.and_then(|c| c.build)
}
pub fn resolve_pool(
config: &BuildersConfig,
home: &Path,
checkout: &Path,
) -> Result<(SlotPool, Option<PathBuf>), String> {
let identity = trusty_common::github_path::derive_github_path(checkout).ok_or_else(|| {
format!(
"{} has no git origin identity to key a slot directory by",
checkout.display()
)
})?;
let shared = crate::core::build_env::resolve_build_env(
load_build_config(home).as_ref(),
home,
Some(&identity),
crate::core::build_env::host_cores(),
)
.ok()
.map(|env| env.cargo_target_dir);
let ceiling = config.effective_max_concurrent(crate::core::builders::host_memory_tier());
Ok((
SlotPool::new(config.effective_slot_pool_root(home), identity, ceiling),
shared,
))
}
#[cfg(test)]
mod tests {
use super::*;
fn shared() -> SharedDirs {
SharedDirs {
roots: vec![PathBuf::from("/shared/target"), PathBuf::from("/pool")],
}
}
fn pool() -> Result<(SlotPool, Option<PathBuf>), String> {
let id = trusty_common::github_path::GithubPath {
owner: "o".into(),
repo: "r".into(),
};
Ok((SlotPool::new(PathBuf::from("/pool"), id, 4), None))
}
#[test]
fn an_explicit_target_dir_is_kept() {
let got = plan(Some("/work/target-mine"), &shared(), pool());
assert!(matches!(got, TargetPlan::Keep(d) if d == "/work/target-mine"));
}
#[test]
fn the_shared_dir_is_replaced_by_the_slot() {
for ambient in ["/shared/target", "/shared/target/", "/shared/target/o/r"] {
let got = plan(Some(ambient), &shared(), pool());
assert!(
matches!(got, TargetPlan::Slot { .. }),
"{ambient:?}: {got:?}"
);
}
}
#[test]
fn an_unset_target_dir_is_left_to_cargo() {
assert!(matches!(plan(None, &shared(), pool()), TargetPlan::Unset));
assert!(matches!(
plan(Some(" "), &shared(), pool()),
TargetPlan::Unset
));
}
#[test]
fn a_shared_dir_without_a_pool_is_refused() {
let got = plan(Some("/shared/target"), &shared(), Err("no origin".into()));
assert!(
matches!(&got, TargetPlan::Refuse(why) if why.contains("no origin")),
"{got:?}"
);
}
#[test]
fn an_ambient_pool_slot_counts_as_shared() {
let got = plan(Some("/pool/o/r/slot-0"), &shared(), pool());
assert!(matches!(got, TargetPlan::Slot { .. }), "{got:?}");
}
#[test]
fn shared_dirs_need_no_repo_identity() {
let tmp = tempfile::tempdir().expect("tempdir");
let dirs = SharedDirs::resolve(&BuildersConfig::default(), tmp.path());
assert!(dirs.contains(&tmp.path().join(".trusty-tools/cargo-target/any/repo")));
assert!(
dirs.contains(
&tmp.path()
.join(".trusty-tools/cargo-target-pool/o/r/slot-3")
)
);
assert!(!dirs.contains(&tmp.path().join("private-target")));
}
#[test]
#[serial_test::serial(build_slot_fds)] fn a_checkout_without_an_origin_has_no_pool() {
let tmp = tempfile::tempdir().expect("tempdir");
let err = resolve_pool(&BuildersConfig::default(), tmp.path(), tmp.path())
.expect_err("a bare directory has no origin");
assert!(err.contains("no git origin identity"), "{err}");
}
fn argv(words: &[&str]) -> Vec<String> {
words.iter().map(ToString::to_string).collect()
}
#[test]
fn an_explicit_target_dir_flag_is_read_from_argv() {
assert_eq!(
explicit_target_dir_arg(&argv(&["cargo", "build", "--target-dir", "/shared"])),
Some("/shared")
);
assert_eq!(
explicit_target_dir_arg(&argv(&["cargo", "build", "--target-dir=/shared"])),
Some("/shared")
);
assert_eq!(
explicit_target_dir_arg(&argv(&[
"cargo",
"build",
"--target-dir",
"/first",
"--target-dir",
"/second"
])),
Some("/second")
);
assert_eq!(
explicit_target_dir_arg(&argv(&["cargo", "build", "-p", "x"])),
None
);
}
#[test]
fn an_explicit_target_dir_flag_is_rewritten_to_the_slot() {
assert_eq!(
rewrite_target_dir_arg(
&argv(&["cargo", "build", "--target-dir", "/shared"]),
"/pool/slot-0"
),
argv(&["cargo", "build", "--target-dir", "/pool/slot-0"])
);
assert_eq!(
rewrite_target_dir_arg(
&argv(&["cargo", "build", "--target-dir=/shared"]),
"/pool/slot-0"
),
argv(&["cargo", "build", "--target-dir=/pool/slot-0"])
);
assert_eq!(
rewrite_target_dir_arg(&argv(&["cargo", "build", "-p", "x"]), "/pool/slot-0"),
argv(&["cargo", "build", "-p", "x"])
);
}
#[test]
fn an_explicit_target_dir_flag_stops_at_the_double_dash() {
assert_eq!(
explicit_target_dir_arg(&argv(&[
"cargo",
"test",
"--target-dir",
"/shared",
"--",
"--target-dir",
"x"
])),
Some("/shared")
);
assert_eq!(
explicit_target_dir_arg(&argv(&["cargo", "test", "--", "--target-dir", "X"])),
None
);
}
#[test]
fn rewriting_the_target_dir_flag_leaves_everything_after_double_dash_untouched() {
let input = argv(&[
"cargo",
"test",
"--target-dir",
"/shared",
"--",
"--target-dir",
"x",
]);
assert_eq!(
rewrite_target_dir_arg(&input, "/pool/slot-0"),
argv(&[
"cargo",
"test",
"--target-dir",
"/pool/slot-0",
"--",
"--target-dir",
"x",
])
);
let no_pre_dash_flag = argv(&["cargo", "test", "--", "--target-dir", "X"]);
assert_eq!(
rewrite_target_dir_arg(&no_pre_dash_flag, "/pool/slot-0"),
no_pre_dash_flag
);
}
}