ic_build/cargo/execution/
invocation.rs1use super::{communicate_cargo, schema};
2use crate::{artifacts::CargoBuildFacts, cargo::selection::CargoSelection};
3use ic_host_process::{
4 child::{CleanupPolicy, OwnedChild},
5 tool::{
6 CompletedExecution, OutputLimit, OutputLimits, OutputStream, ToolError,
7 capture_group_command_with_cancellation,
8 },
9};
10use std::{
11 collections::BTreeMap,
12 ffi::{OsStr, OsString},
13 io,
14 path::PathBuf,
15 process::{Command, Stdio},
16};
17
18pub const TOOL_IDENTITY_BYTES: usize = 64 * 1024;
19
20#[derive(Clone, Debug, Eq, PartialEq)]
22pub enum CargoRecipe {
23 Library,
25 Example(String),
27 RustcLibrary(Vec<OsString>),
29}
30
31#[derive(Clone, Eq, PartialEq)]
38pub struct CargoInvocation {
39 pub cargo_program: OsString,
41 pub working_directory: PathBuf,
43 pub environment: BTreeMap<OsString, OsString>,
47 pub target: String,
49 pub selection: CargoSelection,
51 pub packages: Vec<String>,
53 pub manifest_path: Option<PathBuf>,
55 pub recipe: CargoRecipe,
57 pub configurations: Vec<OsString>,
59 pub render_diagnostics: bool,
61 pub keep_going: bool,
63}
64
65#[derive(Clone, Copy)]
66pub enum CargoTool {
67 Cargo,
68 Rustc,
69}
70
71impl CargoInvocation {
72 pub fn command(&self) -> io::Result<Command> {
82 if !self.working_directory.is_absolute() {
83 return Err(io::Error::new(
84 io::ErrorKind::InvalidInput,
85 "Cargo invocation requires an absolute working directory",
86 ));
87 }
88 self.selection
89 .profile_target_dir()
90 .map_err(|message| io::Error::new(io::ErrorKind::InvalidInput, message))?;
91 let mut command = Command::new(&self.cargo_program);
92 command
93 .current_dir(&self.working_directory)
94 .env_clear()
95 .envs(&self.environment);
96 command.arg(if matches!(self.recipe, CargoRecipe::RustcLibrary(_)) {
97 "rustc"
98 } else {
99 "build"
100 });
101 command.arg(if self.render_diagnostics {
102 "--message-format=json-render-diagnostics"
103 } else {
104 "--message-format=json"
105 });
106 match &self.recipe {
107 CargoRecipe::Example(name) => {
108 command.args(["--example", name]);
109 }
110 _ => {
111 command.arg("--lib");
112 }
113 }
114 command
115 .args(["--target", &self.target])
116 .args(self.selection.build_arguments());
117 if self.keep_going {
118 command.arg("--keep-going");
119 }
120 for package in &self.packages {
121 command.args(["-p", package]);
122 }
123 if let Some(manifest) = &self.manifest_path {
124 command.arg("--manifest-path").arg(manifest);
125 }
126 for configuration in &self.configurations {
127 command.arg("--config").arg(configuration);
128 }
129 if let CargoRecipe::RustcLibrary(arguments) = &self.recipe {
130 command.arg("--").args(arguments);
131 }
132 Ok(command)
133 }
134
135 pub(crate) fn observe<E>(
136 &self,
137 rustc_fallback: &OsStr,
138 mut capture: impl FnMut(CargoTool, &mut Command) -> Result<CompletedExecution, E>,
139 ) -> Result<CargoBuildFacts, E> {
140 let cargo_identity = command_identity(
141 &self.cargo_program,
142 &self.working_directory,
143 &self.environment,
144 &["--version", "--verbose"],
145 |command| capture(CargoTool::Cargo, command),
146 )?;
147 let rustc = self
148 .environment
149 .get(OsStr::new("RUSTC"))
150 .map_or(rustc_fallback, OsString::as_os_str);
151 let rustc_identity = command_identity(
152 rustc,
153 &self.working_directory,
154 &self.environment,
155 &["-vV"],
156 |command| capture(CargoTool::Rustc, command),
157 )?;
158 Ok(self.facts(cargo_identity, rustc_identity))
159 }
160
161 pub(crate) fn facts(
162 &self,
163 cargo_identity: Vec<u8>,
164 rustc_identity: Vec<u8>,
165 ) -> CargoBuildFacts {
166 CargoBuildFacts {
167 selection: self.selection.clone(),
168 target: self.target.clone(),
169 environment: self.environment.clone(),
170 cargo_identity,
171 rustc_identity,
172 }
173 }
174}
175
176pub fn command_identity<E>(
177 program: &OsStr,
178 directory: &std::path::Path,
179 environment: &BTreeMap<OsString, OsString>,
180 arguments: &[&str],
181 capture: impl FnOnce(&mut Command) -> Result<CompletedExecution, E>,
182) -> Result<Vec<u8>, E> {
183 let mut command = Command::new(program);
184 command
185 .current_dir(directory)
186 .env_clear()
187 .envs(environment)
188 .args(arguments);
189 let output = capture(&mut command)?;
190 let mut identity = output.stdout;
191 identity.extend_from_slice(&output.stderr);
192 Ok(identity)
193}
194
195#[derive(Clone, Copy)]
197pub struct CargoExecutionPolicy {
198 pub output: OutputLimits,
200 pub message_bytes: usize,
202 pub probe_cleanup: CleanupPolicy,
204}
205
206#[must_use = "inspect execution, messages and facts before admitting artifacts"]
214pub struct CargoExecution {
215 pub execution: Result<CompletedExecution, ToolError>,
217 pub messages: io::Result<()>,
219 pub facts: io::Result<Option<CargoBuildFacts>>,
221}
222
223pub fn execute_cargo(
235 invocation: &CargoInvocation,
236 policy: CargoExecutionPolicy,
237 mut spawn: impl FnMut(&mut Command) -> io::Result<OwnedChild>,
238 mut cancelled: impl FnMut() -> bool + Send,
239 output: impl FnMut(OutputStream, &[u8]) -> io::Result<()>,
240 message: impl FnMut(schema::Message) -> io::Result<()>,
241) -> io::Result<CargoExecution> {
242 let mut command = invocation.command()?;
243 command
244 .stdin(Stdio::null())
245 .stdout(Stdio::piped())
246 .stderr(Stdio::piped());
247 if invocation
248 .environment
249 .get(OsStr::new("RUSTC"))
250 .is_none_or(|program| program.is_empty())
251 {
252 return Err(io::Error::new(
253 io::ErrorKind::InvalidInput,
254 "observed Cargo execution requires an explicit RUSTC environment entry",
255 ));
256 }
257 let observe = |cancelled: &mut (dyn FnMut() -> bool + Send)| {
258 invocation.observe(OsStr::new("rustc"), |_, probe| {
259 capture_group_command_with_cancellation(
260 probe,
261 OutputLimits {
262 stdout: OutputLimit::Terminate(TOOL_IDENTITY_BYTES),
263 stderr: OutputLimit::Terminate(TOOL_IDENTITY_BYTES),
264 timeout: None,
265 },
266 policy.probe_cleanup,
267 None,
268 &mut *cancelled,
269 )
270 .and_then(CompletedExecution::require_success)
271 .and_then(CompletedExecution::require_complete)
272 .map_err(io::Error::other)
273 })
274 };
275 let before = observe(&mut cancelled)?;
276 let mut child = spawn(&mut command)?;
277 let expected = invocation.command()?;
278 let unchanged = command.get_program() == expected.get_program()
279 && command.get_current_dir() == expected.get_current_dir()
280 && command.get_args().eq(expected.get_args())
281 && command.get_envs().eq(expected.get_envs());
282 let observed = communicate_cargo(
283 &mut child,
284 policy.output,
285 policy.message_bytes,
286 &mut cancelled,
287 output,
288 message,
289 );
290 let facts = if observed
291 .execution
292 .as_ref()
293 .is_ok_and(|execution| execution.status.success())
294 && observed.messages.is_ok()
295 {
296 if unchanged {
297 observe(&mut cancelled).and_then(|after| {
298 if before == after {
299 Ok(Some(before))
300 } else {
301 Err(io::Error::new(
302 io::ErrorKind::InvalidData,
303 "Cargo/compiler identity changed during compilation",
304 ))
305 }
306 })
307 } else {
308 Err(io::Error::new(
309 io::ErrorKind::InvalidData,
310 "Cargo spawner changed the selected invocation",
311 ))
312 }
313 } else {
314 Ok(None)
315 };
316 Ok(CargoExecution {
317 execution: observed.execution,
318 messages: observed.messages,
319 facts,
320 })
321}