1use super::{
4 messages::{MessageDecoder, artifact_matches},
5 observation::{metadata_arguments, 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum NativeTargetKind {
27 Binary,
29 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#[derive(Clone, Debug)]
51pub struct NativeExecutableSpec<'a> {
52 pub package: &'a str,
54 pub target_name: &'a str,
56 pub target_kind: NativeTargetKind,
58 pub selection: CargoSelection,
61 pub max_message_bytes: usize,
64}
65
66#[derive(Debug)]
68pub struct NativeExecutable {
69 pub executable: PathBuf,
71 pub target_directory: PathBuf,
73}
74
75pub fn build_native_executable(
98 spec: &NativeExecutableSpec<'_>,
99 mut cargo: impl FnMut() -> Command,
100 mut diagnostics: impl Write,
101 mut admit: impl FnMut() -> io::Result<()>,
102 mut spawn: impl FnMut(&mut Command) -> io::Result<OwnedChild>,
103 mut interrupted: impl FnMut() -> bool,
104) -> Result<NativeExecutable, Box<dyn Error + Send + Sync>> {
105 if spec.package.is_empty() || spec.target_name.is_empty() || spec.max_message_bytes == 0 {
106 return Err(protocol("empty native target selection or message allowance").into());
107 }
108 spec.selection.profile_target_dir().map_err(protocol)?;
109 let arguments = CargoSelection {
110 locked: true,
111 ..spec.selection.clone()
112 };
113 admit()?;
114 let mut command = cargo();
115 command.args(metadata_arguments(false, None));
116 command.args(arguments.metadata_arguments());
117 let mut bytes = Vec::new();
118 execute(
119 &mut command,
120 &mut spawn,
121 &mut interrupted,
122 |stream, chunk| match stream {
123 OutputStream::Stdout => append(&mut bytes, chunk, spec.max_message_bytes),
124 OutputStream::Stderr => diagnostics.write_all(chunk),
125 },
126 )?;
127 let (package_id, target_directory) = selection(serde_json::from_slice(&bytes)?, spec)?;
128 admit()?;
129 let mut command = cargo();
130 command.args([
131 "build",
132 "--message-format=json",
133 "-p",
134 spec.package,
135 spec.target_kind.argument(),
136 spec.target_name,
137 ]);
138 command.args(arguments.build_arguments());
139 let mut decoder = MessageDecoder::new(spec.max_message_bytes, false);
140 let mut executable = None;
141 let mut finished = None;
142 execute(
143 &mut command,
144 &mut spawn,
145 &mut interrupted,
146 |stream, chunk| {
147 if stream == OutputStream::Stderr {
148 return diagnostics.write_all(chunk);
149 }
150 decoder.feed(chunk, |decoded| {
151 selected_message(
152 decoded,
153 &package_id,
154 spec,
155 &mut executable,
156 &mut finished,
157 &mut diagnostics,
158 )
159 })
160 },
161 )?;
162 decoder.finish(|decoded| {
163 selected_message(
164 decoded,
165 &package_id,
166 spec,
167 &mut executable,
168 &mut finished,
169 &mut diagnostics,
170 )
171 })?;
172 match finished {
173 Some(true) => {}
174 Some(false) => return Err(protocol("invalid Cargo build completion").into()),
175 None => return Err(protocol("Cargo omitted successful build completion").into()),
176 }
177 Ok(NativeExecutable {
178 executable: executable.ok_or_else(|| protocol("Cargo omitted the selected executable"))?,
179 target_directory,
180 })
181}
182
183fn selection(
184 metadata: schema::Metadata,
185 spec: &NativeExecutableSpec<'_>,
186) -> io::Result<(String, PathBuf)> {
187 let mut matching = metadata
188 .packages
189 .into_iter()
190 .filter(|package| package.name.as_str() == spec.package);
191 let package = matching
192 .next()
193 .ok_or_else(|| protocol("Cargo omitted selected package"))?;
194 if matching.next().is_some() {
195 return Err(protocol("Cargo package name is ambiguous"));
196 }
197 if package
198 .targets
199 .iter()
200 .filter(|target| matches_target(target, spec))
201 .count()
202 != 1
203 {
204 return Err(protocol("Cargo target is missing or ambiguous"));
205 }
206 if package.id.repr.is_empty() {
207 return Err(protocol("Cargo omitted package identity"));
208 }
209 if !metadata.target_directory.is_absolute() {
210 return Err(protocol("Cargo omitted absolute target directory"));
211 }
212 Ok((
213 package.id.repr,
214 metadata.target_directory.into_std_path_buf(),
215 ))
216}
217
218fn matches_target(target: &schema::Target, spec: &NativeExecutableSpec<'_>) -> bool {
219 target.name == spec.target_name && target.kind.contains(&spec.target_kind.cargo_kind())
220}
221
222fn selected_message(
223 message: schema::Message,
224 package_id: &str,
225 spec: &NativeExecutableSpec<'_>,
226 executable: &mut Option<PathBuf>,
227 finished: &mut Option<bool>,
228 diagnostics: &mut impl Write,
229) -> io::Result<()> {
230 match message {
231 schema::Message::BuildFinished(result) => {
232 *finished = Some(finished.is_none() && result.success);
235 }
236 schema::Message::CompilerMessage(message) => {
237 if let Some(rendered) = message.message.rendered {
238 diagnostics.write_all(rendered.as_bytes())?;
239 }
240 }
241 schema::Message::CompilerArtifact(artifact)
242 if artifact_matches(
243 &artifact,
244 package_id,
245 spec.target_name,
246 &spec.target_kind.cargo_kind(),
247 ) =>
248 {
249 let path = artifact
250 .executable
251 .filter(|path| path.is_absolute())
252 .ok_or_else(|| protocol("Cargo omitted absolute executable path"))?
253 .into_std_path_buf();
254 if executable.replace(path).is_some() {
255 return Err(protocol("Cargo reported multiple selected executables"));
256 }
257 }
258 _ => {}
259 }
260 Ok(())
261}
262
263fn execute(
264 command: &mut Command,
265 spawn: &mut impl FnMut(&mut Command) -> io::Result<OwnedChild>,
266 interrupted: &mut impl FnMut() -> bool,
267 output: impl FnMut(OutputStream, &[u8]) -> io::Result<()>,
268) -> Result<(), Box<dyn Error + Send + Sync>> {
269 command.stdin(Stdio::null()).stdout(Stdio::piped());
270 let mut child = spawn(command)?;
271 communicate_child_with_observer(
272 &mut child,
273 None,
274 OutputLimits {
275 stdout: OutputLimit::Truncate(0),
276 stderr: OutputLimit::Truncate(0),
277 timeout: None,
278 },
279 SuccessfulExit::Cleanup,
280 interrupted,
281 output,
282 )?
283 .require_success()?;
284 Ok(())
285}
286
287fn append(buffer: &mut Vec<u8>, bytes: &[u8], maximum: usize) -> io::Result<()> {
288 if bytes.len() > maximum.saturating_sub(buffer.len()) {
289 return Err(protocol("Cargo JSON message exceeds caller allowance"));
290 }
291 buffer.try_reserve(bytes.len()).map_err(io::Error::other)?;
292 buffer.extend_from_slice(bytes);
293 Ok(())
294}
295
296fn protocol(message: &'static str) -> io::Error {
297 io::Error::new(io::ErrorKind::InvalidData, message)
298}