feat(macos): port Ghostty's macOS code paths (Metal, CoreText, Darwin)
Ports the macOS-specific Zig code behind cfg(target_os = "macos"): - renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend (all renderer/metal/* files, shaders.metal verbatim, runtime library compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga; OpenGL remains the backend elsewhere. - font: CoreText face, shaper and discovery with build.rs backend selection (coretext default; coretext-freetype / coretext-harfbuzz / coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple. - input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes. - os: macos, mach, cf_release_thread, NSProcessInfo args and macOS branches; config: macOS defaults, keybinds, Application Support paths. - termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit handling, os_log logging. Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot run here. The embedded runtime/C API (embedded.zig) is the next step. Also includes the cross-platform integration edits shared by the crash, verification, Windows and macOS work (Cargo target-gating, thread crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg gates), a cfg fix so ghostty-config builds on wasm32, and an allow for the intentional recursion in the crash overflow test. Test Plan: - cargo test --workspace (3224 passed) - cargo clippy --workspace --all-targets -- -D warnings - cargo check --target aarch64-apple-darwin -p ghostty-renderer (with the documented cross env)
This commit is contained in:
1 parent
76771bed0d
commit
bab49ee596
124 files changed
+14936
-1242
No files matched your search
@@ -221,9 +221,37 @@ impl std::fmt::Debug for Command {
|
||||
|
||||
/// Everything that must be allocated before forking.
|
||||
struct Prepared {
|
||||
#[cfg_attr(target_vendor = "apple", allow(dead_code))]
|
||||
path: CString,
|
||||
argv: Vec<CString>,
|
||||
envp: Vec<CString>,
|
||||
/// Darwin has no `execvpe`: the candidate executable paths (the PATH
|
||||
/// search of Zig's `execvpeZ`, precomputed so the child doesn't allocate).
|
||||
#[cfg(target_vendor = "apple")]
|
||||
candidates: Vec<CString>,
|
||||
}
|
||||
|
||||
/// The default PATH used when `PATH` is unset (Zig `std.posix.execvpeZ`:
|
||||
/// `/usr/local/bin:/bin/:/usr/bin`).
|
||||
#[cfg(any(target_vendor = "apple", test))]
|
||||
const DEFAULT_PATH: &str = "/usr/local/bin:/bin/:/usr/bin";
|
||||
|
||||
/// The paths `execvpe` tries for `file` given a PATH value: the file itself
|
||||
/// if it contains a slash, otherwise `<dir>/<file>` per PATH entry (an empty
|
||||
/// entry means the current directory).
|
||||
#[cfg(any(target_vendor = "apple", test))]
|
||||
fn exec_candidates(file: &str, path_env: Option<&str>) -> Vec<String> {
|
||||
if file.contains('/') {
|
||||
return vec![file.to_owned()];
|
||||
}
|
||||
path_env
|
||||
.unwrap_or(DEFAULT_PATH)
|
||||
.split(':')
|
||||
.map(|dir| {
|
||||
let dir = if dir.is_empty() { "." } else { dir };
|
||||
format!("{dir}/{file}")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl Command {
|
||||
@@ -266,7 +294,18 @@ impl Command {
|
||||
})
|
||||
.collect::<Result<_, _>>()?,
|
||||
};
|
||||
Ok(Prepared { path, argv, envp })
|
||||
#[cfg(target_vendor = "apple")]
|
||||
let candidates = exec_candidates(&self.path, std::env::var("PATH").ok().as_deref())
|
||||
.into_iter()
|
||||
.map(CString::new)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(Prepared {
|
||||
path,
|
||||
argv,
|
||||
envp,
|
||||
#[cfg(target_vendor = "apple")]
|
||||
candidates,
|
||||
})
|
||||
}
|
||||
|
||||
/// Start the subprocess. This returns immediately once the child is
|
||||
@@ -409,6 +448,16 @@ impl Command {
|
||||
fn setup_fd(dup: impl Fn(&OwnedFd) -> rustix::io::Result<()>, src: &OwnedFd) -> Result<(), Errno> {
|
||||
// dup2 (like Zig's dup3 with flags=0) leaves the target without
|
||||
// CLOEXEC: we do NOT want this file descriptor to be closed on exec.
|
||||
//
|
||||
// Mac doesn't support dup3 so Zig uses dup2 there, and purposely
|
||||
// clears CLOEXEC on the source fd first.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
{
|
||||
let flags = rustix::io::fcntl_getfd(src)?;
|
||||
if flags.contains(rustix::io::FdFlags::CLOEXEC) {
|
||||
rustix::io::fcntl_setfd(src, flags - rustix::io::FdFlags::CLOEXEC)?;
|
||||
}
|
||||
}
|
||||
loop {
|
||||
match dup(src) {
|
||||
Err(Errno::INTR) => {}
|
||||
@@ -442,6 +491,7 @@ fn fork() -> Result<i32, Errno> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
#[allow(unsafe_code)]
|
||||
fn execvpe(prep: &Prepared, argv: &[*const c_char], envp: &[*const c_char]) -> Errno {
|
||||
debug_assert!(argv.last().is_some_and(|p| p.is_null()));
|
||||
@@ -455,12 +505,50 @@ fn execvpe(prep: &Prepared, argv: &[*const c_char], envp: &[*const c_char]) -> E
|
||||
Errno::from_raw_os_error(errno_now())
|
||||
}
|
||||
|
||||
/// Darwin has no `execvpe`; do the PATH search by hand like Zig's
|
||||
/// `execvpeZ` (EACCES is remembered, ENOENT/ENOTDIR continue the search,
|
||||
/// anything else is returned immediately).
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[allow(unsafe_code)]
|
||||
fn execvpe(prep: &Prepared, argv: &[*const c_char], envp: &[*const c_char]) -> Errno {
|
||||
debug_assert!(argv.last().is_some_and(|p| p.is_null()));
|
||||
debug_assert!(envp.last().is_some_and(|p| p.is_null()));
|
||||
let mut seen_eacces = false;
|
||||
for candidate in &prep.candidates {
|
||||
// SAFETY: the candidate is a valid NUL-terminated string; argv/envp
|
||||
// are NULL-terminated arrays of pointers into CStrings (owned by
|
||||
// `prep`) that outlive the call. execve only returns on failure.
|
||||
unsafe {
|
||||
libc::execve(candidate.as_ptr(), argv.as_ptr(), envp.as_ptr());
|
||||
}
|
||||
let err = errno_now();
|
||||
match err {
|
||||
libc::EACCES => seen_eacces = true,
|
||||
libc::ENOENT | libc::ENOTDIR => {}
|
||||
_ => return Errno::from_raw_os_error(err),
|
||||
}
|
||||
}
|
||||
Errno::from_raw_os_error(if seen_eacces {
|
||||
libc::EACCES
|
||||
} else {
|
||||
libc::ENOENT
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
#[allow(unsafe_code)]
|
||||
fn errno_now() -> i32 {
|
||||
// SAFETY: __errno_location returns a valid thread-local pointer.
|
||||
unsafe { *libc::__errno_location() }
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[allow(unsafe_code)]
|
||||
fn errno_now() -> i32 {
|
||||
// SAFETY: __error returns a valid thread-local pointer.
|
||||
unsafe { *libc::__error() }
|
||||
}
|
||||
|
||||
fn write_stderr(msg: &[u8]) {
|
||||
// Best effort, no allocation.
|
||||
let _ = rustix::io::write(borrow_stderr(), msg);
|
||||
@@ -910,4 +998,18 @@ mod tests {
|
||||
assert!(linux_cgroup_post_fork(&cmd, &hard, |_, _, _| None).is_err());
|
||||
assert!(linux_cgroup_post_fork(&cmd, &hard, |_, _, _| Some(Err("x".into()))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_candidates_search_path() {
|
||||
assert_eq!(exec_candidates("/bin/sh", Some("/x")), ["/bin/sh"]);
|
||||
assert_eq!(exec_candidates("a/b", None), ["a/b"]);
|
||||
assert_eq!(
|
||||
exec_candidates("sh", Some("/usr/bin::/bin")),
|
||||
["/usr/bin/sh", "./sh", "/bin/sh"]
|
||||
);
|
||||
assert_eq!(
|
||||
exec_candidates("sh", None),
|
||||
["/usr/local/bin/sh", "/bin//sh", "/usr/bin/sh"]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user