Files
codex/scripts/codex_package/targets.py
Michael Bolin c7bcb90f9b package: include zsh fork in Codex package (#23756)
## Why

The package layout gives Codex a stable place for runtime helpers that
should travel with the entrypoint. `shell_zsh_fork` still required users
to configure `zsh_path` manually, even though we already publish
prebuilt zsh fork artifacts.

This PR builds on #24129 and uses the shared DotSlash artifact fetcher
to include the zsh fork in Codex packages when a matching target
artifact exists. Packaged Codex builds can then discover the bundled
fork automatically; the user/profile `zsh_path` override is removed so
the feature uses the package-managed artifact instead of a legacy path
knob.

## What Changed

- Added `scripts/codex_package/codex-zsh`, a checked-in DotSlash
manifest for the current macOS arm64 and Linux zsh fork artifacts.
- Taught `scripts/build_codex_package.py` to fetch the matching zsh fork
artifact and install it at `codex-resources/zsh/bin/zsh` when available
for the selected target.
- Added package layout validation for the optional bundled zsh resource.
- Added `InstallContext::bundled_zsh_path()` and
`InstallContext::bundled_zsh_bin_dir()` for package-layout resource
discovery.
- Threaded the packaged zsh path through config loading as the runtime
`zsh_path` for packaged installs, and removed the config/profile/CLI
override path.
- Kept the packaged default zsh override typed as `AbsolutePathBuf`
until the existing runtime `Config::zsh_path` boundary.
- Updated app-server zsh-fork integration tests to spawn
`codex-app-server` from a temporary package layout with
`codex-resources/zsh/bin/zsh`, matching the new packaged discovery path
instead of setting `zsh_path` in config.
- Switched package executable copying from metadata-preserving `copy2()`
to `copyfile()` plus explicit executable bits, which avoids macOS
file-flag failures when local smoke tests use system binaries as inputs.

## Testing

To verify that the `zsh` executable from the Codex package is picked up
correctly, first I ran:

```shell
./scripts/build_codex_package.py
```

which created:

```
/private/var/folders/vw/x2knqmks50sfhfpy27nftl900000gp/T/codex-package-pms94kdp/
```

so then I ran:

```
/private/var/folders/vw/x2knqmks50sfhfpy27nftl900000gp/T/codex-package-pms94kdp/bin/codex exec --enable shell_zsh_fork 'run `echo $0`'
```

which reported the following, as expected:

```
/private/var/folders/vw/x2knqmks50sfhfpy27nftl900000gp/T/codex-package-pms94kdp/codex-resources/zsh/bin/zsh
```



---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/23756).
* #23768
* __->__ #23756
2026-05-22 17:54:07 -07:00

165 lines
4.4 KiB
Python

"""Supported package targets and default binary discovery."""
import platform
import stat
from dataclasses import dataclass
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parents[1]
REPO_ROOT = SCRIPT_DIR.parent
@dataclass(frozen=True)
class TargetSpec:
target: str
is_windows: bool
is_linux: bool
dotslash_platform: str
@property
def exe_suffix(self) -> str:
return ".exe" if self.is_windows else ""
@property
def rg_name(self) -> str:
return f"rg{self.exe_suffix}"
@dataclass(frozen=True)
class PackageVariant:
name: str
cargo_bin: str
executable_stem: str
def entrypoint_name(self, spec: TargetSpec) -> str:
return f"{self.executable_stem}{spec.exe_suffix}"
@dataclass(frozen=True)
class PackageInputs:
entrypoint_bin: Path
rg_bin: Path
zsh_bin: Path | None
bwrap_bin: Path | None
codex_command_runner_bin: Path | None
codex_windows_sandbox_setup_bin: Path | None
PACKAGE_VARIANTS: dict[str, PackageVariant] = {
"codex": PackageVariant(
name="codex",
cargo_bin="codex",
executable_stem="codex",
),
"codex-app-server": PackageVariant(
name="codex-app-server",
cargo_bin="codex-app-server",
executable_stem="codex-app-server",
),
}
TARGET_SPECS: dict[str, TargetSpec] = {
"x86_64-unknown-linux-gnu": TargetSpec(
target="x86_64-unknown-linux-gnu",
is_windows=False,
is_linux=True,
dotslash_platform="linux-x86_64",
),
"x86_64-unknown-linux-musl": TargetSpec(
target="x86_64-unknown-linux-musl",
is_windows=False,
is_linux=True,
dotslash_platform="linux-x86_64",
),
"aarch64-unknown-linux-gnu": TargetSpec(
target="aarch64-unknown-linux-gnu",
is_windows=False,
is_linux=True,
dotslash_platform="linux-aarch64",
),
"aarch64-unknown-linux-musl": TargetSpec(
target="aarch64-unknown-linux-musl",
is_windows=False,
is_linux=True,
dotslash_platform="linux-aarch64",
),
"x86_64-apple-darwin": TargetSpec(
target="x86_64-apple-darwin",
is_windows=False,
is_linux=False,
dotslash_platform="macos-x86_64",
),
"aarch64-apple-darwin": TargetSpec(
target="aarch64-apple-darwin",
is_windows=False,
is_linux=False,
dotslash_platform="macos-aarch64",
),
"x86_64-pc-windows-msvc": TargetSpec(
target="x86_64-pc-windows-msvc",
is_windows=True,
is_linux=False,
dotslash_platform="windows-x86_64",
),
"aarch64-pc-windows-msvc": TargetSpec(
target="aarch64-pc-windows-msvc",
is_windows=True,
is_linux=False,
dotslash_platform="windows-aarch64",
),
}
HOST_RELEASE_TARGETS: dict[tuple[str, str], str] = {
("darwin", "aarch64"): "aarch64-apple-darwin",
("darwin", "x86_64"): "x86_64-apple-darwin",
("linux", "aarch64"): "aarch64-unknown-linux-musl",
("linux", "x86_64"): "x86_64-unknown-linux-musl",
("windows", "aarch64"): "aarch64-pc-windows-msvc",
("windows", "x86_64"): "x86_64-pc-windows-msvc",
}
def default_target() -> str:
system = platform.system().lower()
machine = normalize_machine(platform.machine())
target = HOST_RELEASE_TARGETS.get((system, machine))
if target is None:
supported = ", ".join(sorted(TARGET_SPECS))
raise RuntimeError(
f"Unsupported host platform {platform.system()}/{platform.machine()}. "
f"Pass --target explicitly. Supported targets: {supported}"
)
return target
def resolve_input_path(
explicit_path: Path | None,
description: str,
flag_name: str,
) -> Path:
if explicit_path is not None:
path = explicit_path.resolve()
if not path.is_file():
raise RuntimeError(f"{description} does not exist: {path}")
if not is_executable(path):
raise RuntimeError(f"{description} is not executable: {path}")
return path
raise RuntimeError(f"Must specify {flag_name} for {description}.")
def is_executable(path: Path) -> bool:
return bool(path.stat().st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH))
def normalize_machine(machine: str) -> str:
machine = machine.lower()
if machine in ("amd64", "x86_64"):
return "x86_64"
if machine in ("aarch64", "arm64"):
return "aarch64"
return machine