From 22781d40019779abc158723f2387cc6bce22a7ce Mon Sep 17 00:00:00 2001 From: Channing Conger Date: Thu, 9 Jul 2026 13:11:28 -0700 Subject: [PATCH] code-mode: fix installation on darwin (#31876) Installer needs to symlink code-mode-host next to codex on install. --- scripts/install/install.sh | 14 ++ scripts/install/test_install_sh.py | 248 ++++++++++++++++++++++------- 2 files changed, 201 insertions(+), 61 deletions(-) diff --git a/scripts/install/install.sh b/scripts/install/install.sh index adc79a8a27..0c52542a7a 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -7,6 +7,7 @@ NON_INTERACTIVE="${CODEX_NON_INTERACTIVE:-false}" BIN_DIR="${CODEX_INSTALL_DIR:-$HOME/.local/bin}" BIN_PATH="$BIN_DIR/codex" +CODE_MODE_HOST_BIN_PATH="$BIN_DIR/codex-code-mode-host" CODEX_HOME_DIR="${CODEX_HOME:-$HOME/.codex}" STANDALONE_ROOT="$CODEX_HOME_DIR/packages/standalone" RELEASES_DIR="$STANDALONE_ROOT/releases" @@ -869,10 +870,23 @@ update_visible_command() { codex_relative_path="$(release_codex_relative_path "$release_dir")" replace_path_with_symlink "$BIN_PATH" "$CURRENT_LINK/$codex_relative_path" "$tmp_link" + + if [ "$os" = "darwin" ] && [ -x "$release_dir/bin/codex-code-mode-host" ]; then + replace_path_with_symlink \ + "$CODE_MODE_HOST_BIN_PATH" \ + "$CURRENT_LINK/bin/codex-code-mode-host" \ + "$tmp_link" + elif [ "$(readlink "$CODE_MODE_HOST_BIN_PATH" 2>/dev/null || true)" = \ + "$CURRENT_LINK/bin/codex-code-mode-host" ]; then + rm -f "$CODE_MODE_HOST_BIN_PATH" + fi } verify_visible_command() { "$BIN_PATH" --version >/dev/null + if [ "$os" = "darwin" ] && [ "$install_layout" = "package" ]; then + [ -x "$CODE_MODE_HOST_BIN_PATH" ] + fi } parse_args "$@" diff --git a/scripts/install/test_install_sh.py b/scripts/install/test_install_sh.py index c77a0b0dac..a805fbf23d 100644 --- a/scripts/install/test_install_sh.py +++ b/scripts/install/test_install_sh.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 +import hashlib import json import os from pathlib import Path import subprocess +import tarfile import tempfile import textwrap import unittest @@ -86,6 +88,32 @@ class InstallShTest(unittest.TestCase): self.assertIn("/codex-npm-", requests[1]) self.assertNotIn("codex-package_SHA256SUMS", requests[1]) + def test_macos_install_exposes_code_mode_host_beside_codex(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + archive_path, checksum_path, metadata_json = create_package_release(root) + + result, _requests = run_installer_in( + root, + VERSION, + metadata_json=metadata_json, + archive_path=archive_path, + checksum_path=checksum_path, + force_macos=True, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + install_bin = root / "install-bin" + current = root / "codex-home" / "packages" / "standalone" / "current" + codex_path = install_bin / "codex" + host_path = install_bin / "codex-code-mode-host" + self.assertEqual(os.readlink(codex_path), str(current / "bin" / "codex")) + self.assertEqual( + os.readlink(host_path), + str(current / "bin" / "codex-code-mode-host"), + ) + self.assertTrue(os.access(host_path, os.X_OK)) + def run_installer( release: str, @@ -94,71 +122,169 @@ def run_installer( metadata_json: str | None = None, ) -> tuple[subprocess.CompletedProcess[str], list[str]]: with tempfile.TemporaryDirectory() as temp_dir: - root = Path(temp_dir) - bin_dir = root / "bin" - bin_dir.mkdir() - request_log = root / "requests.log" - fake_curl = bin_dir / "curl" - fake_curl.write_text( - textwrap.dedent( - """\ - #!/bin/sh - url="" - for arg in "$@"; do - case "$arg" in - https://*) url="$arg" ;; - esac - done - printf '%s\n' "$url" >>"$CODEX_TEST_REQUEST_LOG" + return run_installer_in( + Path(temp_dir), + release, + metadata_failure=metadata_failure, + metadata_json=metadata_json, + ) - case "$url" in - https://api.github.com/*) - if [ "$CODEX_TEST_METADATA_FAILURE" = "1" ]; then - echo "curl: (22) The requested URL returned error: 403" >&2 - exit 22 - fi - printf '%s\n' "$CODEX_TEST_METADATA_JSON" - ;; - *) - exit 22 - ;; - esac - """ - ), + +def run_installer_in( + root: Path, + release: str, + *, + metadata_failure: bool = False, + metadata_json: str | None = None, + archive_path: Path | None = None, + checksum_path: Path | None = None, + force_macos: bool = False, +) -> tuple[subprocess.CompletedProcess[str], list[str]]: + bin_dir = root / "bin" + bin_dir.mkdir() + request_log = root / "requests.log" + fake_curl = bin_dir / "curl" + fake_curl.write_text( + textwrap.dedent( + """\ + #!/bin/sh + url="" + output="" + previous="" + for arg in "$@"; do + case "$arg" in + https://*) url="$arg" ;; + esac + if [ "$previous" = "-o" ]; then + output="$arg" + fi + previous="$arg" + done + printf '%s\n' "$url" >>"$CODEX_TEST_REQUEST_LOG" + + case "$url" in + https://api.github.com/*) + if [ "$CODEX_TEST_METADATA_FAILURE" = "1" ]; then + echo "curl: (22) The requested URL returned error: 403" >&2 + exit 22 + fi + printf '%s\n' "$CODEX_TEST_METADATA_JSON" + ;; + */codex-package_SHA256SUMS) + if [ -n "$CODEX_TEST_CHECKSUM_PATH" ]; then + cp "$CODEX_TEST_CHECKSUM_PATH" "$output" + else + exit 22 + fi + ;; + */codex-package-*.tar.gz) + if [ -n "$CODEX_TEST_ARCHIVE_PATH" ]; then + cp "$CODEX_TEST_ARCHIVE_PATH" "$output" + else + exit 22 + fi + ;; + *) + exit 22 + ;; + esac + """ + ), + encoding="utf-8", + ) + fake_curl.chmod(0o755) + if force_macos: + fake_uname = bin_dir / "uname" + fake_uname.write_text( + "#!/bin/sh\n" + 'case "$1" in\n' + " -s) printf 'Darwin\\n' ;;\n" + " -m) printf 'arm64\\n' ;;\n" + "esac\n", encoding="utf-8", ) - fake_curl.chmod(0o755) + fake_uname.chmod(0o755) - env = os.environ.copy() - env.update( - { - "CODEX_HOME": str(root / "codex-home"), - "CODEX_INSTALL_DIR": str(root / "install-bin"), - "CODEX_NON_INTERACTIVE": "1", - "CODEX_RELEASE": release, - "CODEX_TEST_METADATA_FAILURE": "1" if metadata_failure else "0", - "CODEX_TEST_METADATA_JSON": ( - metadata_json if metadata_json is not None else release_metadata() - ), - "CODEX_TEST_REQUEST_LOG": str(request_log), - "HOME": str(root / "home"), - "PATH": f"{bin_dir}:/usr/bin:/bin", - "SHELL": "/bin/sh", - } - ) - result = subprocess.run( - ["/bin/sh", str(INSTALL_SCRIPT)], - capture_output=True, - check=False, - env=env, - text=True, - ) - requests = ( - request_log.read_text(encoding="utf-8").splitlines() - if request_log.exists() - else [] - ) - return result, requests + home = root / "home" + home.mkdir() + env = os.environ.copy() + env.update( + { + "CODEX_HOME": str(root / "codex-home"), + "CODEX_INSTALL_DIR": str(root / "install-bin"), + "CODEX_NON_INTERACTIVE": "1", + "CODEX_RELEASE": release, + "CODEX_TEST_ARCHIVE_PATH": str(archive_path or ""), + "CODEX_TEST_CHECKSUM_PATH": str(checksum_path or ""), + "CODEX_TEST_METADATA_FAILURE": "1" if metadata_failure else "0", + "CODEX_TEST_METADATA_JSON": ( + metadata_json if metadata_json is not None else release_metadata() + ), + "CODEX_TEST_REQUEST_LOG": str(request_log), + "HOME": str(home), + "PATH": f"{bin_dir}:/usr/bin:/bin", + "SHELL": "/bin/sh", + } + ) + result = subprocess.run( + ["/bin/sh", str(INSTALL_SCRIPT)], + capture_output=True, + check=False, + env=env, + text=True, + ) + requests = ( + request_log.read_text(encoding="utf-8").splitlines() + if request_log.exists() + else [] + ) + return result, requests + + +def create_package_release(root: Path) -> tuple[Path, Path, str]: + package_dir = root / "package" + (package_dir / "bin").mkdir(parents=True) + (package_dir / "codex-path").mkdir() + (package_dir / "codex-package.json").write_text("{}\n", encoding="utf-8") + write_executable( + package_dir / "bin" / "codex", + f"#!/bin/sh\nprintf 'codex-cli {VERSION}\\n'\n", + ) + write_executable( + package_dir / "bin" / "codex-code-mode-host", + "#!/bin/sh\nexit 0\n", + ) + write_executable(package_dir / "codex-path" / "rg", "#!/bin/sh\nexit 0\n") + + asset = "codex-package-aarch64-apple-darwin.tar.gz" + archive_path = root / asset + with tarfile.open(archive_path, "w:gz") as archive: + for path in package_dir.iterdir(): + archive.add(path, arcname=path.name) + + archive_digest = hashlib.sha256(archive_path.read_bytes()).hexdigest() + checksum_path = root / "codex-package_SHA256SUMS" + checksum_path.write_text(f"{archive_digest} {asset}\n", encoding="utf-8") + checksum_digest = hashlib.sha256(checksum_path.read_bytes()).hexdigest() + metadata_json = json.dumps( + { + "assets": [ + {"name": asset, "digest": f"sha256:{archive_digest}"}, + { + "name": "codex-package_SHA256SUMS", + "digest": f"sha256:{checksum_digest}", + }, + ], + "tag_name": f"rust-v{VERSION}", + }, + indent=2, + ) + return archive_path, checksum_path, metadata_json + + +def write_executable(path: Path, contents: str) -> None: + path.write_text(contents, encoding="utf-8") + path.chmod(0o755) def release_metadata(*, compact: bool = False, reorder: bool = False) -> str: