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 ffi::OsString,
17 io::{self, Write},
18 path::PathBuf,
19 process::{Command, Stdio},
20};
21
22#[cfg(test)]
23mod tests;
24
25pub struct NativeExecutableSpec<'a> {
27 pub package: &'a str,
29 pub target_name: &'a str,
31 pub target_kind: &'a str,
33 pub build_args: &'a [OsString],
38 pub max_message_bytes: usize,
41}
42
43#[derive(Debug)]
45pub struct NativeExecutable {
46 pub executable: PathBuf,
48 pub target_directory: PathBuf,
50}
51
52pub fn build_native_executable(
72 spec: &NativeExecutableSpec<'_>,
73 mut cargo: impl FnMut() -> Command,
74 mut diagnostics: impl Write,
75 mut admit: impl FnMut() -> io::Result<()>,
76) -> Result<NativeExecutable, Box<dyn Error + Send + Sync>> {
77 let selector = match spec.target_kind {
78 "bin" => "--bin",
79 "example" => "--example",
80 _ => return Err(protocol("native target kind must be bin or example").into()),
81 };
82 if spec.package.is_empty() || spec.target_name.is_empty() || spec.max_message_bytes == 0 {
83 return Err(protocol("empty native target selection or message allowance").into());
84 }
85 let arguments = CargoSelection::parse(spec.build_args.to_vec());
86 if !arguments.flags.is_empty() {
87 return Err(protocol("native build arguments override the selected recipe").into());
88 }
89 arguments
90 .profile
91 .as_ref()
92 .map_err(|message| protocol(message))?;
93 admit()?;
94 let mut command = cargo();
95 command.args(["metadata", "--locked", "--no-deps", "--format-version", "1"]);
96 command.args(&arguments.metadata_arguments);
97 let mut bytes = Vec::new();
98 execute(&mut command, |stream, chunk| match stream {
99 OutputStream::Stdout => append(&mut bytes, chunk, spec.max_message_bytes),
100 OutputStream::Stderr => diagnostics.write_all(chunk),
101 })?;
102 let (package_id, target_directory) = selection(serde_json::from_slice(&bytes)?, spec)?;
103 admit()?;
104 let mut command = cargo();
105 command.args([
106 "build",
107 "--locked",
108 "--message-format=json",
109 "-p",
110 spec.package,
111 selector,
112 spec.target_name,
113 ]);
114 command.args(spec.build_args);
115 let mut decoder = MessageDecoder::new(spec.max_message_bytes, false);
116 let mut executable = None;
117 execute(&mut command, |stream, chunk| {
118 if stream == OutputStream::Stderr {
119 return diagnostics.write_all(chunk);
120 }
121 decoder.feed(chunk, |decoded| {
122 selected_message(
123 decoded,
124 &package_id,
125 spec,
126 &mut executable,
127 &mut diagnostics,
128 )
129 })
130 })?;
131 decoder.finish(|decoded| {
132 selected_message(
133 decoded,
134 &package_id,
135 spec,
136 &mut executable,
137 &mut diagnostics,
138 )
139 })?;
140 Ok(NativeExecutable {
141 executable: executable.ok_or_else(|| protocol("Cargo omitted the selected executable"))?,
142 target_directory,
143 })
144}
145
146fn selection(
147 metadata: schema::Metadata,
148 spec: &NativeExecutableSpec<'_>,
149) -> io::Result<(String, PathBuf)> {
150 let mut matching = metadata
151 .packages
152 .into_iter()
153 .filter(|package| package.name.as_str() == spec.package);
154 let package = matching
155 .next()
156 .ok_or_else(|| protocol("Cargo omitted selected package"))?;
157 if matching.next().is_some() {
158 return Err(protocol("Cargo package name is ambiguous"));
159 }
160 if package
161 .targets
162 .iter()
163 .filter(|target| matches_target(target, spec))
164 .count()
165 != 1
166 {
167 return Err(protocol("Cargo target is missing or ambiguous"));
168 }
169 if package.id.repr.is_empty() {
170 return Err(protocol("Cargo omitted package identity"));
171 }
172 if !metadata.target_directory.is_absolute() {
173 return Err(protocol("Cargo omitted absolute target directory"));
174 }
175 Ok((
176 package.id.repr,
177 metadata.target_directory.into_std_path_buf(),
178 ))
179}
180
181fn matches_target(target: &schema::Target, spec: &NativeExecutableSpec<'_>) -> bool {
182 target.name == spec.target_name && target.kind.contains(&spec.target_kind.into())
183}
184
185fn selected_message(
186 message: schema::Message,
187 package_id: &str,
188 spec: &NativeExecutableSpec<'_>,
189 executable: &mut Option<PathBuf>,
190 diagnostics: &mut impl Write,
191) -> io::Result<()> {
192 match message {
193 schema::Message::CompilerMessage(message) => {
194 if let Some(rendered) = message.message.rendered {
195 diagnostics.write_all(rendered.as_bytes())?;
196 }
197 }
198 schema::Message::CompilerArtifact(artifact)
199 if artifact_matches(
200 &artifact,
201 package_id,
202 spec.target_name,
203 &spec.target_kind.into(),
204 ) =>
205 {
206 let path = artifact
207 .executable
208 .filter(|path| path.is_absolute())
209 .ok_or_else(|| protocol("Cargo omitted absolute executable path"))?
210 .into_std_path_buf();
211 if executable.replace(path).is_some() {
212 return Err(protocol("Cargo reported multiple selected executables"));
213 }
214 }
215 _ => {}
216 }
217 Ok(())
218}
219
220fn execute(
221 command: &mut Command,
222 output: impl FnMut(OutputStream, &[u8]) -> io::Result<()>,
223) -> Result<(), Box<dyn Error + Send + Sync>> {
224 command.stdin(Stdio::null()).stdout(Stdio::piped());
225 let mut child = OwnedChild::spawn_direct(command)?;
226 communicate_child_with_observer(
227 &mut child,
228 None,
229 OutputLimits {
230 stdout: OutputLimit::Truncate(0),
231 stderr: OutputLimit::Truncate(0),
232 timeout: None,
233 },
234 SuccessfulExit::Cleanup,
235 || false,
236 output,
237 )?
238 .require_success()?;
239 Ok(())
240}
241
242fn append(buffer: &mut Vec<u8>, bytes: &[u8], maximum: usize) -> io::Result<()> {
243 if bytes.len() > maximum.saturating_sub(buffer.len()) {
244 return Err(protocol("Cargo JSON message exceeds caller allowance"));
245 }
246 buffer.try_reserve(bytes.len()).map_err(io::Error::other)?;
247 buffer.extend_from_slice(bytes);
248 Ok(())
249}
250
251fn protocol(message: &'static str) -> io::Error {
252 io::Error::new(io::ErrorKind::InvalidData, message)
253}