Skip to main content

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}