use crate::cli::TreemapArgs;
use anyhow::{bail, Context, Result};
use cargo_metadata::{Message, TargetKind};
use std::collections::HashSet;
use std::path::PathBuf;
use std::process::{Command, Stdio};
pub struct BuildOutput {
pub artifacts: Vec<PathBuf>,
pub rlibs: Vec<(String, PathBuf)>,
pub crate_names: HashSet<String>,
pub target: Option<String>,
pub profile: String,
}
pub(crate) fn push_build_flags(cmd: &mut Command, args: &TreemapArgs) {
if let Some(t) = &args.target {
cmd.arg("--target").arg(t);
}
if args.workspace.workspace || args.workspace.all {
cmd.arg("--workspace");
}
for p in &args.workspace.package {
cmd.arg("--package").arg(p);
}
for e in &args.workspace.exclude {
cmd.arg("--exclude").arg(e);
}
for b in &args.bin {
cmd.arg("--bin").arg(b);
}
if args.lib {
cmd.arg("--lib");
}
}
fn select_artifacts(artifacts: Vec<(String, PathBuf)>, selected: &HashSet<String>) -> Vec<PathBuf> {
if selected.is_empty() {
return artifacts.into_iter().map(|(_, p)| p).collect();
}
let kept: Vec<PathBuf> = artifacts
.iter()
.filter(|(c, _)| selected.contains(c))
.map(|(_, p)| p.clone())
.collect();
if kept.is_empty() {
artifacts.into_iter().map(|(_, p)| p).collect()
} else {
kept
}
}
pub fn build(
args: &TreemapArgs,
usage_mode: bool,
feat_override: Option<(bool, Vec<String>)>,
) -> Result<BuildOutput> {
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
let mut cmd = Command::new(&cargo);
cmd.arg("build")
.arg("--message-format=json-render-diagnostics");
if usage_mode {
let mut rf = std::env::var("RUSTFLAGS").unwrap_or_default();
rf.push_str(" -C symbol-mangling-version=v0");
cmd.env("RUSTFLAGS", rf.trim());
} else if let Some(p) = &args.profile {
cmd.arg("--profile").arg(p);
} else if args.release {
cmd.arg("--release");
}
match &feat_override {
Some((no_default, feats)) => {
if *no_default {
cmd.arg("--no-default-features");
}
if !feats.is_empty() {
cmd.arg("--features").arg(feats.join(","));
}
}
None => {
if args.features.all_features {
cmd.arg("--all-features");
}
if args.features.no_default_features {
cmd.arg("--no-default-features");
}
if !args.features.features.is_empty() {
cmd.arg("--features").arg(args.features.features.join(","));
}
}
}
push_build_flags(&mut cmd, args);
if let Some(mp) = &args.manifest.manifest_path {
cmd.arg("--manifest-path").arg(mp);
}
if args.dwarf && !usage_mode {
let pname = match (&args.profile, args.release) {
(Some(p), _) => p.to_uppercase().replace('-', "_"),
(None, true) => "RELEASE".to_string(),
(None, false) => "DEV".to_string(),
};
cmd.env(format!("CARGO_PROFILE_{pname}_DEBUG"), "2");
}
cmd.stdout(Stdio::piped()).stderr(Stdio::inherit());
let mut child = cmd.spawn().context("failed to spawn cargo build")?;
let reader = std::io::BufReader::new(child.stdout.take().expect("piped stdout"));
let mut artifacts: Vec<(String, PathBuf)> = Vec::new();
let mut rlibs = Vec::new();
let mut crate_names = HashSet::new();
let mut finished_ok = false;
for msg in Message::parse_stream(reader) {
match msg.context("failed to parse cargo message stream")? {
Message::CompilerArtifact(art) => {
let cname = art.target.name.replace('-', "_");
crate_names.insert(cname.clone());
let is_analyzable = art
.target
.kind
.iter()
.any(|k| matches!(k, TargetKind::Bin | TargetKind::CDyLib));
if is_analyzable {
if let Some(exe) = &art.executable {
artifacts.push((cname.clone(), exe.clone().into_std_path_buf()));
}
for f in &art.filenames {
let ext = f.extension().unwrap_or("");
if matches!(ext, "so" | "dylib" | "dll" | "wasm") {
artifacts.push((cname.clone(), f.clone().into_std_path_buf()));
}
}
}
for f in &art.filenames {
if f.extension() == Some("rlib") {
rlibs.push((cname.clone(), f.clone().into_std_path_buf()));
}
}
}
Message::BuildFinished(bf) => finished_ok = bf.success,
_ => {}
}
}
let status = child.wait().context("waiting on cargo build")?;
if !finished_ok || !status.success() {
bail!("cargo build failed; see diagnostics above");
}
artifacts.sort();
artifacts.dedup();
let selected: HashSet<String> = args
.workspace
.package
.iter()
.map(|p| p.replace('-', "_"))
.collect();
let artifacts = select_artifacts(artifacts, &selected);
let profile = match (usage_mode, &args.profile, args.release) {
(true, _, _) => "dev (usage probe)".to_string(),
(false, Some(p), _) => p.clone(),
(false, None, true) => "release".to_string(),
(false, None, false) => "debug".to_string(),
};
Ok(BuildOutput {
artifacts,
rlibs,
crate_names,
target: args.target.clone(),
profile,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn arts() -> Vec<(String, PathBuf)> {
vec![
("veilid_wasm".into(), PathBuf::from("veilid_wasm.wasm")),
("veilid_core".into(), PathBuf::from("veilid_core.wasm")),
("veilid_tools".into(), PathBuf::from("veilid_tools.wasm")),
]
}
#[test]
fn no_selection_keeps_all() {
let kept = select_artifacts(arts(), &HashSet::new());
assert_eq!(kept.len(), 3);
}
#[test]
fn selection_drops_dependency_cdylibs() {
let sel = HashSet::from(["veilid_wasm".to_string()]);
let kept = select_artifacts(arts(), &sel);
assert_eq!(kept, vec![PathBuf::from("veilid_wasm.wasm")]);
}
#[test]
fn empty_match_falls_back_to_all() {
let sel = HashSet::from(["foo_cli".to_string()]);
let kept = select_artifacts(arts(), &sel);
assert_eq!(kept.len(), 3);
}
}