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