use crate::config::Config;
use crate::target;
use bytesize::ByteSize;
use eyre::{Result, WrapErr as _};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
#[derive(usage::Args)]
pub(super) struct AdoptArgs {
#[usage(short = 'r', long)]
pub(super) recursive: bool,
#[usage(long)]
pub(super) dry_run: bool,
#[usage(value_name = "PATH")]
pub(super) paths: Vec<PathBuf>,
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Adoption {
Adopted { target: PathBuf, bytes: u64 },
Skipped { target: PathBuf, reason: String },
}
pub(super) fn run(config: &Config, args: &AdoptArgs) -> Result<ExitCode> {
let working_dir = std::env::current_dir()?;
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
let paths = if args.paths.is_empty() {
vec![working_dir.clone()]
} else {
args.paths
.iter()
.map(|path| {
if path.is_absolute() {
path.clone()
} else {
working_dir.join(path)
}
})
.collect()
};
let mut checkouts = Vec::new();
for path in &paths {
if args.recursive {
checkouts.extend(find_checkouts(path)?);
} else {
checkouts.push(path.clone());
}
}
if checkouts.is_empty() {
println!("no target directories to adopt");
return Ok(ExitCode::SUCCESS);
}
let verb = if args.dry_run {
"would adopt"
} else {
"adopted"
};
let mut adopted = 0_u64;
let mut adopted_bytes = 0_u64;
let mut failures = 0_u64;
for checkout in &checkouts {
match adopt_checkout(config, &cargo, checkout, args.dry_run) {
Ok(Adoption::Adopted { target, bytes }) => {
adopted += 1;
adopted_bytes = adopted_bytes.saturating_add(bytes);
println!(
"{verb} {} ({} logical)",
target.display(),
ByteSize::b(bytes).display().iec()
);
}
Ok(Adoption::Skipped { target, reason }) => {
println!("left {} alone: {reason}", target.display());
}
Err(error) => {
failures += 1;
log::error!(
"{} was not adopted: {error:#}",
checkout.join("target").display()
);
}
}
}
if checkouts.len() > 1 {
println!(
"{verb} {adopted} target directories ({} logical)",
ByteSize::b(adopted_bytes).display().iec()
);
}
Ok(if failures == 0 {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
pub(super) fn adopt_checkout(
config: &Config,
cargo: &OsStr,
checkout: &Path,
dry_run: bool,
) -> Result<Adoption> {
let target = checkout.join("target");
let skipped = |reason: String| {
Ok(Adoption::Skipped {
target: target.clone(),
reason,
})
};
match std::fs::symlink_metadata(&target) {
Ok(metadata) if metadata.file_type().is_symlink() => {
return skipped("it is a link, not a directory of outputs".to_string());
}
Ok(metadata) if metadata.is_dir() => {}
Ok(_) => return skipped("it is not a directory".to_string()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return skipped("there is no target directory".to_string());
}
Err(error) => {
return Err(error).wrap_err_with(|| format!("could not inspect {}", target.display()));
}
}
if !checkout.join("Cargo.toml").is_file() {
return skipped("there is no Cargo.toml beside it".to_string());
}
let target_dir_env = std::env::var_os(super::cargo::CARGO_TARGET_DIR_ENV);
let Some(roots) = mbx_cache_cargo::resolve_reported(cargo, &[], checkout, target_dir_env)
else {
return skipped("Cargo could not describe the checkout".to_string());
};
if !same_directory(&roots.workspace_root, checkout) {
return skipped(format!(
"it belongs to a member of the workspace at {}, whose target directory is the one Cargo uses",
roots.workspace_root.display()
));
}
if roots.target_dir_requested || roots.target_dir != roots.workspace_root.join("target") {
return skipped(
"a flag, environment variable, or Cargo configuration names the target directory"
.to_string(),
);
}
let mut config = config.clone();
config.apply_workspace_policy(&roots.workspace_root)?;
if !config.target.views {
return skipped("managed targets are turned off for this checkout".to_string());
}
let target = roots.workspace_root.join("target");
if !target::can_move_existing(&config, &roots.workspace_root, &target) {
return skipped(format!(
"it is not on the same filesystem as the managed target root {}",
config.target.root.display()
));
}
crate::storage::require_local(
&config.target.root,
"managed target directory",
"MBX_TARGET_ROOT",
)?;
if dry_run {
return Ok(Adoption::Adopted {
bytes: target::tree_bytes(&target),
target,
});
}
let outcome = target::adopt_existing(&config, &roots.workspace_root, &target, false)?;
if outcome.managed.is_none() {
eyre::bail!("a managed target directory could not be placed for it");
}
Ok(Adoption::Adopted {
target,
bytes: outcome.adopted_bytes,
})
}
fn same_directory(a: &Path, b: &Path) -> bool {
let resolve = |path: &Path| std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
resolve(a) == resolve(b)
}
pub(super) fn find_checkouts(root: &Path) -> Result<Vec<PathBuf>> {
let mut found = Vec::new();
let mut pending = vec![root.to_path_buf()];
while let Some(directory) = pending.pop() {
let entries = match std::fs::read_dir(&directory) {
Ok(entries) => entries,
Err(error) if directory == root => {
return Err(error)
.wrap_err_with(|| format!("could not search {}", directory.display()));
}
Err(error) => {
log::warn!("{} was not searched: {error}", directory.display());
continue;
}
};
let mut has_manifest = false;
let mut has_target = false;
let mut children = Vec::new();
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
log::warn!("{} was not fully searched: {error}", directory.display());
continue;
}
};
let name = entry.file_name();
let Ok(kind) = entry.file_type() else {
continue;
};
if name == "Cargo.toml" && kind.is_file() {
has_manifest = true;
} else if name == "target" && kind.is_dir() {
has_target = true;
} else if kind.is_dir() && !name.to_string_lossy().starts_with('.') {
children.push(entry.path());
}
}
if has_manifest && has_target {
found.push(directory);
}
children.sort();
pending.extend(children.into_iter().rev());
}
Ok(found)
}