1use std::collections::HashMap;
52use std::ffi::{OsStr, OsString};
53use std::fs::File;
54use std::io::{self, Read};
55use std::path::{Path, PathBuf};
56use std::sync::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
57use std::time::SystemTime;
58
59pub use crate::{
60 ape_is_exec_format_error as is_exec_format_error,
61 APE_EXECVP_SHELL_FALLBACK as EXECVP_SHELL_FALLBACK, APE_LOADER_HOST as LOADER_HOST,
62 APE_NEEDS_LOADER as NEEDS_LOADER, APE_SHELL as SHELL, APE_SYSTEM_LOADERS as SYSTEM_LOADERS,
63};
64
65pub const LOADER_ENV: &str = "RUNNING_PROCESS_APE_LOADER";
68
69pub const CACHE_DIR_ENV: &str = "RUNNING_PROCESS_APE_CACHE_DIR";
74
75pub const MAGICS: [&[u8]; 3] = [b"MZqFpD='", b"jartsr='", b"APEDBG='"];
78
79#[cfg(feature = "ape-loader")]
82const PROLOGUE_SCAN_BYTES: u64 = 64 * 1024;
83
84#[cfg(feature = "ape-loader")]
87const MAX_LOADER_BYTES: u64 = 4 * 1024 * 1024;
88
89pub fn is_ape_header(header: &[u8]) -> bool {
91 MAGICS.iter().any(|magic| header.starts_with(magic))
92}
93
94pub fn is_ape_file(path: &Path) -> bool {
96 let mut header = [0u8; 8];
97 File::open(path)
98 .and_then(|mut file| file.read_exact(&mut header))
99 .is_ok_and(|()| is_ape_header(&header))
100}
101
102static FORK_LOCK: RwLock<()> = RwLock::new(());
112
113pub fn fork_guard() -> RwLockReadGuard<'static, ()> {
118 FORK_LOCK.read().unwrap_or_else(|error| error.into_inner())
119}
120
121pub fn exclusive_fork_guard() -> RwLockWriteGuard<'static, ()> {
123 FORK_LOCK.write().unwrap_or_else(|error| error.into_inner())
124}
125
126pub fn retry_while_busy<T>(mut spawn: impl FnMut() -> io::Result<T>) -> io::Result<T> {
132 for delay_ms in [1, 4, 16, 64, 256] {
133 match spawn() {
134 Err(error) if error.kind() == io::ErrorKind::ExecutableFileBusy => {
135 std::thread::sleep(std::time::Duration::from_millis(delay_ms));
136 }
137 result => return result,
138 }
139 }
140 spawn()
141}
142
143#[derive(Clone, Debug, Default, Eq, PartialEq)]
149pub struct ApeOptions {
150 pub path: Option<OsString>,
152 pub loader: Option<OsString>,
154 pub cache_dirs: Vec<PathBuf>,
157}
158
159impl ApeOptions {
160 pub fn inherited() -> Self {
164 Self::with_overrides(false, [])
165 }
166
167 pub fn with_overrides<'a>(
172 clear: bool,
173 overrides: impl IntoIterator<Item = (&'a OsStr, Option<&'a OsStr>)>,
174 ) -> Self {
175 let inherited = |name: &str| {
176 if clear {
177 return None;
178 }
179 match name {
180 "PATH" => crate::env_vars::PATH.os(),
181 LOADER_ENV => crate::env_vars::APE_LOADER.os(),
182 _ => crate::env_vars::APE_CACHE_DIR.os(),
183 }
184 };
185 let mut path = inherited("PATH");
186 let mut loader = inherited(LOADER_ENV);
187 let mut cache_dir = inherited(CACHE_DIR_ENV);
188 for (key, value) in overrides {
189 let slot = match key.to_str() {
190 Some("PATH") => &mut path,
191 Some(LOADER_ENV) => &mut loader,
192 Some(CACHE_DIR_ENV) => &mut cache_dir,
193 _ => continue,
194 };
195 *slot = value.map(OsStr::to_os_string);
196 }
197 let set = |value: Option<OsString>| value.filter(|value| !value.is_empty());
198 let mut cache_dirs: Vec<PathBuf> = set(cache_dir).map(PathBuf::from).into_iter().collect();
199 cache_dirs.extend(crate::ape_default_loader_dirs());
200 Self {
201 path: set(path),
202 loader: set(loader),
203 cache_dirs,
204 }
205 }
206}
207
208#[derive(Clone, Copy, Debug, Eq, PartialEq)]
210pub enum LoaderKind {
211 Explicit,
213 Embedded,
215 System,
217 Shell,
219}
220
221#[derive(Clone, Debug, Eq, PartialEq)]
223pub struct ApeLaunch {
224 pub kind: LoaderKind,
225 pub loader: PathBuf,
227 pub image: PathBuf,
229 pub ape_path_dir: Option<PathBuf>,
235}
236
237impl ApeLaunch {
238 pub fn child_path(&self, inherited: Option<&OsStr>) -> Option<OsString> {
241 let dir = self.ape_path_dir.as_ref()?;
242 let rest = inherited.filter(|path| !path.is_empty());
243 std::env::join_paths(
244 std::iter::once(dir.clone()).chain(rest.into_iter().flat_map(std::env::split_paths)),
245 )
246 .ok()
247 }
248
249 pub fn args<I, S>(&self, args: I) -> Vec<OsString>
251 where
252 I: IntoIterator<Item = S>,
253 S: AsRef<OsStr>,
254 {
255 std::iter::once(self.image.clone().into_os_string())
256 .chain(args.into_iter().map(|arg| arg.as_ref().to_os_string()))
257 .collect()
258 }
259}
260
261pub fn plan_launch(
268 program: &OsStr,
269 current_dir: Option<&Path>,
270 options: &ApeOptions,
271) -> Option<ApeLaunch> {
272 if !NEEDS_LOADER {
273 return None;
274 }
275 let image = resolve_program(program, current_dir, options.path.as_deref())?;
276 if !is_ape_file(&image) {
277 return None;
278 }
279 let launch = |kind, loader: PathBuf| ApeLaunch {
280 kind,
281 ape_path_dir: ape_path_dir(&loader, &options.cache_dirs),
282 loader,
283 image: image.clone(),
284 };
285 if let Some(explicit) = options.loader.as_deref() {
286 return resolve_explicit_loader(explicit, options.path.as_deref())
287 .map(|loader| launch(LoaderKind::Explicit, loader));
288 }
289 if let Some(loader) = embedded_loader(&image, &options.cache_dirs) {
290 return Some(launch(LoaderKind::Embedded, loader));
291 }
292 if let Some(loader) = system_loader(options.path.as_deref()) {
293 return Some(launch(LoaderKind::System, loader));
294 }
295 shell(options.path.as_deref()).map(|loader| launch(LoaderKind::Shell, loader))
296}
297
298pub fn resolve_program(
302 program: &OsStr,
303 current_dir: Option<&Path>,
304 path: Option<&OsStr>,
305) -> Option<PathBuf> {
306 let program_path = Path::new(program);
307 if program_path.components().count() > 1 || program_path.is_absolute() {
308 let resolved = match current_dir {
309 Some(dir) if program_path.is_relative() => dir.join(program_path),
310 _ => program_path.to_path_buf(),
311 };
312 return std::path::absolute(resolved).ok();
313 }
314 find_executable_on_path(program, path).and_then(|found| std::path::absolute(found).ok())
315}
316
317fn resolve_explicit_loader(explicit: &OsStr, path: Option<&OsStr>) -> Option<PathBuf> {
318 let explicit_path = Path::new(explicit);
319 if explicit_path.components().count() > 1 || explicit_path.is_absolute() {
320 return Some(explicit_path.to_path_buf());
321 }
322 find_executable_on_path(explicit, path)
323}
324
325fn system_loader(path: Option<&OsStr>) -> Option<PathBuf> {
328 find_executable_on_path(OsStr::new("ape"), path)
329 .or_else(|| first_executable(SYSTEM_LOADERS.iter().map(PathBuf::from)))
330}
331
332fn shell(path: Option<&OsStr>) -> Option<PathBuf> {
333 first_executable([PathBuf::from(SHELL)])
334 .or_else(|| find_executable_on_path(OsStr::new("sh"), path))
335}
336
337fn find_executable_on_path(name: &OsStr, path: Option<&OsStr>) -> Option<PathBuf> {
338 let dirs = std::env::split_paths(path?).filter(|dir| !dir.as_os_str().is_empty());
339 first_executable(dirs.map(|dir| dir.join(name)))
340}
341
342fn first_executable(candidates: impl IntoIterator<Item = PathBuf>) -> Option<PathBuf> {
343 candidates.into_iter().find(|candidate| {
344 std::fs::metadata(candidate)
345 .is_ok_and(|meta| meta.is_file() && crate::ape_is_executable(&meta))
346 })
347}
348
349#[derive(Clone, Copy, Debug, Eq, PartialEq)]
355pub enum LoaderHost {
356 None,
358 Linux,
360 Macos,
363}
364
365pub type Range = (u64, u64);
367
368#[derive(Clone, Debug, Default, Eq, PartialEq)]
380pub struct Prologue {
381 pub linux_loader_x86_64: Option<Range>,
382 pub linux_loader_aarch64: Option<Range>,
383 pub macos_loader_x86_64: Option<Range>,
384 pub macos_loader_source_aarch64: Option<Range>,
385}
386
387impl Prologue {
388 pub fn parse(prologue: &[u8]) -> Self {
390 let text = String::from_utf8_lossy(prologue);
391 let mut parsed = Self::default();
392 let mut cpu = None;
393 let mut lines = text.lines().map(str::trim).peekable();
394 while let Some(line) = lines.next() {
395 if line.contains("\"$m\" = x86_64") {
396 cpu = Some("x86_64");
397 } else if line.contains("\"$m\" = aarch64") {
398 cpu = Some("aarch64");
399 }
400 let Some(cpu) = cpu else { continue };
401 if !line.starts_with("dd if=\"$o\" ") {
402 continue;
403 }
404 if line.contains("| gzip -dc >\"$t.c.$$\"") {
405 if cpu == "aarch64" {
406 parsed.macos_loader_source_aarch64 = range(line);
407 }
408 } else if line.contains("| gzip -dc >\"$t.$$\"") {
409 let patched = lines
410 .peek()
411 .is_some_and(|next| next.starts_with("dd if=\"$t.$$\" of=\"$t.$$\""));
412 match (cpu, patched) {
413 ("x86_64", true) => parsed.macos_loader_x86_64 = range(line),
414 ("x86_64", false) => parsed.linux_loader_x86_64 = range(line),
415 ("aarch64", false) => parsed.linux_loader_aarch64 = range(line),
416 _ => {}
417 }
418 }
419 }
420 parsed
421 }
422
423 pub fn linux_loader(&self, machine: &str) -> Option<Range> {
425 match machine {
426 "x86_64" => self.linux_loader_x86_64,
427 "aarch64" => self.linux_loader_aarch64,
428 _ => None,
429 }
430 }
431}
432
433fn range(line: &str) -> Option<Range> {
434 let field = |key: &str| {
435 line.split_whitespace()
436 .find_map(|token| token.strip_prefix(key))
437 .and_then(|value| value.parse::<u64>().ok())
438 };
439 Some((field("skip=")?, field("count=")?))
440}
441
442pub fn loader_blob_range(prologue: &[u8], machine: &str) -> Option<Range> {
444 Prologue::parse(prologue).linux_loader(machine)
445}
446
447type ImageKey = (PathBuf, u64, Option<SystemTime>, Vec<PathBuf>);
451
452struct Memo(Mutex<Option<HashMap<ImageKey, PathBuf>>>);
453
454impl Memo {
455 const fn new() -> Self {
456 Self(Mutex::new(None))
457 }
458
459 fn get(&self, key: &ImageKey) -> Option<PathBuf> {
460 let guard = self.0.lock().unwrap_or_else(|error| error.into_inner());
461 let hit = guard.as_ref()?.get(key)?.clone();
462 hit.exists().then_some(hit)
464 }
465
466 fn put(&self, key: ImageKey, path: PathBuf) {
467 let mut guard = self.0.lock().unwrap_or_else(|error| error.into_inner());
468 guard.get_or_insert_with(HashMap::new).insert(key, path);
469 }
470}
471
472static LOADER_MEMO: Memo = Memo::new();
473
474fn image_key(image: &Path, cache_dirs: &[PathBuf]) -> Option<ImageKey> {
475 let meta = std::fs::metadata(image).ok()?;
476 Some((
477 image.to_path_buf(),
478 meta.len(),
479 meta.modified().ok(),
480 cache_dirs.to_vec(),
481 ))
482}
483
484pub fn embedded_loader(image: &Path, cache_dirs: &[PathBuf]) -> Option<PathBuf> {
489 if LOADER_HOST == LoaderHost::None {
490 return None;
491 }
492 let key = image_key(image, cache_dirs)?;
493 if let Some(hit) = LOADER_MEMO.get(&key) {
494 return Some(hit);
495 }
496 let loader = install_embedded_loader(image, cache_dirs)?;
497 LOADER_MEMO.put(key, loader.clone());
498 Some(loader)
499}
500
501#[cfg(feature = "ape-loader")]
502fn install_embedded_loader(image: &Path, cache_dirs: &[PathBuf]) -> Option<PathBuf> {
503 let machine = std::env::consts::ARCH;
504 match (LOADER_HOST, machine) {
505 (LoaderHost::Linux, _) => {
506 let bytes = extract_loader(image, machine)?;
507 let name = format!("ape-loader-linux-{machine}-{}", short_hash(&bytes));
508 materialize(&bytes, &name, cache_dirs)
509 }
510 (LoaderHost::Macos, "x86_64") => {
511 let bytes = extract_macos_x86_64_loader(image)?;
512 let name = format!("ape-loader-macos-{machine}-{}", short_hash(&bytes));
513 materialize(&bytes, &name, cache_dirs)
514 }
515 (LoaderHost::Macos, "aarch64") => compile_macos_aarch64_loader(image, cache_dirs),
516 _ => None,
517 }
518}
519
520#[cfg(not(feature = "ape-loader"))]
521fn install_embedded_loader(_image: &Path, _cache_dirs: &[PathBuf]) -> Option<PathBuf> {
522 None
523}
524
525#[cfg(feature = "ape-loader")]
528fn materialize(bytes: &[u8], name: &str, dirs: &[PathBuf]) -> Option<PathBuf> {
529 install(dirs, None, name, bytes).or_else(|| crate::ape_anonymous_executable(bytes, name))
530}
531
532fn short_hash(bytes: &[u8]) -> String {
535 let hash = bytes.iter().fold(0xcbf2_9ce4_8422_2325_u64, |hash, &byte| {
536 (hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3)
537 });
538 format!("{hash:016x}")
539}
540
541pub fn install(
545 dirs: &[PathBuf],
546 subdir: Option<&str>,
547 name: &str,
548 bytes: &[u8],
549) -> Option<PathBuf> {
550 dirs.iter().find_map(|dir| match subdir {
551 Some(sub) => crate::ape_private_exec_dir(dir)
553 .then(|| install_in(&dir.join(sub), bytes, name))
554 .flatten(),
555 None => install_in(dir, bytes, name),
556 })
557}
558
559pub fn install_in(dir: &Path, bytes: &[u8], name: &str) -> Option<PathBuf> {
564 use std::io::Write;
565 use std::sync::atomic::{AtomicU64, Ordering};
566
567 if !crate::ape_private_exec_dir(dir) {
568 return None;
569 }
570 let target = dir.join(name);
571 if is_installed(&target, bytes) {
572 return Some(target);
573 }
574 static SEQ: AtomicU64 = AtomicU64::new(0);
575 let staging = dir.join(format!(
576 ".{name}.{}.{}",
577 std::process::id(),
578 SEQ.fetch_add(1, Ordering::Relaxed)
579 ));
580 let written = {
583 let _fork = exclusive_fork_guard();
584 std::fs::OpenOptions::new()
585 .write(true)
586 .create_new(true)
587 .open(&staging)
588 .and_then(|mut file| {
589 file.write_all(bytes)?;
590 file.sync_all()
591 })
592 }
593 .and_then(|()| crate::ape_mark_executable(&staging))
594 .and_then(|()| std::fs::rename(&staging, &target));
597 if written.is_err() {
598 let _ = std::fs::remove_file(&staging);
599 return None;
600 }
601 is_installed(&target, bytes).then_some(target)
602}
603
604fn is_installed(path: &Path, bytes: &[u8]) -> bool {
607 std::fs::symlink_metadata(path).is_ok_and(|meta| {
608 meta.file_type().is_file()
609 && crate::ape_is_executable(&meta)
610 && meta.len() == bytes.len() as u64
611 }) && std::fs::read(path).is_ok_and(|content| content == bytes)
612}
613
614fn ape_path_dir(loader: &Path, cache_dirs: &[PathBuf]) -> Option<PathBuf> {
620 if loader.file_name().is_some_and(|name| name == "sh") || loader.starts_with("/proc/self") {
621 return None;
622 }
623 if loader.file_name().is_some_and(|name| name == "ape") {
624 return loader.parent().map(Path::to_path_buf);
625 }
626 let bytes = std::fs::read(loader).ok()?;
627 if bytes.len() as u64 > MAX_PATH_LOADER_BYTES {
628 return None;
629 }
630 let sub = format!("bin-{}", short_hash(&bytes));
631 install(cache_dirs, Some(&sub), "ape", &bytes)?
632 .parent()
633 .map(Path::to_path_buf)
634}
635
636const MAX_PATH_LOADER_BYTES: u64 = 4 * 1024 * 1024;
638
639#[cfg(feature = "ape-loader")]
641struct Image {
642 file: File,
643 len: u64,
644 prologue: Prologue,
645}
646
647#[cfg(feature = "ape-loader")]
648impl Image {
649 fn open(image: &Path) -> Option<Self> {
650 let mut file = File::open(image).ok()?;
651 let len = file.metadata().ok()?.len();
652 let mut head = Vec::new();
653 (&mut file)
654 .take(PROLOGUE_SCAN_BYTES)
655 .read_to_end(&mut head)
656 .ok()?;
657 if !is_ape_header(&head) {
658 return None;
659 }
660 Some(Self {
661 file,
662 len,
663 prologue: Prologue::parse(&head),
664 })
665 }
666
667 fn read(&mut self, (skip, count): Range) -> Option<Vec<u8>> {
669 use std::io::{Seek, SeekFrom};
670 let end = skip.checked_add(count)?;
671 if count == 0 || count > MAX_LOADER_BYTES || end > self.len {
672 return None;
673 }
674 self.file.seek(SeekFrom::Start(skip)).ok()?;
675 let mut out = Vec::with_capacity(count as usize);
676 (&mut self.file).take(count).read_to_end(&mut out).ok()?;
677 (out.len() as u64 == count).then_some(out)
678 }
679
680 fn inflate(&mut self, range: Range) -> Option<Vec<u8>> {
681 gunzip(&self.read(range)?, MAX_LOADER_BYTES as usize).ok()
682 }
683}
684
685#[cfg(feature = "ape-loader")]
688fn elf_machine(machine: &str) -> Option<u16> {
689 match machine {
690 "x86_64" => Some(62), "aarch64" => Some(183), _ => None,
693 }
694}
695
696#[cfg(feature = "ape-loader")]
699pub fn extract_loader(image: &Path, machine: &str) -> Option<Vec<u8>> {
700 let elf_machine = elf_machine(machine)?;
701 let mut image = Image::open(image)?;
702 let range = image.prologue.linux_loader(machine)?;
703 let elf = image.inflate(range)?;
704 is_static_elf_for(&elf, elf_machine).then_some(elf)
705}
706
707#[cfg(feature = "ape-loader")]
711pub fn extract_macos_x86_64_loader(image: &Path) -> Option<Vec<u8>> {
712 let mut image = Image::open(image)?;
713 let range = image.prologue.macos_loader_x86_64?;
714 let mut bin = image.inflate(range)?;
715 if bin.len() < 320 + 512 {
716 return None;
717 }
718 bin.copy_within(320..320 + 512, 0);
719 is_macho_x86_64(&bin).then_some(bin)
720}
721
722#[cfg(feature = "ape-loader")]
724pub fn extract_macos_aarch64_loader_source(image: &Path) -> Option<Vec<u8>> {
725 let mut image = Image::open(image)?;
726 let range = image.prologue.macos_loader_source_aarch64?;
727 let source = image.inflate(range)?;
728 (std::str::from_utf8(&source).is_ok() && source.windows(4).any(|window| window == b"main"))
729 .then_some(source)
730}
731
732#[cfg(feature = "ape-loader")]
736fn compile_macos_aarch64_loader(image: &Path, dirs: &[PathBuf]) -> Option<PathBuf> {
737 let source = extract_macos_aarch64_loader_source(image)?;
738 let name = format!("ape-loader-macos-aarch64-{}", short_hash(&source));
739 let dir = dirs.iter().find(|dir| crate::ape_private_exec_dir(dir))?;
740 let target = dir.join(&name);
741 if first_executable([target.clone()]).is_some() {
742 return Some(target);
743 }
744 let tag = format!(
745 "{}.{}",
746 std::process::id(),
747 short_hash(target.as_os_str().as_encoded_bytes())
748 );
749 let source_path = dir.join(format!(".{name}.{tag}.c"));
750 let out_path = dir.join(format!(".{name}.{tag}"));
751 std::fs::write(&source_path, &source).ok()?;
752 let cc =
753 first_executable([PathBuf::from("/usr/bin/cc")]).unwrap_or_else(|| PathBuf::from("cc"));
754 let mut command = std::process::Command::new(&cc);
755 command
756 .args(["-w", "-O", "-o"])
757 .arg(&out_path)
758 .arg(&source_path)
759 .stdin(std::process::Stdio::null());
760 let compiled = retry_while_busy(|| {
761 let _fork = fork_guard();
762 command.output()
763 });
764 let _ = std::fs::remove_file(&source_path);
765 let built = compiled.is_ok_and(|output| output.status.success())
766 && std::fs::rename(&out_path, &target).is_ok();
767 if !built {
768 let _ = std::fs::remove_file(&out_path);
769 return None;
770 }
771 Some(target)
772}
773
774#[cfg(feature = "ape-loader")]
776fn is_static_elf_for(elf: &[u8], machine: u16) -> bool {
777 elf.len() >= 64
778 && elf.len() as u64 <= MAX_LOADER_BYTES
779 && elf.starts_with(b"\x7fELF")
780 && elf[4] == 2 && elf[5] == 1 && matches!(u16::from_le_bytes([elf[16], elf[17]]), 2 | 3)
784 && u16::from_le_bytes([elf[18], elf[19]]) == machine
785}
786
787#[cfg(feature = "ape-loader")]
789fn is_macho_x86_64(bin: &[u8]) -> bool {
790 bin.len() >= 32 && bin.starts_with(&[0xcf, 0xfa, 0xed, 0xfe]) && bin[4..8] == [7, 0, 0, 1]
791}
792
793#[cfg(feature = "ape-loader")]
795pub fn gunzip(member: &[u8], limit: usize) -> io::Result<Vec<u8>> {
796 const FHCRC: u8 = 0x02;
797 const FEXTRA: u8 = 0x04;
798 const FNAME: u8 = 0x08;
799 const FCOMMENT: u8 = 0x10;
800
801 if member.len() < 18 || member[..3] != [0x1f, 0x8b, 8] {
802 return Err(invalid("not a deflate gzip member"));
803 }
804 let flags = member[3];
805 let mut at = 10;
806 if flags & FEXTRA != 0 {
807 let extra = member
808 .get(at..at + 2)
809 .ok_or_else(|| invalid("truncated gzip"))?;
810 at += 2 + usize::from(u16::from_le_bytes([extra[0], extra[1]]));
811 }
812 for flag in [FNAME, FCOMMENT] {
813 if flags & flag != 0 {
814 let terminator = member
815 .get(at..)
816 .and_then(|rest| rest.iter().position(|&byte| byte == 0))
817 .ok_or_else(|| invalid("truncated gzip"))?;
818 at += terminator + 1;
819 }
820 }
821 if flags & FHCRC != 0 {
822 at += 2;
823 }
824 let trailer = member.len() - 8;
825 let deflate = member
826 .get(at..trailer)
827 .ok_or_else(|| invalid("truncated gzip"))?;
828 let output = miniz_oxide::inflate::decompress_to_vec_with_limit(deflate, limit)
829 .map_err(|error| invalid(&format!("corrupt gzip member: {error}")))?;
830 let size = u32::from_le_bytes(member[trailer + 4..].try_into().expect("four bytes"));
831 if output.len() as u32 != size {
832 return Err(invalid("gzip member size does not match its trailer"));
833 }
834 Ok(output)
835}
836
837#[cfg(feature = "ape-loader")]
838fn invalid(message: &str) -> io::Error {
839 io::Error::new(io::ErrorKind::InvalidData, message.to_owned())
840}
841
842pub fn command(program: impl AsRef<OsStr>) -> std::process::Command {
853 let program = program.as_ref();
854 let options = ApeOptions::inherited();
855 match plan_launch(program, None, &options) {
856 Some(launch) => {
857 let mut command = std::process::Command::new(&launch.loader);
858 command.arg(&launch.image);
859 if let Some(path) = launch.child_path(options.path.as_deref()) {
860 command.env("PATH", path);
861 }
862 command
863 }
864 None => std::process::Command::new(program),
865 }
866}
867
868#[cfg(feature = "async-process")]
870pub fn tokio_command(program: impl AsRef<OsStr>) -> tokio::process::Command {
871 let program = program.as_ref();
872 let options = ApeOptions::inherited();
873 match plan_launch(program, None, &options) {
874 Some(launch) => {
875 let mut command = tokio::process::Command::new(&launch.loader);
876 command.arg(&launch.image);
877 if let Some(path) = launch.child_path(options.path.as_deref()) {
878 command.env("PATH", path);
879 }
880 command
881 }
882 None => tokio::process::Command::new(program),
883 }
884}
885
886pub fn prepare_std_retry(command: &mut std::process::Command, error: &io::Error) -> bool {
894 if !retryable(command, error) {
895 return false;
896 }
897 crate::ape_route_through_execvp(command);
898 true
899}
900
901#[cfg(feature = "async-process")]
903pub fn prepare_tokio_retry(command: &mut tokio::process::Command, error: &io::Error) -> bool {
904 if !retryable(command.as_std(), error) {
905 return false;
906 }
907 crate::ape_route_tokio_through_execvp(command);
908 true
909}
910
911fn retryable(command: &std::process::Command, error: &io::Error) -> bool {
912 if !EXECVP_SHELL_FALLBACK || !is_exec_format_error(error) {
913 return false;
914 }
915 let options = ApeOptions::with_overrides(false, command.get_envs());
916 resolve_program(
917 command.get_program(),
918 command.get_current_dir(),
919 options.path.as_deref(),
920 )
921 .is_some_and(|image| is_ape_file(&image))
922}
923
924pub fn spawn_std<T>(
928 command: &mut std::process::Command,
929 mut spawn: impl FnMut(&mut std::process::Command) -> io::Result<T>,
930) -> io::Result<T> {
931 let first = retry_while_busy(|| {
932 let _fork = fork_guard();
933 spawn(command)
934 });
935 match first {
936 Err(error) if prepare_std_retry(command, &error) => {
937 let _fork = fork_guard();
938 spawn(command)
939 }
940 result => result,
941 }
942}
943
944#[cfg(feature = "async-process")]
947pub fn spawn_tokio<T>(
948 command: &mut tokio::process::Command,
949 mut spawn: impl FnMut(&mut tokio::process::Command) -> io::Result<T>,
950) -> io::Result<T> {
951 let first = retry_while_busy(|| {
952 let _fork = fork_guard();
953 spawn(command)
954 });
955 match first {
956 Err(error) if prepare_tokio_retry(command, &error) => {
957 let _fork = fork_guard();
958 spawn(command)
959 }
960 result => result,
961 }
962}
963
964#[cfg(test)]
965#[path = "ape_tests.rs"]
966mod tests;