#![doc = include_str!("../README.md")]
#![warn(clippy::doc_markdown)]
use std::ffi::OsStr;
use std::io::{self, Read, Write};
use std::process::{Command, Stdio};
use std::{env, iter};
use anyhow::Context;
use indexmap::{IndexMap, IndexSet};
use serde::Deserialize;
use serde_json::{Deserializer, Value};
use cargo_metadata::PackageId;
use clap::Parser;
mod spec;
use spec::{cargo_path, resolve_pkg_spec, AnalysedMetadata, PackageSpec};
#[derive(Debug, Parser)]
#[clap(about, version, author)]
struct Cli {
#[clap(flatten)]
manifest: clap_cargo::Manifest,
#[clap(flatten)]
features: clap_cargo::Features,
#[clap(long, short)]
verbose: bool,
#[clap(long, short)]
quiet: bool,
#[clap(long)]
json: bool,
#[clap(
long,
conflicts_with = "json",
conflicts_with = "all",
conflicts_with = "workspace"
)]
no_names: bool,
#[clap(long = "skip-missing")]
skip_missing_outdirs: bool,
#[clap(long = "include-missing", conflicts_with = "skip-missing-outdirs")]
include_missing_outdirs: bool,
#[clap(long, group = "target-packages")]
all: bool,
#[clap(long, group = "target-packages")]
workspace: bool,
#[clap(long, short, group = "target-packages")]
current: bool,
#[clap(group = "target-packages")]
explicit_packages: Vec<String>,
}
impl Cli {
fn target(&self) -> TargetPackages<'_> {
if self.all {
TargetPackages::All
} else if self.workspace {
TargetPackages::Workspace
} else if self.current {
TargetPackages::Current
} else if !self.explicit_packages.is_empty() {
TargetPackages::Explicit(&self.explicit_packages)
} else {
TargetPackages::Current }
}
}
#[derive(Debug)]
enum TargetPackages<'a> {
All,
Workspace,
Current,
Explicit(&'a [String]),
}
impl<'a> TargetPackages<'a> {
fn collect_packages(
&self,
meta: &AnalysedMetadata,
) -> Result<IndexSet<PackageId>, anyhow::Error> {
match *self {
TargetPackages::All => Ok(meta.packages.iter().map(|pkg| &pkg.id).cloned().collect()),
TargetPackages::Workspace => Ok(meta.workspace_members.iter().cloned().collect()),
TargetPackages::Current | TargetPackages::Explicit(_) => {
let specs = self.collect_explicit_package_specs(meta)?;
Ok(specs
.iter()
.map(|spec| meta.find_matching_id(spec))
.cloned()
.collect())
}
}
}
fn collect_explicit_package_specs(
&self,
_meta: &AnalysedMetadata,
) -> Result<Vec<PackageSpec>, anyhow::Error> {
match *self {
TargetPackages::Current => Ok(vec![resolve_pkg_spec(None)?]),
TargetPackages::Explicit(specs) => specs
.iter()
.map(|spec| resolve_pkg_spec(Some(&**spec)))
.collect(),
_ => unreachable!("Not explicit spec: {:?}", self),
}
}
#[inline]
fn is_explicit(&self) -> bool {
matches!(*self, TargetPackages::Explicit(_) | TargetPackages::Current)
}
}
fn main() -> anyhow::Result<()> {
let mut args = env::args_os().peekable();
let binary_name = args.next().expect("Should have binary name"); if args.peek().map(OsStr::new) == Some(OsStr::new("outdir")) {
args.next();
}
let args = iter::once(binary_name).chain(args).collect::<Vec<_>>();
let cli = Cli::parse_from(args);
let mut metadata_command = cli.manifest.metadata();
cli.features.forward_metadata(&mut metadata_command);
let metadata = metadata_command
.exec()
.context("Failed to execute `cargo metadata`")?;
let metadata = AnalysedMetadata::analyse(metadata);
let target = cli.target();
let mut check = Command::new(cargo_path());
let quiet = !cli.verbose && (cli.quiet || atty::isnt(atty::Stream::Stderr));
let include_missing_outdirs = match (cli.include_missing_outdirs, cli.skip_missing_outdirs) {
(true, true) => anyhow::bail!(
"Cannot specify to both include and skip packages that are missing outdirs"
),
(true, false) => true,
(false, true) => false,
(false, false) => {
target.is_explicit() || cli.json
}
};
check.arg("check").arg("--message-format=json");
if matches!(target, TargetPackages::Current) {
} else {
check.arg("--workspace");
}
let desired_packages = target.collect_packages(&metadata)?;
if !quiet {
eprintln!("Running `cargo check`:");
check.stderr(Stdio::inherit());
} else {
check.stderr(Stdio::piped());
}
check.stdout(Stdio::piped());
check.stdin(Stdio::null());
let mut child = check.spawn().context("Failed to spawn `cargo check`")?;
let mut deser = Deserializer::from_reader(child.stdout.take().unwrap()).into_iter::<Value>();
let build_script_reason = serde_json::json!("build-script-executed");
let mut out_dirs = IndexMap::with_capacity(metadata.packages.len());
while let Some(value) = deser
.next()
.transpose()
.context("Failed to read json from `cargo check`")?
{
if let Value::Object(ref map) = value {
if map.get("reason") == Some(&build_script_reason) {
let package_id = PackageId::deserialize(map["package_id"].clone()).unwrap();
let out_dir = match map.get("out_dir") {
Some(&Value::String(ref s)) => Some(s.clone()),
Some(&Value::Null) | None => None,
Some(s) => panic!("Out dir is not a string: {:?}", s),
};
let old = out_dirs.insert(package_id.clone(), out_dir);
if old.is_some() && cli.verbose {
eprintln!(
"Warning: Overriding old out_dir for {:?}: {:?}",
package_id, old
);
}
}
} else if cli.verbose {
eprintln!(
"Expected an object but got a {:?}",
if value.is_array() {
String::from("array")
} else {
format!("{:?}", value)
}
);
}
}
let stderr = child
.stderr
.take()
.map(|mut stderr| {
let mut buf = String::new();
match stderr
.read_to_string(&mut buf)
.context("Failed to handle `cargo check` stderr")
{
Err(e) => Err(e),
Ok(_) => Ok(buf),
}
})
.transpose()?;
let check_status = child.wait().context("`cargo check` exited abnormally")?;
let mut problem = None;
if check_status.success() {
let out_dirs = desired_packages
.iter()
.map(|id| {
(
metadata.determine_spec(id),
out_dirs.get(id).and_then(|o| o.as_ref()),
)
})
.filter(|(_pkg, out_dir)| include_missing_outdirs || out_dir.is_some())
.collect::<IndexMap<_, _>>();
if out_dirs.is_empty() {
problem = Some(Problem::NothingToPrint);
}
if cli.json {
problem = None;
serde_json::to_writer(io::stdout(), &out_dirs).expect("Failed to write output");
io::stdout().write_all(b"\n").unwrap();
} else {
for (&spec, out_dir) in out_dirs.iter() {
if !cli.no_names {
print!("{} ", spec.name.as_ref().unwrap());
}
match out_dir {
Some(out) => {
println!("{}", out);
}
None => {
assert!(include_missing_outdirs); problem = Some(Problem::MissingOutDir);
println!("<MISSING OUT_DIR>");
}
}
}
}
} else if let Some(err) = stderr {
io::stderr()
.write_all(err.as_bytes())
.expect("Failed to dump cargo error messages :(");
}
match problem {
Some(Problem::MissingOutDir) => {
if !quiet {
eprintln!("ERROR: Some of the specified crates are missing an $OUT_DIR (or don't have build scripts)");
}
std::process::exit(2);
}
Some(Problem::NothingToPrint) => {
if !quiet {
eprintln!("ERROR: None of the specified packages have an an $OUT_DIR (or don't have build scripts)");
}
std::process::exit(2);
}
None => Ok(()),
}
}
enum Problem {
MissingOutDir,
NothingToPrint,
}