use super::{REPLACE_FILE_OPTIONS, Result, require};
use ic_build::cargo::{
observation::{CargoMetadataSpec, decode_metadata},
selection::CargoFeatureSelection,
};
use ic_host_artifacts::artifact::Sha256Digest;
use ic_host_process::tool::{
CompletedExecution, OutputLimit, OutputLimits, ToolError, capture_command,
};
use serde::Serialize;
use std::{
io::{BufWriter, Write as _},
path::{Path, PathBuf},
process::{Command, Stdio},
time::Instant,
};
pub(super) trait Execute {
fn execute(&self, root: &Path, program: &str, args: &[&str]) -> Result<()>;
fn capture(&self, root: &Path, program: &str, args: &[&str]) -> Result<String>;
fn cargo_target(&self, root: &Path) -> Result<PathBuf>;
fn write_bytes(&self, path: &Path, bytes: &[u8]) -> Result<()> {
Ok(ic_host_fs::durable::write_bytes(
path,
REPLACE_FILE_OPTIONS,
bytes,
)?)
}
fn write_json(&self, path: &Path, value: &impl Serialize) -> Result<()> {
Ok(ic_host_fs::durable::write_with(
path,
REPLACE_FILE_OPTIONS,
|file| {
let mut writer = BufWriter::new(file);
serde_json::to_writer_pretty(&mut writer, value)?;
writer.flush().map_err(serde_json::Error::io)
},
)?)
}
fn copy_verified(&self, source: &Path, path: &Path, expected: Sha256Digest) -> Result<()> {
ic_host_fs::durable::copy_verified(source, path, expected, u64::MAX, REPLACE_FILE_OPTIONS)?;
Ok(())
}
}
pub(super) struct Processes;
impl Execute for Processes {
fn cargo_target(&self, root: &Path) -> Result<PathBuf> {
let mut command = CargoMetadataSpec {
manifest_path: &root.join("Cargo.toml"),
include_dependencies: false,
filter_platform: None,
locked: true,
offline: true,
features: &CargoFeatureSelection::default(),
}
.command(Command::new("cargo"))?;
let output = capture_complete(
"cargo",
&mut command,
OutputLimits {
stdout: OutputLimit::Terminate(4 * 1024 * 1024),
stderr: OutputLimit::Terminate(64 * 1024),
timeout: None,
},
)?;
let metadata = decode_metadata(&output.stdout)?;
Ok(metadata.target_directory.into_std_path_buf())
}
fn capture(&self, root: &Path, program: &str, args: &[&str]) -> Result<String> {
let mut command = Command::new(program);
command.current_dir(root).args(args);
let output = capture_complete(
program,
&mut command,
OutputLimits {
stdout: OutputLimit::Terminate(8 * 1024 * 1024),
stderr: OutputLimit::Terminate(256 * 1024),
timeout: None,
},
)?;
eprint!("{}", String::from_utf8_lossy(&output.stderr));
Ok(String::from_utf8(output.stdout)?)
}
fn execute(&self, root: &Path, program: &str, args: &[&str]) -> Result<()> {
let mut command = Command::new(program);
command.current_dir(root).args(args);
let phase = args
.iter()
.find(|arg| !arg.starts_with('-') && !arg.starts_with('+'))
.copied()
.unwrap_or("");
let started = Instant::now();
eprintln!("repo-tool: starting {program} {phase}");
let status = command
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status();
eprintln!(
"repo-tool: {program} {phase} finished in {:.3}s ({})",
started.elapsed().as_secs_f64(),
match &status {
Ok(status) => status.to_string(),
Err(error) => format!("spawn/wait failed: {error}"),
}
);
let status = status?;
require(
status.success(),
&format!("{program} {args:?} failed ({status})"),
)?;
Ok(())
}
}
fn capture_complete(
program: &str,
command: &mut Command,
limits: OutputLimits,
) -> Result<CompletedExecution> {
Ok(capture_command(command, limits)
.and_then(CompletedExecution::require_success)
.and_then(CompletedExecution::require_complete)
.inspect_err(|error| report_tool_error(program, error))?)
}
fn report_tool_error(program: &str, error: &ToolError) {
if let ToolError::Execution(failure) = error {
if let Some(status) = failure.evidence.status {
eprintln!("{program} failed ({status})");
}
eprint!("{}", String::from_utf8_lossy(&failure.evidence.stderr));
}
}