cargo_hyperlight/command.rs
1use std::collections::{BTreeMap, HashMap};
2use std::convert::Infallible;
3use std::env::VarsOs;
4use std::ffi::{OsStr, OsString, c_char};
5use std::fmt::Debug;
6use std::path::{Path, PathBuf};
7use std::process::Command as StdCommand;
8use std::{env, iter};
9
10use anyhow::{Context, Result};
11
12use crate::CargoCommandExt;
13use crate::cargo_cmd::{CargoBinary, CargoCmd as _, find_cargo, merge_env};
14use crate::cli::{Args, Warning};
15
16/// A process builder for cargo commands, providing a similar API to `std::process::Command`.
17///
18/// `Command` is a wrapper around `std::process::Command` specifically designed for
19/// executing cargo commands targeting [hyperlight](https://github.com/hyperlight-dev/hyperlight)
20/// guest code.
21/// Before executing the desired command, `Command` takes care of setting up the
22/// appropriate environment. It:
23/// * creates a custom rust target for hyperlight guest code
24/// * creates a sysroot with Rust's libs core and alloc
25/// * finds the appropriate compiler and archiver for any C dependencies
26/// * sets up necessary environment variables for `cc-rs` and `bindgen` to work correctly.
27///
28/// # Examples
29///
30/// Basic usage:
31///
32/// ```rust,no_run
33/// use cargo_hyperlight::cargo;
34///
35/// let mut command = cargo().unwrap();
36/// command.arg("build").arg("--release");
37/// command.exec(); // This will replace the current process
38/// ```
39///
40/// Setting environment variables and working directory:
41///
42/// ```rust
43/// use cargo_hyperlight::cargo;
44///
45/// let mut command = cargo().unwrap();
46/// command
47/// .current_dir("/path/to/project")
48/// .env("CARGO_TARGET_DIR", "/custom/target")
49/// .args(["build", "--release"]);
50/// ```
51#[derive(Clone)]
52pub struct Command {
53 cargo: CargoBinary,
54 /// Arguments to pass to the cargo program
55 args: Vec<OsString>,
56 /// Environment variable mappings to set for the child process
57 inherit_envs: bool,
58 envs: BTreeMap<OsString, Option<OsString>>,
59 // Working directory for the child process
60 current_dir: Option<PathBuf>,
61}
62
63impl Debug for Command {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 let args = self.build_args_infallible();
66 let mut cmd = self.command();
67 cmd.populate_from_args(&args);
68
69 write!(f, "env ")?;
70 if let Some(current_dir) = &self.current_dir {
71 write!(f, "-C {current_dir:?} ")?;
72 }
73 if !self.inherit_envs {
74 write!(f, "-i ")?;
75 }
76 for (k, v) in cmd.get_envs() {
77 match v {
78 Some(v) => write!(f, "{}={:?} ", k.to_string_lossy(), v)?,
79 None => write!(f, "-u {} ", k.to_string_lossy())?,
80 }
81 }
82 write!(f, "{:?} ", self.get_program())?;
83 for arg in &self.args {
84 write!(f, "{arg:?} ")?;
85 }
86 writeln!(f)
87 }
88}
89
90impl Command {
91 /// Constructs a new `Command` for launching the cargo program.
92 ///
93 /// The value of the `CARGO` environment variable is used if it is set; otherwise, the
94 /// default `cargo` from the system PATH is used.
95 /// If `RUSTUP_TOOLCHAIN` is set in the environment, it is also propagated to the
96 /// child process to ensure correct functioning of the rustup wrappers.
97 ///
98 /// The default configuration is:
99 /// - No arguments to the program
100 /// - Inherits the current process's environment
101 /// - Inherits the current process's working directory
102 ///
103 /// # Errors
104 ///
105 /// This function will return an error if:
106 /// - If the `CARGO` environment variable is set but it specifies an invalid path
107 /// - If the `CARGO` environment variable is not set and the `cargo` program cannot be found in the system PATH
108 ///
109 /// # Examples
110 ///
111 /// Basic usage:
112 ///
113 /// ```rust
114 /// use cargo_hyperlight::cargo;
115 ///
116 /// let command = cargo().unwrap();
117 /// ```
118 pub(crate) fn new() -> Result<Self> {
119 let cargo = find_cargo()?;
120 Ok(Self {
121 cargo,
122 args: Vec::new(),
123 envs: BTreeMap::new(),
124 inherit_envs: true,
125 current_dir: None,
126 })
127 }
128
129 /// Adds an argument to pass to the cargo program.
130 ///
131 /// Only one argument can be passed per use. So instead of:
132 ///
133 /// ```no_run
134 /// # let mut command = cargo_hyperlight::cargo().unwrap();
135 /// command.arg("--features some_feature");
136 /// ```
137 ///
138 /// usage would be:
139 ///
140 /// ```no_run
141 /// # let mut command = cargo_hyperlight::cargo().unwrap();
142 /// command.arg("--features").arg("some_feature");
143 /// ```
144 ///
145 /// To pass multiple arguments see [`args`].
146 ///
147 /// [`args`]: Command::args
148 ///
149 /// Note that the argument is not shell-escaped, so if you pass an argument like
150 /// `"hello world"`, it will be passed as a single argument with the literal
151 /// `hello world`, not as two arguments `hello` and `world`.
152 ///
153 /// # Examples
154 ///
155 /// Basic usage:
156 ///
157 /// ```no_run
158 /// use cargo_hyperlight::cargo;
159 ///
160 /// cargo()
161 /// .unwrap()
162 /// .arg("build")
163 /// .arg("--release")
164 /// .exec();
165 /// ```
166 pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
167 self.args.push(arg.as_ref().to_os_string());
168 self
169 }
170
171 /// Adds multiple arguments to pass to the cargo program.
172 ///
173 /// To pass a single argument see [`arg`].
174 ///
175 /// [`arg`]: Command::arg
176 ///
177 /// Note that the arguments are not shell-escaped, so if you pass an argument
178 /// like `"hello world"`, it will be passed as a single argument with the
179 /// literal `hello world`, not as two arguments `hello` and `world`.
180 ///
181 /// # Examples
182 ///
183 /// Basic usage:
184 ///
185 /// ```no_run
186 /// use cargo_hyperlight::cargo;
187 ///
188 /// cargo()
189 /// .unwrap()
190 /// .args(["build", "--release"])
191 /// .exec();
192 /// ```
193 pub fn args(&mut self, args: impl IntoIterator<Item = impl AsRef<OsStr>>) -> &mut Self {
194 for arg in args {
195 self.arg(arg);
196 }
197 self
198 }
199
200 /// Sets the working directory for the child process.
201 ///
202 /// # Examples
203 ///
204 /// Basic usage:
205 ///
206 /// ```no_run
207 /// use cargo_hyperlight::cargo;
208 ///
209 /// cargo()
210 /// .unwrap()
211 /// .current_dir("path/to/project")
212 /// .arg("build")
213 /// .exec();
214 /// ```
215 ///
216 /// [`canonicalize`]: std::fs::canonicalize
217 pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
218 self.current_dir = Some(dir.as_ref().to_path_buf());
219 self
220 }
221
222 /// Inserts or updates an explicit environment variable mapping.
223 ///
224 /// This method allows you to add an environment variable mapping to the spawned process
225 /// or overwrite a variable if it already exists.
226 ///
227 /// Child processes will inherit environment variables from their parent process by
228 /// default. Environment variables explicitly set using [`env`] take precedence
229 /// over inherited variables. You can disable environment variable inheritance entirely
230 /// using [`env_clear`] or for a single key using [`env_remove`].
231 ///
232 /// Note that environment variable names are case-insensitive (but
233 /// case-preserving) on Windows and case-sensitive on all other platforms.
234 ///
235 /// # Examples
236 ///
237 /// Basic usage:
238 ///
239 /// ```no_run
240 /// use cargo_hyperlight::cargo;
241 ///
242 /// cargo()
243 /// .unwrap()
244 /// .env("CARGO_TARGET_DIR", "/path/to/target")
245 /// .arg("build")
246 /// .exec();
247 /// ```
248 ///
249 /// [`env`]: Command::env
250 /// [`env_clear`]: Command::env_clear
251 /// [`env_remove`]: Command::env_remove
252 pub fn env(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {
253 self.envs
254 .insert(key.as_ref().to_owned(), Some(value.as_ref().to_owned()));
255 self
256 }
257
258 /// Clears all environment variables that will be set for the child process.
259 ///
260 /// This method will remove all environment variables from the child process,
261 /// including those that would normally be inherited from the parent process.
262 /// Environment variables can be added back individually using [`env`].
263 ///
264 /// If `RUSTUP_TOOLCHAIN` was set in the parent process, it will be preserved.
265 ///
266 /// # Examples
267 ///
268 /// Basic usage:
269 ///
270 /// ```no_run
271 /// use cargo_hyperlight::cargo;
272 ///
273 /// cargo()
274 /// .unwrap()
275 /// .env_clear()
276 /// .env("CARGO_TARGET_DIR", "/path/to/target")
277 /// .arg("build")
278 /// .exec();
279 /// ```
280 ///
281 /// [`env`]: Command::env
282 pub fn env_clear(&mut self) -> &mut Self {
283 self.inherit_envs = false;
284 self.envs.clear();
285 self
286 }
287
288 /// Removes an explicitly set environment variable and prevents inheriting
289 /// it from a parent process.
290 ///
291 /// This method will ensure that the specified environment variable is not
292 /// present in the spawned process's environment, even if it was present
293 /// in the parent process. This serves to "unset" environment variables.
294 ///
295 /// Note that environment variable names are case-insensitive (but
296 /// case-preserving) on Windows and case-sensitive on all other platforms.
297 ///
298 /// # Examples
299 ///
300 /// Basic usage:
301 ///
302 /// ```no_run
303 /// use cargo_hyperlight::cargo;
304 ///
305 /// cargo()
306 /// .unwrap()
307 /// .env_remove("CARGO_TARGET_DIR")
308 /// .arg("build")
309 /// .exec();
310 /// ```
311 pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
312 self.envs.insert(key.as_ref().to_owned(), None);
313 self
314 }
315
316 /// Inserts or updates multiple explicit environment variable mappings.
317 ///
318 /// This method allows you to add multiple environment variable mappings
319 /// to the spawned process or overwrite variables if they already exist.
320 /// Environment variables can be passed as a `HashMap` or any other type
321 /// implementing `IntoIterator` with the appropriate item type.
322 ///
323 /// Child processes will inherit environment variables from their parent process by
324 /// default. Environment variables explicitly set using [`env`] take precedence
325 /// over inherited variables. You can disable environment variable inheritance entirely
326 /// using [`env_clear`] or for a single key using [`env_remove`].
327 ///
328 /// Note that environment variable names are case-insensitive (but
329 /// case-preserving) on Windows and case-sensitive on all other platforms.
330 ///
331 /// # Examples
332 ///
333 /// Basic usage:
334 ///
335 /// ```no_run
336 /// use std::collections::HashMap;
337 /// use cargo_hyperlight::cargo;
338 ///
339 /// let mut envs = HashMap::new();
340 /// envs.insert("CARGO_TARGET_DIR", "/path/to/target");
341 /// envs.insert("CARGO_HOME", "/path/to/.cargo");
342 ///
343 /// cargo()
344 /// .unwrap()
345 /// .envs(&envs)
346 /// .arg("build")
347 /// .exec();
348 /// ```
349 ///
350 /// ```no_run
351 /// use cargo_hyperlight::cargo;
352 ///
353 /// cargo()
354 /// .unwrap()
355 /// .envs([
356 /// ("CARGO_TARGET_DIR", "/path/to/target"),
357 /// ("CARGO_HOME", "/path/to/.cargo"),
358 /// ])
359 /// .arg("build")
360 /// .exec();
361 /// ```
362 ///
363 /// [`env`]: Command::env
364 /// [`env_clear`]: Command::env_clear
365 /// [`env_remove`]: Command::env_remove
366 pub fn envs(
367 &mut self,
368 envs: impl IntoIterator<Item = (impl AsRef<OsStr>, impl AsRef<OsStr>)>,
369 ) -> &mut Self {
370 for (k, v) in envs {
371 self.env(k, v);
372 }
373 self
374 }
375
376 /// Returns an iterator over the arguments that will be passed to the cargo program.
377 ///
378 /// This does not include the program name itself (which can be retrieved with
379 /// [`get_program`]).
380 ///
381 /// # Examples
382 ///
383 /// ```no_run
384 /// use cargo_hyperlight::cargo;
385 ///
386 /// let mut command = cargo().unwrap();
387 /// command.arg("build").arg("--release");
388 ///
389 /// let args: Vec<&std::ffi::OsStr> = command.get_args().collect();
390 /// assert_eq!(args, &["build", "--release"]);
391 /// ```
392 ///
393 /// [`get_program`]: Command::get_program
394 pub fn get_args(&'_ self) -> impl Iterator<Item = &OsStr> {
395 self.args.iter().map(|s| s.as_os_str())
396 }
397
398 /// Returns the working directory for the child process.
399 ///
400 /// This returns `None` if the working directory will not be changed from
401 /// the current directory of the parent process.
402 ///
403 /// # Examples
404 ///
405 /// ```no_run
406 /// use std::path::Path;
407 /// use cargo_hyperlight::cargo;
408 ///
409 /// let mut command = cargo().unwrap();
410 /// assert_eq!(command.get_current_dir(), None);
411 ///
412 /// command.current_dir("/tmp");
413 /// assert_eq!(command.get_current_dir(), Some(Path::new("/tmp")));
414 /// ```
415 pub fn get_current_dir(&self) -> Option<&Path> {
416 self.current_dir.as_deref()
417 }
418
419 /// Returns an iterator over the environment mappings that will be set for the child process.
420 ///
421 /// Environment variables explicitly set or unset via [`env`], [`envs`], and
422 /// [`env_remove`] can be retrieved with this method.
423 ///
424 /// Note that this output does not include environment variables inherited from the
425 /// parent process.
426 ///
427 /// Each element is a tuple key/value where `None` means the variable is explicitly
428 /// unset in the child process environment.
429 ///
430 /// # Examples
431 ///
432 /// ```no_run
433 /// use std::ffi::OsStr;
434 /// use cargo_hyperlight::cargo;
435 ///
436 /// let mut command = cargo().unwrap();
437 /// command.env("CARGO_HOME", "/path/to/.cargo");
438 /// command.env_remove("CARGO_TARGET_DIR");
439 ///
440 /// for (key, value) in command.get_envs() {
441 /// println!("{key:?} => {value:?}");
442 /// }
443 /// ```
444 ///
445 /// [`env`]: Command::env
446 /// [`envs`]: Command::envs
447 /// [`env_remove`]: Command::env_remove
448 pub fn get_envs(&'_ self) -> impl Iterator<Item = (&OsStr, Option<&OsStr>)> {
449 self.envs.iter().map(|(k, v)| (k.as_os_str(), v.as_deref()))
450 }
451
452 /// Returns the base environment variables for the command.
453 ///
454 /// This method returns the environment variables that will be inherited
455 /// from the current process, taking into account whether [`env_clear`] has been called.
456 ///
457 /// [`env_clear`]: Command::env_clear
458 fn base_env(&self) -> VarsOs {
459 let mut env = env::vars_os();
460 if !self.inherit_envs {
461 // iterate over the whole VarOs to consume it
462 env.find(|_| false);
463 }
464 env
465 }
466
467 fn resolve_env(&self) -> HashMap<OsString, OsString> {
468 merge_env(self.base_env(), self.get_envs())
469 }
470
471 fn command(&self) -> StdCommand {
472 let mut command = self.cargo.command();
473 command.args(self.get_args());
474 if let Some(cwd) = &self.current_dir {
475 command.current_dir(cwd);
476 }
477 if !self.inherit_envs {
478 command.env_clear();
479 }
480 for (k, v) in self.get_envs() {
481 match v {
482 Some(v) => command.env(k, v),
483 None => command.env_remove(k),
484 };
485 }
486 command
487 }
488
489 /// Returns the path to the cargo program that will be executed.
490 ///
491 /// # Examples
492 ///
493 /// ```no_run
494 /// use cargo_hyperlight::cargo;
495 ///
496 /// let command = cargo().unwrap();
497 /// println!("Program: {:?}", command.get_program());
498 /// ```
499 pub fn get_program(&self) -> &OsStr {
500 self.cargo.path.as_os_str()
501 }
502
503 fn build_args(&self) -> Args {
504 // parse the arguments and environment variables
505 match Args::parse(
506 self.get_args(),
507 self.resolve_env(),
508 self.get_current_dir(),
509 Warning::WARN,
510 ) {
511 Ok(args) => args,
512 }
513 }
514
515 fn build_args_infallible(&self) -> Args {
516 match Args::parse(
517 self.get_args(),
518 self.resolve_env(),
519 self.get_current_dir(),
520 Warning::IGNORE,
521 ) {
522 Ok(args) => args,
523 Err(err) => {
524 eprintln!("Failed to parse arguments: {err}");
525 std::process::exit(1);
526 }
527 }
528 }
529
530 /// Executes a cargo command as a child process, waiting for it to finish and
531 /// collecting its exit status.
532 ///
533 /// The process stdin, stdout and stderr are inherited from the parent.
534 ///
535 /// # Examples
536 ///
537 /// Basic usage:
538 ///
539 /// ```no_run
540 /// use cargo_hyperlight::cargo;
541 ///
542 /// let result = cargo()
543 /// .unwrap()
544 /// .arg("build")
545 /// .status();
546 ///
547 /// match result {
548 /// Ok(()) => println!("Cargo command succeeded"),
549 /// Err(e) => println!("Cargo command failed: {}", e),
550 /// }
551 /// ```
552 ///
553 /// # Errors
554 ///
555 /// This method will return an error if:
556 /// - The sysroot preparation fails
557 /// - The cargo process could not be spawned
558 /// - The cargo process returned a non-zero exit status
559 pub fn status(&self) -> anyhow::Result<()> {
560 let args = self.build_args();
561
562 args.prepare_sysroot()
563 .context("Failed to prepare sysroot")?;
564
565 self.command()
566 .populate_from_args(&args)
567 .checked_status()
568 .context("Failed to execute cargo")?;
569 Ok(())
570 }
571
572 /// Executes the cargo command, replacing the current process.
573 ///
574 /// This function will never return on success, as it replaces the current process
575 /// with the cargo process. On error, it will print the error and exit with code 101.
576 ///
577 /// # Examples
578 ///
579 /// Basic usage:
580 ///
581 /// ```no_run
582 /// use cargo_hyperlight::cargo;
583 ///
584 /// cargo()
585 /// .unwrap()
586 /// .arg("build")
587 /// .exec(); // This will never return
588 /// ```
589 ///
590 /// # Errors
591 ///
592 /// This function will exit the process with code 101 if:
593 /// - The sysroot preparation fails
594 /// - The process replacement fails
595 pub fn exec(&self) -> ! {
596 match self.exec_impl() {
597 Err(e) => {
598 eprintln!("{e:?}");
599 std::process::exit(101);
600 }
601 }
602 }
603
604 /// Internal implementation of process replacement.
605 ///
606 /// This method prepares the sysroot and then calls the low-level `exec` function
607 /// to replace the current process.
608 fn exec_impl(&self) -> anyhow::Result<Infallible> {
609 let args = self.build_args();
610
611 args.prepare_sysroot()
612 .context("Failed to prepare sysroot")?;
613
614 let mut command = self.command();
615 command.populate_from_args(&args);
616
617 if let Some(cwd) = self.get_current_dir() {
618 env::set_current_dir(cwd).context("Failed to change current directory")?;
619 }
620
621 Ok(exec(
622 command.get_program(),
623 command.get_args(),
624 command.resolve_env(self.base_env()),
625 )?)
626 }
627}
628
629/// Replaces the current process with the specified program using `execvpe`.
630///
631/// This function converts the provided arguments and environment variables into
632/// the format expected by the `execvpe` system call and then replaces the current
633/// process with the new program.
634///
635/// # Arguments
636///
637/// * `program` - The path to the program to execute
638/// * `args` - The command-line arguments to pass to the program
639/// * `envs` - The environment variables to set for the new process
640///
641/// # Returns
642///
643/// This function should never return on success. On failure, it returns an
644/// `std::io::Error` describing what went wrong.
645///
646/// # Safety
647///
648/// This function uses unsafe code to call `libc::execvpe`. The implementation
649/// carefully manages memory to ensure null-terminated strings are properly
650/// constructed for the system call.
651fn exec(
652 program: impl AsRef<OsStr>,
653 args: impl IntoIterator<Item = impl AsRef<OsStr>>,
654 envs: impl IntoIterator<Item = (impl AsRef<OsStr>, impl AsRef<OsStr>)>,
655) -> std::io::Result<Infallible> {
656 let mut env_bytes = vec![];
657 let mut env_offsets = vec![];
658 for (k, v) in envs.into_iter() {
659 env_offsets.push(env_bytes.len());
660 env_bytes.extend_from_slice(k.as_ref().as_encoded_bytes());
661 env_bytes.push(b'=');
662 env_bytes.extend_from_slice(v.as_ref().as_encoded_bytes());
663 env_bytes.push(0);
664 }
665 let env_ptrs = env_offsets
666 .into_iter()
667 .map(|offset| env_bytes[offset..].as_ptr() as *const c_char)
668 .chain(iter::once(std::ptr::null()))
669 .collect::<Vec<_>>();
670
671 let mut arg_bytes = vec![];
672 let mut arg_offsets = vec![];
673
674 arg_offsets.push(arg_bytes.len());
675 arg_bytes.extend_from_slice(program.as_ref().as_encoded_bytes());
676 arg_bytes.push(0);
677
678 for arg in args {
679 arg_offsets.push(arg_bytes.len());
680 arg_bytes.extend_from_slice(arg.as_ref().as_encoded_bytes());
681 arg_bytes.push(0);
682 }
683 let arg_ptrs = arg_offsets
684 .into_iter()
685 .map(|offset| arg_bytes[offset..].as_ptr() as *const c_char)
686 .chain(iter::once(std::ptr::null()))
687 .collect::<Vec<_>>();
688
689 unsafe { libc::execvpe(arg_ptrs[0], arg_ptrs.as_ptr(), env_ptrs.as_ptr()) };
690
691 Err(std::io::Error::last_os_error())
692}