cargo_hyperlight/
command.rs

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