Skip to main content

ic_build/cargo/native/
mod.rs

1//! Selected native executables from Cargo's own package and artifact messages.
2
3use super::{
4    messages::{MessageDecoder, artifact_matches},
5    observation::schema,
6    selection::CargoSelection,
7};
8use ic_host_process::{
9    child::OwnedChild,
10    tool::{
11        OutputLimit, OutputLimits, OutputStream, SuccessfulExit, communicate_child_with_observer,
12    },
13};
14use std::{
15    error::Error,
16    io::{self, Write},
17    path::PathBuf,
18    process::{Command, Stdio},
19};
20
21#[cfg(test)]
22mod tests;
23
24/// Native executable target selected structurally before any Cargo command.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum NativeTargetKind {
27    /// A package binary selected with `--bin`.
28    Binary,
29    /// A package example selected with `--example`.
30    Example,
31}
32
33impl NativeTargetKind {
34    const fn argument(self) -> &'static str {
35        match self {
36            Self::Binary => "--bin",
37            Self::Example => "--example",
38        }
39    }
40
41    const fn cargo_kind(self) -> schema::TargetKind {
42        match self {
43            Self::Binary => schema::TargetKind::Bin,
44            Self::Example => schema::TargetKind::Example,
45        }
46    }
47}
48
49/// Exact native target and caller-selected recipe and decoding allowance.
50#[derive(Clone, Debug)]
51pub struct NativeExecutableSpec<'a> {
52    /// Unique package name in the selected Cargo metadata graph.
53    pub package: &'a str,
54    /// Cargo target name, without guessing an output filename.
55    pub target_name: &'a str,
56    /// Binary or example, selecting Cargo's corresponding build target.
57    pub target_kind: NativeTargetKind,
58    /// Profile, feature and network/lock selections shared by metadata and build.
59    /// Commands additionally require locked operation.
60    pub selection: CargoSelection,
61    /// Maximum metadata bytes or bytes in one Cargo JSON message, including LF.
62    /// Total streamed diagnostic output and build duration have no default limit.
63    pub max_message_bytes: usize,
64}
65
66/// Cargo-reported paths; mutable build-directory custody remains caller-owned.
67#[derive(Debug)]
68pub struct NativeExecutable {
69    /// Absolute executable reported for the exact selected package and target.
70    pub executable: PathBuf,
71    /// Absolute target root reported by the same Cargo command's metadata.
72    pub target_directory: PathBuf,
73}
74
75/// Build a selected native target and return Cargo's reported executable.
76///
77/// The factory supplies the same Cargo prefix, working directory, environment
78/// and network/tool policy for metadata and build. Commands add `--locked`, exact
79/// package/target selection and Cargo's JSON diagnostics format. Stdout is a
80/// bounded-per-message protocol; rendered compiler diagnostics and piped stderr
81/// stream to `diagnostics`. Other stderr configurations remain intact. Stdin is
82/// null. `spawn` selects direct-child or owned-group custody and cleanup policy.
83/// `admit` runs before each command factory. `interrupted` is polled during both
84/// metadata and build capture; cancellation retains Host's typed cleanup evidence.
85/// The caller owns signal handlers and cancellation meaning.
86///
87/// Callers exclude source/config/tool mutation and retain artifact custody. No
88/// cache, installation, release admission, retry or elapsed-time default is added.
89/// Interrupted or rejected capture uses the caller-selected child cleanup.
90///
91/// # Errors
92/// Refuses invalid selection before command effects, missing/ambiguous metadata
93/// or artifacts, malformed/oversized messages, sink failures and command failures.
94/// Original Host execution/status/cleanup causes remain downcastable.
95pub fn build_native_executable(
96    spec: &NativeExecutableSpec<'_>,
97    mut cargo: impl FnMut() -> Command,
98    mut diagnostics: impl Write,
99    mut admit: impl FnMut() -> io::Result<()>,
100    mut spawn: impl FnMut(&mut Command) -> io::Result<OwnedChild>,
101    mut interrupted: impl FnMut() -> bool,
102) -> Result<NativeExecutable, Box<dyn Error + Send + Sync>> {
103    if spec.package.is_empty() || spec.target_name.is_empty() || spec.max_message_bytes == 0 {
104        return Err(protocol("empty native target selection or message allowance").into());
105    }
106    spec.selection.profile_target_dir().map_err(protocol)?;
107    let arguments = CargoSelection {
108        locked: true,
109        ..spec.selection.clone()
110    };
111    admit()?;
112    let mut command = cargo();
113    command.args(["metadata", "--no-deps", "--format-version", "1"]);
114    command.args(arguments.metadata_arguments());
115    let mut bytes = Vec::new();
116    execute(
117        &mut command,
118        &mut spawn,
119        &mut interrupted,
120        |stream, chunk| match stream {
121            OutputStream::Stdout => append(&mut bytes, chunk, spec.max_message_bytes),
122            OutputStream::Stderr => diagnostics.write_all(chunk),
123        },
124    )?;
125    let (package_id, target_directory) = selection(serde_json::from_slice(&bytes)?, spec)?;
126    admit()?;
127    let mut command = cargo();
128    command.args([
129        "build",
130        "--message-format=json",
131        "-p",
132        spec.package,
133        spec.target_kind.argument(),
134        spec.target_name,
135    ]);
136    command.args(arguments.build_arguments());
137    let mut decoder = MessageDecoder::new(spec.max_message_bytes, false);
138    let mut executable = None;
139    execute(
140        &mut command,
141        &mut spawn,
142        &mut interrupted,
143        |stream, chunk| {
144            if stream == OutputStream::Stderr {
145                return diagnostics.write_all(chunk);
146            }
147            decoder.feed(chunk, |decoded| {
148                selected_message(
149                    decoded,
150                    &package_id,
151                    spec,
152                    &mut executable,
153                    &mut diagnostics,
154                )
155            })
156        },
157    )?;
158    decoder.finish(|decoded| {
159        selected_message(
160            decoded,
161            &package_id,
162            spec,
163            &mut executable,
164            &mut diagnostics,
165        )
166    })?;
167    Ok(NativeExecutable {
168        executable: executable.ok_or_else(|| protocol("Cargo omitted the selected executable"))?,
169        target_directory,
170    })
171}
172
173fn selection(
174    metadata: schema::Metadata,
175    spec: &NativeExecutableSpec<'_>,
176) -> io::Result<(String, PathBuf)> {
177    let mut matching = metadata
178        .packages
179        .into_iter()
180        .filter(|package| package.name.as_str() == spec.package);
181    let package = matching
182        .next()
183        .ok_or_else(|| protocol("Cargo omitted selected package"))?;
184    if matching.next().is_some() {
185        return Err(protocol("Cargo package name is ambiguous"));
186    }
187    if package
188        .targets
189        .iter()
190        .filter(|target| matches_target(target, spec))
191        .count()
192        != 1
193    {
194        return Err(protocol("Cargo target is missing or ambiguous"));
195    }
196    if package.id.repr.is_empty() {
197        return Err(protocol("Cargo omitted package identity"));
198    }
199    if !metadata.target_directory.is_absolute() {
200        return Err(protocol("Cargo omitted absolute target directory"));
201    }
202    Ok((
203        package.id.repr,
204        metadata.target_directory.into_std_path_buf(),
205    ))
206}
207
208fn matches_target(target: &schema::Target, spec: &NativeExecutableSpec<'_>) -> bool {
209    target.name == spec.target_name && target.kind.contains(&spec.target_kind.cargo_kind())
210}
211
212fn selected_message(
213    message: schema::Message,
214    package_id: &str,
215    spec: &NativeExecutableSpec<'_>,
216    executable: &mut Option<PathBuf>,
217    diagnostics: &mut impl Write,
218) -> io::Result<()> {
219    match message {
220        schema::Message::CompilerMessage(message) => {
221            if let Some(rendered) = message.message.rendered {
222                diagnostics.write_all(rendered.as_bytes())?;
223            }
224        }
225        schema::Message::CompilerArtifact(artifact)
226            if artifact_matches(
227                &artifact,
228                package_id,
229                spec.target_name,
230                &spec.target_kind.cargo_kind(),
231            ) =>
232        {
233            let path = artifact
234                .executable
235                .filter(|path| path.is_absolute())
236                .ok_or_else(|| protocol("Cargo omitted absolute executable path"))?
237                .into_std_path_buf();
238            if executable.replace(path).is_some() {
239                return Err(protocol("Cargo reported multiple selected executables"));
240            }
241        }
242        _ => {}
243    }
244    Ok(())
245}
246
247fn execute(
248    command: &mut Command,
249    spawn: &mut impl FnMut(&mut Command) -> io::Result<OwnedChild>,
250    interrupted: &mut impl FnMut() -> bool,
251    output: impl FnMut(OutputStream, &[u8]) -> io::Result<()>,
252) -> Result<(), Box<dyn Error + Send + Sync>> {
253    command.stdin(Stdio::null()).stdout(Stdio::piped());
254    let mut child = spawn(command)?;
255    communicate_child_with_observer(
256        &mut child,
257        None,
258        OutputLimits {
259            stdout: OutputLimit::Truncate(0),
260            stderr: OutputLimit::Truncate(0),
261            timeout: None,
262        },
263        SuccessfulExit::Cleanup,
264        interrupted,
265        output,
266    )?
267    .require_success()?;
268    Ok(())
269}
270
271fn append(buffer: &mut Vec<u8>, bytes: &[u8], maximum: usize) -> io::Result<()> {
272    if bytes.len() > maximum.saturating_sub(buffer.len()) {
273        return Err(protocol("Cargo JSON message exceeds caller allowance"));
274    }
275    buffer.try_reserve(bytes.len()).map_err(io::Error::other)?;
276    buffer.extend_from_slice(bytes);
277    Ok(())
278}
279
280fn protocol(message: &'static str) -> io::Error {
281    io::Error::new(io::ErrorKind::InvalidData, message)
282}