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