use anyhow::Context as _;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
pub fn resolve(manifest_path: &Path, cargo_target_dir: Option<&OsStr>) -> anyhow::Result<PathBuf> {
let mut command = std::process::Command::new("cargo");
command
.args(["metadata", "--format-version", "1", "--no-deps", "--manifest-path"])
.arg(manifest_path);
if let Some(target_dir) = cargo_target_dir {
command.env("CARGO_TARGET_DIR", target_dir);
}
let output = command.output().with_context(|| {
format!(
"failed to run `cargo metadata --manifest-path {}`",
manifest_path.display()
)
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"`cargo metadata --manifest-path {}` failed: {}",
manifest_path.display(),
stderr.trim()
);
}
let json = String::from_utf8_lossy(&output.stdout);
from_metadata(&json).with_context(|| format!("resolving the target directory for {}", manifest_path.display()))
}
pub fn from_metadata(metadata_json: &str) -> anyhow::Result<PathBuf> {
let metadata: serde_json::Value =
serde_json::from_str(metadata_json).context("failed to parse cargo metadata JSON")?;
let target_directory = metadata["target_directory"]
.as_str()
.context("cargo metadata JSON missing `target_directory`")?;
Ok(PathBuf::from(target_directory))
}
pub fn for_workspace_root(workspace_root: &Path) -> PathBuf {
match resolve(&workspace_root.join("Cargo.toml"), None) {
Ok(target_dir) => target_dir,
Err(error) => {
let fallback = std::env::var_os("CARGO_TARGET_DIR")
.filter(|value| !value.is_empty())
.map_or_else(|| workspace_root.join("target"), |value| workspace_root.join(value));
tracing::debug!(
workspace_root = %workspace_root.display(),
fallback = %fallback.display(),
"cargo metadata could not report a target directory ({error:#}); falling back"
);
fallback
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_an_external_target_directory_out_of_metadata_json() {
let json = r#"{"target_directory": "/var/tmp/some-external-target"}"#;
assert_eq!(
from_metadata(json).expect("parse metadata"),
PathBuf::from("/var/tmp/some-external-target")
);
}
#[test]
fn errors_when_target_directory_is_missing() {
let err = from_metadata(r#"{"packages": []}"#).unwrap_err();
assert!(
format!("{err:#}").contains("missing `target_directory`"),
"unexpected error: {err:#}"
);
}
#[test]
fn errors_on_invalid_json() {
let err = from_metadata("not json").unwrap_err();
assert!(
format!("{err:#}").contains("failed to parse cargo metadata JSON"),
"unexpected error: {err:#}"
);
}
#[test]
fn for_workspace_root_follows_a_cargo_config_target_dir() {
let temp = tempfile::tempdir().expect("tempdir");
let root = temp.path().canonicalize().unwrap_or_else(|_| temp.path().to_path_buf());
std::fs::create_dir_all(root.join("src")).expect("create src");
std::fs::create_dir_all(root.join(".cargo")).expect("create .cargo");
std::fs::write(
root.join("Cargo.toml"),
"[package]\nname = \"target-dir-fixture\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.expect("write Cargo.toml");
std::fs::write(root.join("src/lib.rs"), "pub fn noop() {}\n").expect("write lib.rs");
let external = root.join("external-target");
std::fs::write(
root.join(".cargo/config.toml"),
format!("[build]\ntarget-dir = \"{}\"\n", external.display()),
)
.expect("write .cargo/config.toml");
let resolved = {
let _target_dir_guard = crate::test_support::ClearedCargoTargetDirGuard::clear();
let _cwd = crate::test_support::CwdGuard::enter(&root);
for_workspace_root(&root)
};
assert_eq!(
resolved,
external,
"the resolver must report the `.cargo/config.toml` target-dir, not {}",
root.join("target").display()
);
}
}