use std::collections::{BTreeSet, HashMap, HashSet};
use crate::term_verbosity_glue::Reporter;
pub(crate) struct NinjaRun {
pub status: std::process::ExitStatus,
pub stdout: String,
pub stderr: String,
}
pub(crate) fn run_ninja(
cmd: &mut std::process::Command,
reporter: Reporter,
graph: &cabin_workspace::PackageGraph,
) -> std::io::Result<NinjaRun> {
use std::io::{BufRead, BufReader, Write as _};
use std::process::Stdio;
let keep_ninja_chatter = reporter.verbosity().shows_verbose();
let pkg_by_name: HashMap<&str, &cabin_workspace::WorkspacePackage> = graph
.packages
.iter()
.map(|pkg| (pkg.package.name.as_str(), pkg))
.collect();
let mut announced: HashSet<String> = HashSet::new();
let mut child = cmd
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stderr_thread = child.stderr.take().map(|stderr| {
std::thread::spawn(move || {
let mut captured = String::new();
let real_stderr = std::io::stderr();
let mut sink = real_stderr.lock();
for line in BufReader::new(stderr).lines().map_while(Result::ok) {
let _ = writeln!(sink, "{line}");
captured.push_str(&line);
captured.push('\n');
}
captured
})
});
let mut captured_stdout = String::new();
if let Some(stdout) = child.stdout.take() {
let stdout_handle = std::io::stdout();
let mut sink = stdout_handle.lock();
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
captured_stdout.push_str(&line);
captured_stdout.push('\n');
if let Some(path) = ninja_progress_path(&line) {
if let Some(pkg_name) = package_segment_from_path(path)
&& announced.insert(pkg_name.to_owned())
&& let Some(pkg) = pkg_by_name.get(pkg_name)
{
announce_compiling(reporter, pkg);
}
if keep_ninja_chatter {
let _ = writeln!(sink, "{line}");
}
continue;
}
if is_ninja_chatter(&line) {
if keep_ninja_chatter {
let _ = writeln!(sink, "{line}");
}
continue;
}
let _ = writeln!(sink, "{line}");
}
}
let status = child.wait()?;
let stderr = stderr_thread
.and_then(|t| t.join().ok())
.unwrap_or_default();
Ok(NinjaRun {
status,
stdout: captured_stdout,
stderr,
})
}
pub(crate) fn emit_link_diagnostic_if_applicable(
run: &NinjaRun,
graph: &cabin_workspace::PackageGraph,
feature_resolution: &cabin_feature::FeatureResolution,
include_dev_for: &BTreeSet<String>,
reporter: Reporter,
) {
use cabin_build::link_diagnostics::{TargetDepInfo, diagnose, render};
let combined = if run.stderr.is_empty() {
run.stdout.clone()
} else if run.stdout.is_empty() {
run.stderr.clone()
} else {
format!("{}\n{}", run.stdout, run.stderr)
};
let host_platform = cabin_core::TargetPlatform::current();
let lookup = |pkg_name: &str, target_name: &str| -> Option<TargetDepInfo> {
let pkg_idx = graph.index_of(pkg_name)?;
let wp = &graph.packages[pkg_idx];
let target = wp
.package
.targets
.iter()
.find(|t| t.name.as_str() == target_name)?;
let dev_active = include_dev_for.contains(pkg_name);
let features = feature_resolution.for_package(pkg_idx);
let package_deps: BTreeSet<String> = wp
.package
.dependencies
.iter()
.filter(|d| {
if !d.matches_platform(&host_platform) {
return false;
}
let kind_active = d.kind.is_resolved_by_default()
|| (dev_active && d.kind == cabin_core::DependencyKind::Dev);
if !kind_active {
return false;
}
if d.optional && !features.enabled_optional_deps.contains(d.name.as_str()) {
return false;
}
true
})
.map(|d| d.name.as_str().to_owned())
.collect();
let target_deps: BTreeSet<String> = target
.deps
.iter()
.map(|d| {
d.split_once(':')
.map_or(d.as_str(), |(pkg, _)| pkg)
.to_owned()
})
.collect();
Some(TargetDepInfo {
package_deps,
target_deps,
})
};
if let Some(diag) = diagnose(&combined, lookup) {
reporter.help(&render(&diag));
}
}
fn announce_compiling(reporter: Reporter, pkg: &cabin_workspace::WorkspacePackage) {
let name = pkg.package.name.as_str();
let version = &pkg.package.version;
match pkg.kind {
cabin_workspace::PackageKind::Local => {
reporter.status(
"Compiling",
format_args!("{} v{} ({})", name, version, pkg.manifest_dir.display()),
);
}
cabin_workspace::PackageKind::Registry => {
reporter.status("Compiling", format_args!("{name} v{version}"));
}
}
}
fn is_ninja_chatter(line: &str) -> bool {
line == "ninja: no work to do." || line.starts_with("ninja: Entering directory")
}
fn ninja_progress_path(line: &str) -> Option<&str> {
let rest = line.strip_prefix('[')?;
let (finished, rest) = rest.split_once('/')?;
if finished.is_empty() || !finished.chars().all(|c| c.is_ascii_digit()) {
return None;
}
let (total, after) = rest.split_once("] ")?;
if total.is_empty() || !total.chars().all(|c| c.is_ascii_digit()) {
return None;
}
let (_action, path) = after.split_once(' ')?;
Some(path)
}
fn package_segment_from_path(path: &str) -> Option<&str> {
const SEGMENT: &str = "/packages/";
let after = path.find(SEGMENT)?;
let tail = &path[after + SEGMENT.len()..];
tail.split('/').next().filter(|s| !s.is_empty())
}
pub(crate) fn ninja_jobs_echo(jobs: Option<cabin_core::BuildJobs>) -> String {
match jobs {
Some(j) => format!("-j{j} "),
None => String::new(),
}
}
pub(crate) fn ninja_jobs_arg(jobs: cabin_core::BuildJobs) -> std::ffi::OsString {
std::ffi::OsString::from(format!("-j{}", jobs.get()))
}