Command: Support posix_spawn() on FreeBSD/OSX/GNU Linux#48624
Conversation
spawn() is expected to return an error if the specified file could not be executed. FreeBSD's posix_spawn() supports returning ENOENT/ENOEXEC if the exec() fails, which not all platforms support. This brings a very significant performance improvement for FreeBSD, involving heavy use of Command in threads, due to fork() invoking jemalloc fork handlers and causing lock contention. FreeBSD's posix_spawn() avoids this problem due to using vfork() internally.
|
FYI @alexcrichton, didn't assign you since you wrote the majority of the changes here. |
|
Nice! For posterity I originally didn't land this because it broke our test suite, notably the behavior of @bdrewery you were mentioning that a |
|
FreeBSD's It's able to do this because of This is intended and documented behavior. |
|
Ah ok! I think I must have misunderstood then, that sounds perfect! @bors: r+ |
|
📌 Commit 85b82f2 has been approved by |
|
🌲 The tree is currently closed for pull requests below priority 200, this pull request will be tested once the tree is reopened |
Just for reference, those commits are contained in glibc-2.24. |
|
@cuviper is there a glibc function to call that returns the version? |
|
Yes, |
|
Oh nice catch @cuviper! I was testing locally with 2.23 when I initially wrote this, and I found that this program: use std::process::Command;
fn main() {
println!("{:?}", Command::new("nonexistent").spawn());
} where on today's standard library it prints an error but that's with running glibc 2.23 locally. Taking the same binary and running it with glibc 2.24 it prints an error (as today's rustc does). I haven't tested on OSX yet but sounds like we can certainly do this for Linux too! We'll just need to check at runtime that the version is >= 2.24. I also just tested with musl and it looks like they've implemented it in such a way (presumably the same way?) that prints an error as well, so on musl we can unconditionally use posix_spawn. |
|
@bdrewery would you be up for putting some of these checks in this PR? Or should I file a follow-up issue? |
|
@alexcrichton I had thought the command-exec.rs cases covered this but I see now they don't use |
|
It looks like I was just looking over OSX's opensource code and their It seems like they based their implementation on NetBSD's. |
|
@bdrewery also to be clear I don't want to drag you into a bunch of portability nightmare scenarios, if you'd like to just do FreeBSD and be done with it that's perfectly ok! |
|
@alexcrichton yup understood, I don't mind looking at some of these right now. |
|
Tests pass on OSX for me so I've added support for it in, and fixed the error handling. Working on Linux support still. |
|
📌 Commit 8e0faf7 has been approved by |
…xcrichton Command: Support posix_spawn() on FreeBSD/OSX/GNU Linux
|
@bors r- The |
|
@bors: r+ |
|
📌 Commit 70559c5 has been approved by |
…xcrichton Command: Support posix_spawn() on FreeBSD/OSX/GNU Linux
…es#55672) Closes zed-industries#50536 ## Summary Addresses zed-industries#50536 - On macOS, `posix_spawnp` resolves programs against the parent process's `cwd` and `environ` (via `getcwd` and `getenv("PATH")`), ignoring the child's `current_dir` and `envp`. This causes two classes of failures when Zed spawns external commands via the custom `posix_spawnp`-based `Command`: - **Bare names** (e.g. `"black"`): resolved via the parent's `PATH`, not the child's — binaries only available in a project-specific PATH (Nix/direnv) are not found. - **Relative paths** (e.g. `"./script.sh"`, `"bin/test.sh"`): resolved against the parent's `cwd`, not `current_dir` — only works when Zed's cwd happens to match the project root. - Added program resolution in `spawn_posix_spawn` (`crates/util/src/command/darwin.rs`): before calling `posix_spawnp`, resolve the program to an absolute path — bare names via `which::which_in` against the child's PATH, relative paths via `Path::join(current_dir)`. Falls back to the original program if resolution fails. ## Root Cause Apple's Libc implementation of `posix_spawnp` resolves the program path using the parent process's context — `getcwd()` for relative paths and `getenv("PATH")` for bare names — rather than the `current_dir` (set via `posix_spawn_file_actions_addchdir_np`) or `envp` argument. The child's working directory and environment only take effect **after** the binary has already been located. This is a well-documented macOS behavior that Rust's own `std::process::Command` works around by bypassing `posix_spawn` when PATH is modified (see [rust-lang/rust#48624](rust-lang/rust#48624)). The regression was introduced when PR zed-industries#49090 switched macOS from `std::process::Command` (which uses fork+execvp, correctly using the child's cwd and PATH) to a custom posix_spawnp-based implementation (which does not). ## Testing - `test_bare_program_resolved_via_custom_path` — bare name resolves via child's custom PATH - `test_bare_program_with_custom_path_falls_back_when_not_found` — non-existent binary still errors - `test_bare_program_with_custom_env_no_path_key` — custom env without PATH key falls back gracefully - `test_relative_path_skips_resolution` — relative path resolves against `current_dir` instead of parent's cwd Note: The fix and tests are in `darwin.rs` which is macOS-only. The Linux path uses `smol::process::Command` (via fork+execve) and is unaffected. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Fixed external formatters and language servers failing to launch on macOS when specified as a bare binary name or relative path and only available in the project's PATH (e.g. Nix, direnv) --------- Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
No description provided.