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}