mirror of
https://github.com/openai/codex.git
synced 2026-05-24 13:04:29 +00:00
## 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
180 lines
5.4 KiB
Python
180 lines
5.4 KiB
Python
"""Canonical Codex package directory layout."""
|
|
|
|
import json
|
|
import shutil
|
|
import stat
|
|
from pathlib import Path
|
|
|
|
from .targets import PackageInputs
|
|
from .targets import PackageVariant
|
|
from .targets import TargetSpec
|
|
from .zsh import ZSH_RESOURCE_PATH
|
|
|
|
|
|
LAYOUT_VERSION = 1
|
|
|
|
|
|
def prepare_package_dir(package_dir: Path, *, force: bool) -> None:
|
|
if package_dir.exists():
|
|
if not package_dir.is_dir():
|
|
raise RuntimeError(f"Package output exists and is not a directory: {package_dir}")
|
|
if any(package_dir.iterdir()):
|
|
if not force:
|
|
raise RuntimeError(
|
|
f"Package output directory is not empty: {package_dir}. "
|
|
"Pass --force to replace it."
|
|
)
|
|
shutil.rmtree(package_dir)
|
|
|
|
package_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def build_package_dir(
|
|
package_dir: Path,
|
|
version: str,
|
|
variant: PackageVariant,
|
|
spec: TargetSpec,
|
|
inputs: PackageInputs,
|
|
) -> None:
|
|
bin_dir = package_dir / "bin"
|
|
resources_dir = package_dir / "codex-resources"
|
|
path_dir = package_dir / "codex-path"
|
|
bin_dir.mkdir()
|
|
resources_dir.mkdir()
|
|
path_dir.mkdir()
|
|
|
|
entrypoint_name = variant.entrypoint_name(spec)
|
|
copy_executable(
|
|
inputs.entrypoint_bin,
|
|
bin_dir / entrypoint_name,
|
|
is_windows=spec.is_windows,
|
|
)
|
|
copy_executable(inputs.rg_bin, path_dir / spec.rg_name, is_windows=spec.is_windows)
|
|
|
|
if inputs.zsh_bin is not None:
|
|
copy_executable(
|
|
inputs.zsh_bin,
|
|
resources_dir / ZSH_RESOURCE_PATH,
|
|
is_windows=False,
|
|
)
|
|
|
|
if inputs.bwrap_bin is not None:
|
|
copy_executable(inputs.bwrap_bin, resources_dir / "bwrap", is_windows=False)
|
|
|
|
if inputs.codex_command_runner_bin is not None:
|
|
copy_executable(
|
|
inputs.codex_command_runner_bin,
|
|
resources_dir / "codex-command-runner.exe",
|
|
is_windows=True,
|
|
)
|
|
|
|
if inputs.codex_windows_sandbox_setup_bin is not None:
|
|
copy_executable(
|
|
inputs.codex_windows_sandbox_setup_bin,
|
|
resources_dir / "codex-windows-sandbox-setup.exe",
|
|
is_windows=True,
|
|
)
|
|
|
|
metadata = {
|
|
"layoutVersion": LAYOUT_VERSION,
|
|
"version": version,
|
|
"target": spec.target,
|
|
"variant": variant.name,
|
|
"entrypoint": f"bin/{entrypoint_name}",
|
|
"resourcesDir": "codex-resources",
|
|
"pathDir": "codex-path",
|
|
}
|
|
write_json(package_dir / "codex-package.json", metadata)
|
|
|
|
|
|
def validate_package_dir(
|
|
package_dir: Path,
|
|
variant: PackageVariant,
|
|
spec: TargetSpec,
|
|
*,
|
|
include_zsh: bool,
|
|
) -> None:
|
|
required_dirs = [
|
|
Path("bin"),
|
|
Path("codex-resources"),
|
|
Path("codex-path"),
|
|
]
|
|
for relative_dir in required_dirs:
|
|
path = package_dir / relative_dir
|
|
if not path.is_dir():
|
|
raise RuntimeError(f"Missing package directory: {relative_dir}")
|
|
|
|
metadata_path = package_dir / "codex-package.json"
|
|
if not metadata_path.is_file():
|
|
raise RuntimeError("Missing package metadata: codex-package.json")
|
|
|
|
with open(metadata_path, encoding="utf-8") as fh:
|
|
metadata = json.load(fh)
|
|
|
|
expected_metadata = {
|
|
"layoutVersion": LAYOUT_VERSION,
|
|
"target": spec.target,
|
|
"variant": variant.name,
|
|
"entrypoint": f"bin/{variant.entrypoint_name(spec)}",
|
|
"resourcesDir": "codex-resources",
|
|
"pathDir": "codex-path",
|
|
}
|
|
for key, expected in expected_metadata.items():
|
|
actual = metadata.get(key)
|
|
if actual != expected:
|
|
raise RuntimeError(
|
|
f"Invalid package metadata field {key!r}: expected {expected!r}, got {actual!r}"
|
|
)
|
|
|
|
required_files = [
|
|
Path("bin") / variant.entrypoint_name(spec),
|
|
Path("codex-path") / spec.rg_name,
|
|
]
|
|
executable_files = list(required_files)
|
|
|
|
if include_zsh:
|
|
zsh_path = Path("codex-resources") / ZSH_RESOURCE_PATH
|
|
required_files.append(zsh_path)
|
|
executable_files.append(zsh_path)
|
|
|
|
if spec.is_linux:
|
|
required_files.append(Path("codex-resources") / "bwrap")
|
|
executable_files.append(Path("codex-resources") / "bwrap")
|
|
|
|
if spec.is_windows:
|
|
required_files.extend(
|
|
[
|
|
Path("codex-resources") / "codex-command-runner.exe",
|
|
Path("codex-resources") / "codex-windows-sandbox-setup.exe",
|
|
]
|
|
)
|
|
|
|
for relative_file in required_files:
|
|
path = package_dir / relative_file
|
|
if not path.is_file():
|
|
raise RuntimeError(f"Missing package file: {relative_file}")
|
|
|
|
if not spec.is_windows:
|
|
for relative_file in executable_files:
|
|
path = package_dir / relative_file
|
|
if not is_executable(path):
|
|
raise RuntimeError(f"Package file is not executable: {relative_file}")
|
|
|
|
|
|
def copy_executable(src: Path, dest: Path, *, is_windows: bool) -> None:
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(src, dest)
|
|
if not is_windows:
|
|
mode = dest.stat().st_mode
|
|
dest.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
|
|
|
|
def write_json(path: Path, value: object) -> None:
|
|
with open(path, "w", encoding="utf-8") as out:
|
|
json.dump(value, out, indent=2)
|
|
out.write("\n")
|
|
|
|
|
|
def is_executable(path: Path) -> bool:
|
|
return bool(path.stat().st_mode & stat.S_IXUSR)
|