Skip to main content

ic_build/cargo/execution/
mod.rs

1//! Cargo process communication and exact artifact observations shared by build recipes.
2//!
3//! Callers own command construction, environment, source custody, process spawning,
4//! budgets, cancellation, build-script interpretation and release admission.
5
6use super::{messages::MessageDecoder, observation::schema};
7use ic_host_process::{
8    child::OwnedChild,
9    tool::{
10        CompletedExecution, OutputLimits, OutputStream, SuccessfulExit, ToolError,
11        communicate_child_with_observer,
12    },
13};
14use std::{
15    collections::BTreeSet,
16    io,
17    path::{Path, PathBuf},
18};
19
20#[cfg(test)]
21mod tests;
22
23/// Independent execution and Cargo protocol evidence from one drained child.
24///
25/// A failed Cargo exit remains primary even if its output is not valid JSON.
26/// Successful execution requires checking `messages` before admitting artifacts.
27/// Protocol or message-sink refusal stops decoding but still drains the child.
28#[must_use = "check execution status and protocol evidence before admitting artifacts"]
29pub struct CargoExecution {
30    /// Original Host execution result, including bounded output and cleanup evidence.
31    pub execution: Result<CompletedExecution, ToolError>,
32    /// Original decoder or message-sink refusal, if any.
33    pub messages: io::Result<()>,
34}
35
36/// Drain a caller-spawned Cargo child and decode its stdout once.
37///
38/// Configure JSON Cargo messages and piped stdout/stderr before spawning. Text
39/// diagnostics are accepted; consumers receive canonical Cargo message types.
40/// All raw chunks reach `output`, even after a protocol refusal. An output-sink
41/// failure retains Host's interruption/cleanup behavior. Limits and cancellation
42/// are explicit; no elapsed-time deadline or global state is introduced.
43/// Observer panic unwinds through the caller's owned child and cleanup policy.
44/// The caller must check both execution status and protocol evidence.
45pub fn communicate_cargo(
46    child: &mut OwnedChild,
47    limits: OutputLimits,
48    message_bytes: usize,
49    cancelled: impl FnMut() -> bool,
50    mut output: impl FnMut(OutputStream, &[u8]) -> io::Result<()>,
51    mut message: impl FnMut(schema::Message) -> io::Result<()>,
52) -> CargoExecution {
53    let mut decoder = MessageDecoder::new(message_bytes, true);
54    let mut protocol_error = None;
55    let execution = communicate_child_with_observer(
56        child,
57        None,
58        limits,
59        SuccessfulExit::Cleanup,
60        cancelled,
61        |stream, bytes| {
62            if stream == OutputStream::Stdout && protocol_error.is_none() {
63                protocol_error = decoder.feed(bytes, &mut message).err();
64            }
65            output(stream, bytes)
66        },
67    );
68    let messages = if execution
69        .as_ref()
70        .is_ok_and(|evidence| evidence.status.success())
71    {
72        protocol_error.map_or_else(|| decoder.finish(&mut message), Err)
73    } else {
74        protocol_error.map_or(Ok(()), Err)
75    };
76    CargoExecution {
77        execution,
78        messages,
79    }
80}
81
82/// Cargo's nonempty set of outputs cannot identify one exact cdylib Wasm.
83#[derive(Debug, thiserror::Error)]
84pub enum CargoArtifactError {
85    /// Preserve the original manifest-resolution failure.
86    #[error("Cargo artifact manifest cannot be resolved: {0}")]
87    Io(#[from] io::Error),
88    /// Preserve the admitted manifest and number of distinct reported outputs.
89    #[error(
90        "Cargo did not report exactly one cdylib Wasm for {manifest}; observed {count} outputs"
91    )]
92    Selection {
93        /// Canonical selected manifest.
94        manifest: PathBuf,
95        /// Distinct reported Wasm outputs.
96        count: usize,
97    },
98}
99
100/// Select one reported cdylib Wasm for an exact admitted manifest.
101///
102/// No filename inference, stale-file fallback or artifact lifetime is provided.
103/// Callers must retain/capture the selected bytes before another build can replace them.
104///
105/// # Errors
106/// Returns original path-resolution failures or a missing/ambiguous selection.
107pub fn select_cdylib_wasm(
108    messages: &[schema::Message],
109    manifest: &Path,
110) -> Result<PathBuf, CargoArtifactError> {
111    let manifest = manifest.canonicalize()?;
112    let mut outputs = BTreeSet::new();
113    for message in messages {
114        let schema::Message::CompilerArtifact(artifact) = message else {
115            continue;
116        };
117        if artifact
118            .target
119            .crate_types
120            .contains(&schema::CrateType::CDyLib)
121            && artifact.manifest_path.canonicalize()? == manifest
122        {
123            outputs.extend(wasm_artifact_paths(artifact).map(Path::to_path_buf));
124        }
125    }
126    let count = outputs.len();
127    match outputs.pop_first() {
128        Some(path) if count == 1 => Ok(path),
129        _ => Err(CargoArtifactError::Selection { manifest, count }),
130    }
131}
132
133pub(crate) fn wasm_artifact_paths(artifact: &schema::Artifact) -> impl Iterator<Item = &Path> {
134    artifact
135        .filenames
136        .iter()
137        .filter(|path| path.extension() == Some("wasm"))
138        .map(|path| path.as_std_path())
139}