Skip to content

Fix binary name resolution against custom PATH on macOS - #55672

Merged
kubkon merged 5 commits into
zed-industries:mainfrom
G36maid:fix/posix-spawnp-path-resolution
Jun 19, 2026
Merged

Fix binary name resolution against custom PATH on macOS#55672
kubkon merged 5 commits into
zed-industries:mainfrom
G36maid:fix/posix-spawnp-path-resolution

Conversation

@G36maid

@G36maid G36maid commented May 4, 2026

Copy link
Copy Markdown
Contributor

Closes #50536

Summary

Addresses #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).

The regression was introduced when PR #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:

  • I've reviewed my own diff for quality, security, and reliability
  • Unsafe blocks (if any) have justifying comments
  • The content is consistent with the UI/UX checklist
  • Tests cover the new/changed behavior
  • 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)

@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label May 4, 2026
@G36maid G36maid changed the title util: Fix bare binary name resolution against child PATH on macOS fix: regression with use of relative path for external formatter May 4, 2026
@G36maid
G36maid force-pushed the fix/posix-spawnp-path-resolution branch from 881ad73 to 90267fb Compare May 4, 2026 17:22
@G36maid G36maid changed the title fix: regression with use of relative path for external formatter Fix bare binary name resolution against custom PATH on macOS May 4, 2026
@G36maid G36maid changed the title Fix bare binary name resolution against custom PATH on macOS Fix binary name resolution against custom PATH on macOS May 4, 2026
@G36maid
G36maid marked this pull request as ready for review May 4, 2026 17:30
@G36maid
G36maid force-pushed the fix/posix-spawnp-path-resolution branch from 90267fb to 2caea8d Compare May 4, 2026 17:31
@G36maid
G36maid marked this pull request as draft May 4, 2026 17:39
On macOS, posix_spawnp resolves bare program names using the parent
process's environ (via getenv("PATH")), ignoring the PATH inside the
custom envp argument. This causes commands like external formatters or
LSP binaries that are only available in a project-specific PATH (e.g.
from Nix/direnv) to fail with ENOENT.

Before calling posix_spawnp, if the program name contains no slash and
a custom environment with a PATH entry is provided, resolve the program
to an absolute path using which::which_in against the child's PATH.

Update darwin.rs
@G36maid
G36maid force-pushed the fix/posix-spawnp-path-resolution branch from 2caea8d to 598a7fb Compare May 4, 2026 17:43
@G36maid
G36maid marked this pull request as ready for review May 4, 2026 17:44
@ChristopherBiscardi ChristopherBiscardi added the platform:macOS Platform-specific feedback for macOS behaviors, features, design, etc label May 4, 2026
@zed-industries-bot

zed-industries-bot commented May 5, 2026

Copy link
Copy Markdown
Contributor
Messages
📖

This PR includes links to the following GitHub Issues: #50536
If this PR aims to close an issue, please include a Closes #ISSUE line at the top of the PR body.

Generated by 🚫 dangerJS against f7a9398

@MrSubidubi
MrSubidubi requested a review from kubkon May 7, 2026 07:54

@dnlzro dnlzro left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to add an import before it would build, but otherwise all good. Addresses the linked issue on my Mac.

Comment thread crates/util/src/command/darwin.rs Outdated

@kubkon kubkon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thanks! I merged main and fixed a couple of tiny nits.

@kubkon
kubkon added this pull request to the merge queue Jun 19, 2026
Merged via the queue into zed-industries:main with commit d1f500e Jun 19, 2026
34 checks passed
@G36maid
G36maid deleted the fix/posix-spawnp-path-resolution branch June 20, 2026 03:33
saranblock3 pushed a commit to saranblock3/zed that referenced this pull request Jun 23, 2026
…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>
weihuoya pushed a commit to weihuoya/zed that referenced this pull request Jun 25, 2026
…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>
jolutz pushed a commit to jolutz/zed that referenced this pull request Aug 8, 2026
…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>
vitallium added a commit to zed-extensions/ruby that referenced this pull request Aug 29, 2026
Zed now resolves bare executables against the child `PATH` on macOS
before `posix_spawnp`.
See zed-industries/zed#55672, so extension-side
ruby/gem/bundle probes no longer need a compile-time workaround.

Refs zed-industries/zed#57170
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement platform:macOS Platform-specific feedback for macOS behaviors, features, design, etc

Projects

None yet

Development

Successfully merging this pull request may close these issues.

regression with use of relative path for external formatter

6 participants