use super::BuildTargets;
use crate::cli::Cli;
use camino::Utf8PathBuf;
use std::{
io::{self, BufReader, ErrorKind},
path::Path,
process::{Child, Command, ExitStatus, Stdio},
thread,
};
use tracing::{debug, warn};
mod command_logging;
mod file_io;
mod ninja_program;
mod ninja_status;
mod paths;
mod redaction;
mod streaming;
#[cfg(test)]
mod tests;
use command_logging::{
CommandLogContext, command_span, log_command_execution, log_command_exit_failure,
log_command_spawn_failure,
};
pub use file_io::*;
pub use ninja_program::resolve_ninja_program;
#[cfg(doctest)]
pub use ninja_program::resolve_ninja_program_utf8;
#[cfg(test)]
use ninja_program::resolve_ninja_program_utf8_with;
pub use paths::*;
use streaming::{ForwardStats, forward_child_output, forward_child_output_with_ninja_status};
type StatusObserver<'a> = &'a mut dyn FnMut(u32, u32, &str);
#[cfg(doctest)]
pub mod doc {
pub use super::redaction::{
CommandArg, is_sensitive_arg, redact_argument, redact_sensitive_args,
};
pub use super::{
create_temp_ninja_file, resolve_ninja_program, resolve_ninja_program_utf8,
write_ninja_file, write_text_file_utf8,
};
}
fn configure_ninja_base(cmd: &mut Command, cli: &Cli, build_file: &Path) -> io::Result<()> {
if let Some(dir) = &cli.directory {
let canonical = canonicalize_utf8_path(dir.as_path())?;
cmd.current_dir(canonical.as_std_path());
}
if let Some(jobs) = cli.jobs {
cmd.arg("-j").arg(jobs.to_string());
}
let build_file_path = canonicalize_utf8_path(build_file).or_else(|_| {
Utf8PathBuf::from_path_buf(build_file.to_path_buf()).map_err(|_| {
io::Error::new(
ErrorKind::InvalidData,
format!(
"build file path {} is not valid UTF-8",
build_file.display()
),
)
})
})?;
cmd.arg("-f").arg(build_file_path.as_std_path());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
Ok(())
}
fn configure_ninja_build_command(
cmd: &mut Command,
cli: &Cli,
build_file: &Path,
targets: &BuildTargets<'_>,
) -> io::Result<()> {
configure_ninja_base(cmd, cli, build_file)?;
cmd.args(targets.as_slice());
Ok(())
}
fn configure_ninja_tool_command(
cmd: &mut Command,
cli: &Cli,
build_file: &Path,
tool: &str,
) -> io::Result<()> {
configure_ninja_base(cmd, cli, build_file)?;
cmd.arg("-t").arg(tool);
Ok(())
}
fn check_exit_status_with_context(
status: ExitStatus,
context: &CommandLogContext,
operation: &str,
suppress_stderr: bool,
) -> io::Result<()> {
if status.success() {
Ok(())
} else {
tracing::Span::current().record("failure_category", "exit_status");
log_command_exit_failure(context, operation, suppress_stderr, status);
ninja_exit_error(status)
}
}
fn run_command_and_stream_with_context(
mut cmd: Command,
status_observer: Option<StatusObserver<'_>>,
suppress_stderr: bool,
operation: &str,
) -> io::Result<()> {
let context = CommandLogContext::from_command(&cmd);
let span = command_span(&context, operation, suppress_stderr);
let _entered = span.enter();
log_command_execution(&context, operation, suppress_stderr);
let child = cmd.spawn().inspect_err(|err| {
tracing::Span::current().record("failure_category", "spawn");
log_command_spawn_failure(&context, operation, suppress_stderr, err);
})?;
let status = spawn_and_stream_output(child, status_observer, suppress_stderr)?;
check_exit_status_with_context(status, &context, operation, suppress_stderr)
}
#[derive(Clone, Copy)]
pub(crate) struct NinjaBuildRequest<'a> {
pub(crate) program: &'a Path,
pub(crate) cli: &'a Cli,
pub(crate) build_file: &'a Path,
pub(crate) targets: &'a BuildTargets<'a>,
}
#[derive(Clone, Copy)]
pub(crate) struct NinjaToolRequest<'a> {
pub(crate) program: &'a Path,
pub(crate) cli: &'a Cli,
pub(crate) build_file: &'a Path,
pub(crate) tool: &'a str,
}
pub fn run_ninja(
program: &Path,
cli: &Cli,
build_file: &Path,
targets: &BuildTargets<'_>,
) -> io::Result<()> {
let request = NinjaBuildRequest {
program,
cli,
build_file,
targets,
};
run_ninja_build_internal(request, None)
}
pub fn run_ninja_tool(program: &Path, cli: &Cli, build_file: &Path, tool: &str) -> io::Result<()> {
let request = NinjaToolRequest {
program,
cli,
build_file,
tool,
};
run_ninja_tool_internal(request, None)
}
struct NinjaInternalRequest<'request, 'observer> {
program: &'request Path,
cli: &'request Cli,
status_observer: Option<StatusObserver<'observer>>,
operation: &'request str,
}
fn run_ninja_internal<F>(request: NinjaInternalRequest<'_, '_>, configure: F) -> io::Result<()>
where
F: FnOnce(&mut Command) -> io::Result<()>,
{
let mut cmd = Command::new(request.program);
configure(&mut cmd)?;
run_command_and_stream_with_context(
cmd,
request.status_observer,
request.cli.json,
request.operation,
)
}
fn run_ninja_build_internal(
request: NinjaBuildRequest<'_>,
status_observer: Option<StatusObserver<'_>>,
) -> io::Result<()> {
run_ninja_internal(
NinjaInternalRequest {
program: request.program,
cli: request.cli,
status_observer,
operation: "build",
},
|cmd| configure_ninja_build_command(cmd, request.cli, request.build_file, request.targets),
)
}
fn run_ninja_tool_internal(
request: NinjaToolRequest<'_>,
status_observer: Option<StatusObserver<'_>>,
) -> io::Result<()> {
run_ninja_internal(
NinjaInternalRequest {
program: request.program,
cli: request.cli,
status_observer,
operation: request.tool,
},
|cmd| configure_ninja_tool_command(cmd, request.cli, request.build_file, request.tool),
)
}
pub(crate) fn run_ninja_with_status(
request: NinjaBuildRequest<'_>,
status_observer: StatusObserver<'_>,
) -> io::Result<()> {
run_ninja_build_internal(request, Some(status_observer))
}
pub(crate) fn run_ninja_tool_with_status(
request: NinjaToolRequest<'_>,
status_observer: StatusObserver<'_>,
) -> io::Result<()> {
run_ninja_tool_internal(request, Some(status_observer))
}
fn handle_forwarding_stats(stats: ForwardStats, stream_name: &str) {
if stats.write_failed {
debug!("{stream_name} forwarding encountered closed pipe; output truncated");
}
}
fn handle_forwarding_thread_result(result: thread::Result<ForwardStats>, stream_name: &str) {
match result {
Ok(stats) => handle_forwarding_stats(stats, stream_name),
Err(err) => {
warn!("{stream_name} forwarding thread panicked: {err:?}");
}
}
}
fn forward_stdout(
stdout: impl io::Read,
output: &mut impl io::Write,
status_observer: Option<StatusObserver<'_>>,
) -> ForwardStats {
match status_observer {
Some(observer) => forward_child_output_with_ninja_status(
BufReader::new(stdout),
output,
observer,
"stdout",
),
None => forward_child_output(BufReader::new(stdout), output, "stdout"),
}
}
fn spawn_and_stream_output(
mut child: Child,
status_observer: Option<StatusObserver<'_>>,
suppress_stderr: bool,
) -> io::Result<ExitStatus> {
let Some(stdout) = child.stdout.take() else {
terminate_child(&mut child, "stdout pipe unavailable");
return Err(io::Error::other("child process missing stdout pipe"));
};
let Some(stderr) = child.stderr.take() else {
terminate_child(&mut child, "stderr pipe unavailable");
return Err(io::Error::other("child process missing stderr pipe"));
};
let err_handle = thread::spawn(move || {
if suppress_stderr {
forward_child_output(BufReader::new(stderr), io::sink(), "stderr")
} else {
forward_child_output(BufReader::new(stderr), io::stderr(), "stderr")
}
});
let stdout_stats = if suppress_stderr {
let mut output = io::sink();
forward_stdout(stdout, &mut output, status_observer)
} else {
let mut output = io::stdout().lock();
forward_stdout(stdout, &mut output, status_observer)
};
let wait_result = child.wait();
finalize_streaming(wait_result, stdout_stats, err_handle)
}
fn finalize_streaming(
wait_result: io::Result<ExitStatus>,
stdout_stats: ForwardStats,
err_handle: thread::JoinHandle<ForwardStats>,
) -> io::Result<ExitStatus> {
handle_forwarding_stats(stdout_stats, "stdout");
handle_forwarding_thread_result(err_handle.join(), "stderr");
wait_result
}
fn terminate_child(child: &mut Child, context: &str) {
if let Err(err) = child.kill() {
tracing::debug!("failed to kill child after {context}: {err}");
}
if let Err(err) = child.wait() {
tracing::debug!("failed to reap child after {context}: {err}");
}
}
fn ninja_exit_error(status: ExitStatus) -> io::Result<()> {
Err(io::Error::other(format!("ninja exited with {status}")))
}