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 all `CARGO_` environment variables that will be set for the child process,
295 /// except for `CARGO_HOME`.
296 ///
297 /// This method will remove all environment variables starting with `CARGO_`
298 /// from the child process, including those that would normally be inherited
299 /// from the parent process, except for `CARGO_HOME`. Other environment variables
300 /// will remain unaffected. Environment variables can be added back individually
301 /// using [`env`].
302 ///
303 /// This is particularly useful when using cargo-hyperlight from a build script
304 /// or other cargo-invoked context where `CARGO_` variables may change the behavior
305 /// of the cargo command being executed.
306 ///
307 /// # Examples
308 ///
309 /// Basic usage:
310 ///
311 /// ```no_run
312 /// use cargo_hyperlight::cargo;
313 ///
314 /// cargo()
315 /// .unwrap()
316 /// .env_clear_cargo()
317 /// .env("CARGO_TARGET_DIR", "/path/to/target")
318 /// .arg("build")
319 /// .exec();
320 /// ```
321 ///
322 /// [`env`]: Command::env
323 pub fn env_clear_cargo(&mut self) -> &mut Self {
324 self.inherit_cargo_envs = false;
325 self.envs.retain(|k, _| !is_cargo_env(k));
326 self
327 }
328
329 #[doc(hidden)]
330 #[deprecated(note = "use `env_clear_cargo` instead")]
331 pub fn env_clear_cargo_vars(&mut self) -> &mut Self {
332 self.env_clear_cargo()
333 }
334
335 /// Removes an explicitly set environment variable and prevents inheriting
336 /// it from a parent process.
337 ///
338 /// This method will ensure that the specified environment variable is not
339 /// present in the spawned process's environment, even if it was present
340 /// in the parent process. This serves to "unset" environment variables.
341 ///
342 /// Note that environment variable names are case-insensitive (but
343 /// case-preserving) on Windows and case-sensitive on all other platforms.
344 ///
345 /// # Examples
346 ///
347 /// Basic usage:
348 ///
349 /// ```no_run
350 /// use cargo_hyperlight::cargo;
351 ///
352 /// cargo()
353 /// .unwrap()
354 /// .env_remove("CARGO_TARGET_DIR")
355 /// .arg("build")
356 /// .exec();
357 /// ```
358 pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
359 self.envs.insert(key.as_ref().to_owned(), None);
360 self
361 }
362
363 /// Inserts or updates multiple explicit environment variable mappings.
364 ///
365 /// This method allows you to add multiple environment variable mappings
366 /// to the spawned process or overwrite variables if they already exist.
367 /// Environment variables can be passed as a `HashMap` or any other type
368 /// implementing `IntoIterator` with the appropriate item type.
369 ///
370 /// Child processes will inherit environment variables from their parent process by
371 /// default. Environment variables explicitly set using [`env`] take precedence
372 /// over inherited variables. You can disable environment variable inheritance entirely
373 /// using [`env_clear`] or for a single key using [`env_remove`].
374 ///
375 /// Note that environment variable names are case-insensitive (but
376 /// case-preserving) on Windows and case-sensitive on all other platforms.
377 ///
378 /// # Examples
379 ///
380 /// Basic usage:
381 ///
382 /// ```no_run
383 /// use std::collections::HashMap;
384 /// use cargo_hyperlight::cargo;
385 ///
386 /// let mut envs = HashMap::new();
387 /// envs.insert("CARGO_TARGET_DIR", "/path/to/target");
388 /// envs.insert("CARGO_HOME", "/path/to/.cargo");
389 ///
390 /// cargo()
391 /// .unwrap()
392 /// .envs(&envs)
393 /// .arg("build")
394 /// .exec();
395 /// ```
396 ///
397 /// ```no_run
398 /// use cargo_hyperlight::cargo;
399 ///
400 /// cargo()
401 /// .unwrap()
402 /// .envs([
403 /// ("CARGO_TARGET_DIR", "/path/to/target"),
404 /// ("CARGO_HOME", "/path/to/.cargo"),
405 /// ])
406 /// .arg("build")
407 /// .exec();
408 /// ```
409 ///
410 /// [`env`]: Command::env
411 /// [`env_clear`]: Command::env_clear
412 /// [`env_remove`]: Command::env_remove
413 pub fn envs(
414 &mut self,
415 envs: impl IntoIterator<Item = (impl AsRef<OsStr>, impl AsRef<OsStr>)>,
416 ) -> &mut Self {
417 for (k, v) in envs {
418 self.env(k, v);
419 }
420 self
421 }
422
423 /// Returns an iterator over the arguments that will be passed to the cargo program.
424 ///
425 /// This does not include the program name itself (which can be retrieved with
426 /// [`get_program`]).
427 ///
428 /// # Examples
429 ///
430 /// ```no_run
431 /// use cargo_hyperlight::cargo;
432 ///
433 /// let mut command = cargo().unwrap();
434 /// command.arg("build").arg("--release");
435 ///
436 /// let args: Vec<&std::ffi::OsStr> = command.get_args().collect();
437 /// assert_eq!(args, &["build", "--release"]);
438 /// ```
439 ///
440 /// [`get_program`]: Command::get_program
441 pub fn get_args(&'_ self) -> impl Iterator<Item = &OsStr> {
442 self.args.iter().map(|s| s.as_os_str())
443 }
444
445 /// Returns the working directory for the child process.
446 ///
447 /// This returns `None` if the working directory will not be changed from
448 /// the current directory of the parent process.
449 ///
450 /// # Examples
451 ///
452 /// ```no_run
453 /// use std::path::Path;
454 /// use cargo_hyperlight::cargo;
455 ///
456 /// let mut command = cargo().unwrap();
457 /// assert_eq!(command.get_current_dir(), None);
458 ///
459 /// command.current_dir("/tmp");
460 /// assert_eq!(command.get_current_dir(), Some(Path::new("/tmp")));
461 /// ```
462 pub fn get_current_dir(&self) -> Option<&Path> {
463 self.current_dir.as_deref()
464 }
465
466 /// Returns an iterator over the environment mappings that will be set for the child process.
467 ///
468 /// Environment variables explicitly set or unset via [`env`], [`envs`], and
469 /// [`env_remove`] can be retrieved with this method.
470 ///
471 /// Note that this output does not include environment variables inherited from the
472 /// parent process.
473 ///
474 /// Each element is a tuple key/value where `None` means the variable is explicitly
475 /// unset in the child process environment.
476 ///
477 /// # Examples
478 ///
479 /// ```no_run
480 /// use std::ffi::OsStr;
481 /// use cargo_hyperlight::cargo;
482 ///
483 /// let mut command = cargo().unwrap();
484 /// command.env("CARGO_HOME", "/path/to/.cargo");
485 /// command.env_remove("CARGO_TARGET_DIR");
486 ///
487 /// for (key, value) in command.get_envs() {
488 /// println!("{key:?} => {value:?}");
489 /// }
490 /// ```
491 ///
492 /// [`env`]: Command::env
493 /// [`envs`]: Command::envs
494 /// [`env_remove`]: Command::env_remove
495 pub fn get_envs(&'_ self) -> impl Iterator<Item = (&OsStr, Option<&OsStr>)> {
496 self.envs.iter().map(|(k, v)| (k.as_os_str(), v.as_deref()))
497 }
498
499 /// Returns the base environment variables for the command.
500 ///
501 /// This method returns the environment variables that will be inherited
502 /// from the current process, taking into account whether [`env_clear`] has been called.
503 ///
504 /// [`env_clear`]: Command::env_clear
505 fn base_env(&self) -> impl Iterator<Item = (OsString, OsString)> {
506 env::vars_os().filter(|(k, _)| {
507 if !self.inherit_envs {
508 false
509 } else if !self.inherit_cargo_envs {
510 !is_cargo_env(k)
511 } else {
512 true
513 }
514 })
515 }
516
517 fn resolve_env(&self) -> HashMap<OsString, OsString> {
518 merge_env(self.base_env(), self.get_envs())
519 }
520
521 fn command(&self) -> StdCommand {
522 let mut command = self.cargo.command();
523 command.args(self.get_args());
524 if let Some(cwd) = &self.current_dir {
525 command.current_dir(cwd);
526 }
527 if !self.inherit_envs {
528 command.env_clear();
529 }
530 if !self.inherit_cargo_envs {
531 for (k, _) in env::vars_os() {
532 if is_cargo_env(&k) {
533 command.env_remove(k);
534 }
535 }
536 }
537 if let Some(rustup_toolchain) = &self.cargo.rustup_toolchain {
538 command.env("RUSTUP_TOOLCHAIN", rustup_toolchain);
539 }
540 for (k, v) in self.get_envs() {
541 match v {
542 Some(v) => command.env(k, v),
543 None => command.env_remove(k),
544 };
545 }
546 command
547 }
548
549 /// Returns the path to the cargo program that will be executed.
550 ///
551 /// # Examples
552 ///
553 /// ```no_run
554 /// use cargo_hyperlight::cargo;
555 ///
556 /// let command = cargo().unwrap();
557 /// println!("Program: {:?}", command.get_program());
558 /// ```
559 pub fn get_program(&self) -> &OsStr {
560 self.cargo.path.as_os_str()
561 }
562
563 fn build_args(&self) -> Args {
564 // parse the arguments and environment variables
565 match Args::parse(
566 self.get_args(),
567 self.resolve_env(),
568 self.get_current_dir(),
569 Warning::WARN,
570 ) {
571 Ok(args) => args,
572 }
573 }
574
575 fn build_args_infallible(&self) -> Args {
576 match Args::parse(
577 self.get_args(),
578 self.resolve_env(),
579 self.get_current_dir(),
580 Warning::IGNORE,
581 ) {
582 Ok(args) => args,
583 Err(err) => {
584 eprintln!("Failed to parse arguments: {err}");
585 std::process::exit(1);
586 }
587 }
588 }
589
590 /// Executes a cargo command as a child process, waiting for it to finish and
591 /// collecting its exit status.
592 ///
593 /// The process stdin, stdout and stderr are inherited from the parent.
594 ///
595 /// # Examples
596 ///
597 /// Basic usage:
598 ///
599 /// ```no_run
600 /// use cargo_hyperlight::cargo;
601 ///
602 /// let result = cargo()
603 /// .unwrap()
604 /// .arg("build")
605 /// .status();
606 ///
607 /// match result {
608 /// Ok(()) => println!("Cargo command succeeded"),
609 /// Err(e) => println!("Cargo command failed: {}", e),
610 /// }
611 /// ```
612 ///
613 /// # Errors
614 ///
615 /// This method will return an error if:
616 /// - The sysroot preparation fails
617 /// - The cargo process could not be spawned
618 /// - The cargo process returned a non-zero exit status
619 pub fn status(&self) -> anyhow::Result<()> {
620 let args = self.build_args();
621
622 args.prepare_sysroot()
623 .context("Failed to prepare sysroot")?;
624
625 self.command()
626 .populate_from_args(&args)
627 .checked_status()
628 .context("Failed to execute cargo")?;
629 Ok(())
630 }
631
632 /// Executes the cargo command, replacing the current process.
633 ///
634 /// This function will never return on success, as it replaces the current process
635 /// with the cargo process. On error, it will print the error and exit with code 101.
636 ///
637 /// # Examples
638 ///
639 /// Basic usage:
640 ///
641 /// ```no_run
642 /// use cargo_hyperlight::cargo;
643 ///
644 /// cargo()
645 /// .unwrap()
646 /// .arg("build")
647 /// .exec(); // This will never return
648 /// ```
649 ///
650 /// # Errors
651 ///
652 /// This function will exit the process with code 101 if:
653 /// - The sysroot preparation fails
654 /// - The process replacement fails
655 pub fn exec(&self) -> ! {
656 match self.exec_impl() {
657 Err(e) => {
658 eprintln!("{e:?}");
659 std::process::exit(101);
660 }
661 }
662 }
663
664 /// Internal implementation of process replacement.
665 ///
666 /// This method prepares the sysroot and then calls the low-level `exec` function
667 /// to replace the current process.
668 fn exec_impl(&self) -> anyhow::Result<Infallible> {
669 let args = self.build_args();
670
671 args.prepare_sysroot()
672 .context("Failed to prepare sysroot")?;
673
674 let mut command = self.command();
675 command.populate_from_args(&args);
676
677 if let Some(cwd) = self.get_current_dir() {
678 env::set_current_dir(cwd).context("Failed to change current directory")?;
679 }
680
681 Ok(exec(
682 command.get_program(),
683 command.get_args(),
684 command.resolve_env(self.base_env()),
685 )?)
686 }
687}
688
689/// Replaces the current process with the specified program using `execvpe`.
690///
691/// This function converts the provided arguments and environment variables into
692/// the format expected by the `execvpe` system call and then replaces the current
693/// process with the new program.
694///
695/// # Arguments
696///
697/// * `program` - The path to the program to execute
698/// * `args` - The command-line arguments to pass to the program
699/// * `envs` - The environment variables to set for the new process
700///
701/// # Returns
702///
703/// This function should never return on success. On failure, it returns an
704/// `std::io::Error` describing what went wrong.
705///
706/// # Safety
707///
708/// This function uses unsafe code to call `libc::execvpe`. The implementation
709/// carefully manages memory to ensure null-terminated strings are properly
710/// constructed for the system call.
711fn exec(
712 program: impl AsRef<OsStr>,
713 args: impl IntoIterator<Item = impl AsRef<OsStr>>,
714 envs: impl IntoIterator<Item = (impl AsRef<OsStr>, impl AsRef<OsStr>)>,
715) -> std::io::Result<Infallible> {
716 let mut env_bytes = vec![];
717 let mut env_offsets = vec![];
718 for (k, v) in envs.into_iter() {
719 env_offsets.push(env_bytes.len());
720 env_bytes.extend_from_slice(k.as_ref().as_encoded_bytes());
721 env_bytes.push(b'=');
722 env_bytes.extend_from_slice(v.as_ref().as_encoded_bytes());
723 env_bytes.push(0);
724 }
725 let env_ptrs = env_offsets
726 .into_iter()
727 .map(|offset| env_bytes[offset..].as_ptr() as *const c_char)
728 .chain(iter::once(std::ptr::null()))
729 .collect::<Vec<_>>();
730
731 let mut arg_bytes = vec![];
732 let mut arg_offsets = vec![];
733
734 arg_offsets.push(arg_bytes.len());
735 arg_bytes.extend_from_slice(program.as_ref().as_encoded_bytes());
736 arg_bytes.push(0);
737
738 for arg in args {
739 arg_offsets.push(arg_bytes.len());
740 arg_bytes.extend_from_slice(arg.as_ref().as_encoded_bytes());
741 arg_bytes.push(0);
742 }
743 let arg_ptrs = arg_offsets
744 .into_iter()
745 .map(|offset| arg_bytes[offset..].as_ptr() as *const c_char)
746 .chain(iter::once(std::ptr::null()))
747 .collect::<Vec<_>>();
748
749 unsafe { libc::execvpe(arg_ptrs[0], arg_ptrs.as_ptr(), env_ptrs.as_ptr()) };
750
751 Err(std::io::Error::last_os_error())
752}
753
754fn is_cargo_env(key: &OsStr) -> bool {
755 key != "CARGO_HOME" && key.starts_with("CARGO_")
756}