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