Skip to main content

cargo_hyperlight/
command.rs

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