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