use std::collections::{BTreeSet, HashMap, HashSet};
use anyhow::Context as _;
use crate::cli::term_verbosity::Reporter;
pub(crate) struct NinjaRun {
pub status: std::process::ExitStatus,
pub stdout: String,
pub stderr: String,
}
pub(crate) fn check_stamp_runner() -> std::path::PathBuf {
std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("cabin"))
}
pub(crate) fn discovered_msvc_install_applies(
toolchain: &cabin_core::ResolvedToolchain,
cxx_kind: cabin_core::CompilerKind,
) -> bool {
if cxx_kind == cabin_core::CompilerKind::ClangCl {
return true;
}
match &toolchain.cxx.spec {
cabin_core::ToolSpec::Name(_) => true,
cabin_core::ToolSpec::Path(_) => {
cabin_toolchain::path_is_discovered_msvc_cl(toolchain.cxx.path.as_std_path())
}
}
}
pub(crate) fn run_ninja(
cmd: &mut std::process::Command,
profile_build_root: &std::path::Path,
reporter: Reporter,
graph: &cabin_workspace::PackageGraph,
dialect: cabin_build::Dialect,
apply_discovered_msvc_install: bool,
) -> std::io::Result<NinjaRun> {
use std::io::{BufRead, BufReader, Write as _};
use std::process::Stdio;
if dialect == cabin_build::Dialect::Msvc {
for (key, value) in cabin_toolchain::msvc_environment(apply_discovered_msvc_install) {
cmd.env(key, value);
}
}
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 profile_root_str = profile_build_root.to_str();
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, profile_root_str)
&& 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.reference
.split_once(':')
.map_or(d.reference.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<'a>(path: &'a str, profile_root: Option<&str>) -> Option<&'a str> {
const SEGMENT: &str = "/packages/";
let search = profile_root
.and_then(|root| path.strip_prefix(root))
.unwrap_or(path);
let after = search.find(SEGMENT)?;
let tail = &search[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(),
}
}
fn ninja_verbose_echo(verbose: bool) -> &'static str {
if verbose { "-v " } else { "" }
}
pub(crate) fn ninja_jobs_arg(jobs: cabin_core::BuildJobs) -> std::ffi::OsString {
std::ffi::OsString::from(format!("-j{}", jobs.get()))
}
pub(crate) struct NinjaInvocationRequest<'a> {
pub build_dir: &'a std::path::Path,
pub profile: &'a cabin_core::ResolvedProfile,
pub plan_graph: &'a cabin_build::BuildGraph,
pub graph: &'a cabin_workspace::PackageGraph,
pub toolchain: &'a cabin_core::ResolvedToolchain,
pub cxx_kind: cabin_core::CompilerKind,
pub feature_resolution: &'a cabin_feature::FeatureResolution,
pub dev_for: &'a BTreeSet<String>,
pub ninja: &'a std::path::Path,
pub jobs: Option<cabin_core::BuildJobs>,
pub reporter: Reporter,
}
pub(crate) fn invoke_ninja_and_report(
req: &NinjaInvocationRequest<'_>,
) -> anyhow::Result<std::time::Duration> {
let profile_build_root = req.build_dir.join(req.profile.name.as_str());
std::fs::create_dir_all(&profile_build_root).with_context(|| {
format!(
"failed to create build directory {}",
profile_build_root.display()
)
})?;
let ninja_file = profile_build_root.join("build.ninja");
cabin_ninja::write_build_ninja(&ninja_file, req.plan_graph, &check_stamp_runner())?;
let ccmd_file = profile_build_root.join("compile_commands.json");
cabin_ninja::write_compile_commands(&ccmd_file, req.plan_graph)?;
req.reporter
.verbose(format_args!("cabin: wrote {}", ninja_file.display()));
req.reporter
.verbose(format_args!("cabin: wrote {}", ccmd_file.display()));
let ninja_verbose = req.reporter.verbosity().shows_verbose();
req.reporter.verbose(format_args!(
"cabin: invoking {} {}{}-C {}",
req.ninja.display(),
ninja_jobs_echo(req.jobs),
ninja_verbose_echo(ninja_verbose),
profile_build_root.display()
));
let mut ninja_cmd = std::process::Command::new(req.ninja);
if let Some(jobs) = req.jobs {
ninja_cmd.arg(ninja_jobs_arg(jobs));
}
if ninja_verbose {
ninja_cmd.arg("-v");
}
let build_started = std::time::Instant::now();
let run = run_ninja(
ninja_cmd.arg("-C").arg(&profile_build_root),
&profile_build_root,
req.reporter,
req.graph,
req.plan_graph.dialect,
discovered_msvc_install_applies(req.toolchain, req.cxx_kind),
)
.with_context(|| format!("failed to invoke ninja at {}", req.ninja.display()))?;
if !run.status.success() {
emit_link_diagnostic_if_applicable(
&run,
req.graph,
req.feature_resolution,
req.dev_for,
req.reporter,
);
anyhow::bail!("ninja exited with {}", run.status);
}
Ok(build_started.elapsed())
}
#[cfg(test)]
mod tests {
use super::*;
use cabin_core::{
CompilerKind, ResolvedTool, ResolvedToolchain, ToolKind, ToolSource, ToolSpec,
};
use camino::Utf8PathBuf;
fn toolchain_with_pinned_cxx(path: &str) -> ResolvedToolchain {
let pinned = |kind, p: &str| ResolvedTool {
kind,
path: Utf8PathBuf::from(p),
spec: ToolSpec::Path(Utf8PathBuf::from(p)),
source: ToolSource::Cli,
};
ResolvedToolchain {
cxx: pinned(ToolKind::CxxCompiler, path),
ar: pinned(ToolKind::Archiver, "/llvm/bin/llvm-lib.exe"),
cc: None,
}
}
#[test]
fn explicit_clang_cl_path_takes_the_discovered_overlay() {
let toolchain = toolchain_with_pinned_cxx("/llvm/bin/clang-cl.exe");
assert!(discovered_msvc_install_applies(
&toolchain,
CompilerKind::ClangCl
));
}
#[test]
fn explicit_non_clang_cl_path_still_defers_to_install_match() {
let toolchain = toolchain_with_pinned_cxx("/some/other/vs/cl.exe");
assert!(!discovered_msvc_install_applies(
&toolchain,
CompilerKind::Msvc
));
}
#[test]
fn package_segment_anchors_after_the_profile_build_root() {
let path = "/home/u/packages/proj/build/dev/packages/foo/src_main.cc.o";
assert_eq!(
package_segment_from_path(path, Some("/home/u/packages/proj/build/dev")),
Some("foo")
);
assert_eq!(package_segment_from_path(path, None), Some("proj"));
assert_eq!(
package_segment_from_path(
"/home/u/proj/build/dev/stamp",
Some("/home/u/proj/build/dev")
),
None
);
}
}