1use 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#[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(
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}