1use super::{
4 messages::{BuildCompletion, 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 = BuildCompletion::default();
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 finished.require_success()?;
173 Ok(NativeExecutable {
174 executable: executable.ok_or_else(|| protocol("Cargo omitted the selected executable"))?,
175 target_directory,
176 })
177}
178
179fn selection(
180 metadata: schema::Metadata,
181 spec: &NativeExecutableSpec<'_>,
182) -> io::Result<(String, PathBuf)> {
183 let mut matching = metadata
184 .packages
185 .into_iter()
186 .filter(|package| package.name.as_str() == spec.package);
187 let package = matching
188 .next()
189 .ok_or_else(|| protocol("Cargo omitted selected package"))?;
190 if matching.next().is_some() {
191 return Err(protocol("Cargo package name is ambiguous"));
192 }
193 if package
194 .targets
195 .iter()
196 .filter(|target| matches_target(target, spec))
197 .count()
198 != 1
199 {
200 return Err(protocol("Cargo target is missing or ambiguous"));
201 }
202 if package.id.repr.is_empty() {
203 return Err(protocol("Cargo omitted package identity"));
204 }
205 if !metadata.target_directory.is_absolute() {
206 return Err(protocol("Cargo omitted absolute target directory"));
207 }
208 Ok((
209 package.id.repr,
210 metadata.target_directory.into_std_path_buf(),
211 ))
212}
213
214fn matches_target(target: &schema::Target, spec: &NativeExecutableSpec<'_>) -> bool {
215 target.name == spec.target_name && target.kind.contains(&spec.target_kind.cargo_kind())
216}
217
218fn selected_message(
219 message: schema::Message,
220 package_id: &str,
221 spec: &NativeExecutableSpec<'_>,
222 executable: &mut Option<PathBuf>,
223 finished: &mut BuildCompletion,
224 diagnostics: &mut impl Write,
225) -> io::Result<()> {
226 match message {
227 schema::Message::BuildFinished(result) => {
228 finished.record(result.success);
231 }
232 schema::Message::CompilerMessage(message) => {
233 if let Some(rendered) = message.message.rendered {
234 diagnostics.write_all(rendered.as_bytes())?;
235 }
236 }
237 schema::Message::CompilerArtifact(artifact)
238 if artifact_matches(
239 &artifact,
240 package_id,
241 spec.target_name,
242 &spec.target_kind.cargo_kind(),
243 ) =>
244 {
245 let path = artifact
246 .executable
247 .filter(|path| path.is_absolute())
248 .ok_or_else(|| protocol("Cargo omitted absolute executable path"))?
249 .into_std_path_buf();
250 if executable.replace(path).is_some() {
251 return Err(protocol("Cargo reported multiple selected executables"));
252 }
253 }
254 _ => {}
255 }
256 Ok(())
257}
258
259fn execute(
260 command: &mut Command,
261 spawn: &mut impl FnMut(&mut Command) -> io::Result<OwnedChild>,
262 interrupted: &mut impl FnMut() -> bool,
263 output: impl FnMut(OutputStream, &[u8]) -> io::Result<()>,
264) -> Result<(), Box<dyn Error + Send + Sync>> {
265 command.stdin(Stdio::null()).stdout(Stdio::piped());
266 let mut child = spawn(command)?;
267 communicate_child_with_observer(
268 &mut child,
269 None,
270 OutputLimits {
271 stdout: OutputLimit::Truncate(0),
272 stderr: OutputLimit::Truncate(0),
273 timeout: None,
274 },
275 SuccessfulExit::Cleanup,
276 interrupted,
277 output,
278 )?
279 .require_success()?;
280 Ok(())
281}
282
283fn append(buffer: &mut Vec<u8>, bytes: &[u8], maximum: usize) -> io::Result<()> {
284 if bytes.len() > maximum.saturating_sub(buffer.len()) {
285 return Err(protocol("Cargo JSON message exceeds caller allowance"));
286 }
287 buffer.try_reserve(bytes.len()).map_err(io::Error::other)?;
288 buffer.extend_from_slice(bytes);
289 Ok(())
290}
291
292fn protocol(message: &'static str) -> io::Error {
293 io::Error::new(io::ErrorKind::InvalidData, message)
294}