diff --git a/.github/scripts/build-codex-package-archive.sh b/.github/scripts/build-codex-package-archive.sh index 4592e78ce5..a0ed4a5f34 100644 --- a/.github/scripts/build-codex-package-archive.sh +++ b/.github/scripts/build-codex-package-archive.sh @@ -10,6 +10,8 @@ Usage: build-codex-package-archive.sh \ --archive-dir \ [--bwrap-bin ] \ [--code-mode-host-bin ] \ + [--rg-bin ] \ + [--zsh-bin ] \ [--zsh-manifest ] \ [--codex-command-runner-bin ] \ [--codex-windows-sandbox-setup-bin ] \ @@ -56,6 +58,14 @@ while [[ $# -gt 0 ]]; do code_mode_host_bin_provided="true" shift 2 ;; + --rg-bin) + resource_args+=(--rg-bin "${2:?--rg-bin requires a value}") + shift 2 + ;; + --zsh-bin) + resource_args+=(--zsh-bin "${2:?--zsh-bin requires a value}") + shift 2 + ;; --zsh-manifest) resource_args+=(--zsh-manifest "${2:?--zsh-manifest requires a value}") shift 2 diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index a6e056030e..a1d27caeba 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -597,6 +597,63 @@ jobs: --report-dir "${report_dir}/${binary}" done + - name: Fetch, sign, and notarize pinned macOS helpers + if: ${{ matrix.bundle == 'primary' }} + shell: bash + env: + TARGET: ${{ matrix.target }} + APPLE_NOTARIZATION_KEY_P8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }} + APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} + run: | + set -euo pipefail + + signed_root="${GITHUB_WORKSPACE}/signed-resources/${TARGET}" + zsh_manifest="${RUNNER_TEMP}/codex-zsh-${TARGET}" + mkdir -p "$signed_root" + curl -fsSL \ + "https://github.com/${GITHUB_REPOSITORY}/releases/download/${CODEX_ZSH_RELEASE_TAG}/codex-zsh" \ + -o "$zsh_manifest" + + PYTHONPATH="${GITHUB_WORKSPACE}/scripts" python3 - "$TARGET" "$signed_root" "$zsh_manifest" <<'PY' + import shutil + import sys + from pathlib import Path + + from codex_package.ripgrep import fetch_rg + from codex_package.targets import TARGET_SPECS + from codex_package.zsh import resolve_zsh_bin + + spec = TARGET_SPECS[sys.argv[1]] + signed_root = Path(sys.argv[2]) + zsh_bin = resolve_zsh_bin(spec, Path(sys.argv[3])) + if zsh_bin is None: + raise RuntimeError(f"Pinned zsh release is missing {spec.target}") + shutil.copy2(fetch_rg(spec), signed_root / "rg") + shutil.copy2(zsh_bin, signed_root / "zsh") + PY + + for resource in rg zsh; do + binary="${signed_root}/${resource}" + report_dir="${GITHUB_WORKSPACE}/macos-binary-signing-verification/${TARGET}/${resource}" + mkdir -p "$report_dir" + chmod 0755 "$binary" + .github/scripts/macos-signing/sign_macos_code.sh \ + --target "$binary" \ + --identity unused \ + --deep false \ + --identifier "com.openai.codex.${resource}" \ + --options runtime \ + --timestamp true + + rcodesign print-signature-info "$binary" \ + >"${report_dir}/signature-info.yaml" + + .github/scripts/macos-signing/notarize_macos_binary_with_rcodesign.sh \ + --binary "$binary" \ + --report-dir "$report_dir" + done + - name: Upload signed macOS binaries uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: @@ -604,6 +661,14 @@ jobs: path: signed-macos/${{ matrix.target }}/* if-no-files-found: error + - name: Upload signed macOS helpers + if: ${{ matrix.bundle == 'primary' }} + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: ${{ matrix.target }}-signed-resources + path: signed-resources/${{ matrix.target }}/* + if-no-files-found: error + - name: Upload binary signing verification if: ${{ always() }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 @@ -659,6 +724,12 @@ jobs: name: ${{ matrix.artifact_name }}-signed-binaries path: codex-rs/target/${{ matrix.target }}/release + - name: Download signed macOS helpers + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ matrix.target }}-signed-resources + path: codex-rs/signed-resources/${{ matrix.target }} + - name: Verify signed macOS binaries shell: bash run: | @@ -669,6 +740,12 @@ jobs: codesign --verify --strict --verbose=2 "$binary_path" done + for resource in rg zsh; do + resource_path="signed-resources/${{ matrix.target }}/${resource}" + chmod 0755 "$resource_path" + codesign --verify --strict --verbose=2 "$resource_path" + done + - name: Build unsigned macOS DMG if: ${{ matrix.build_dmg == 'true' }} shell: bash @@ -730,14 +807,6 @@ jobs: cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" done - - name: Download packaged zsh manifest - shell: bash - run: | - set -euo pipefail - curl -fsSL \ - "https://github.com/${GITHUB_REPOSITORY}/releases/download/${CODEX_ZSH_RELEASE_TAG}/codex-zsh" \ - -o "${RUNNER_TEMP}/codex-zsh" - - name: Build Codex package archive shell: bash env: @@ -750,7 +819,8 @@ jobs: --bundle "$BUNDLE" \ --entrypoint-dir "target/${TARGET}/release" \ --archive-dir "dist/${TARGET}" \ - --zsh-manifest "${RUNNER_TEMP}/codex-zsh" + --rg-bin "signed-resources/${TARGET}/rg" \ + --zsh-bin "signed-resources/${TARGET}/zsh" - name: Build Python runtime wheel if: ${{ matrix.bundle == 'primary' }} @@ -1056,6 +1126,23 @@ jobs: verify_signed_binary "${package_dir}/bin/${package_entrypoint}" "$package_entrypoint" verify_signed_binary "${package_dir}/bin/codex-code-mode-host" "codex-code-mode-host" + for resource in \ + "${package_dir}/codex-path/rg" \ + "${package_dir}/codex-resources/zsh/bin/zsh" + do + chmod 0755 "$resource" + lipo "$resource" -verify_arch "$expected_arch" + codesign --verify --strict --verbose=2 "$resource" + entitlements="$(mktemp)" + codesign -d --entitlements :- "$resource" >"$entitlements" + if [[ -s "$entitlements" ]]; then + echo "Bundled helper $resource must not have code-signing entitlements." >&2 + plutil -p "$entitlements" >&2 + exit 1 + fi + rm -f "$entitlements" + done + if [[ "${{ matrix.verify_dmg }}" != "true" ]]; then exit 0 fi diff --git a/scripts/codex_package/README.md b/scripts/codex_package/README.md index 7850bbd354..cf38cfa38f 100644 --- a/scripts/codex_package/README.md +++ b/scripts/codex_package/README.md @@ -78,6 +78,6 @@ The patched zsh fork used by `shell_zsh_fork` is fetched from the DotSlash manifest at `scripts/codex_package/codex-zsh` when the selected target has a matching prebuilt artifact. Downloaded archives are cached under `$TMPDIR/codex-package/-zsh` and installed at -`codex-resources/zsh/bin/zsh`. Pass `--zsh-manifest` to use a different -DotSlash manifest, such as the manifest published with a standalone zsh -artifact release. +`codex-resources/zsh/bin/zsh`. Pass `--zsh-bin` to package a prebuilt, signed +executable, or `--zsh-manifest` to use a different DotSlash manifest, such as +the manifest published with a standalone zsh artifact release. diff --git a/scripts/codex_package/cli.py b/scripts/codex_package/cli.py index f461d0df43..595158d2dc 100644 --- a/scripts/codex_package/cli.py +++ b/scripts/codex_package/cli.py @@ -98,7 +98,8 @@ def parse_args() -> argparse.Namespace: "targets, bwrap is built with Cargo." ), ) - parser.add_argument( + zsh_source = parser.add_mutually_exclusive_group() + zsh_source.add_argument( "--zsh-manifest", type=Path, help=( @@ -106,6 +107,11 @@ def parse_args() -> argparse.Namespace: "scripts/codex_package/codex-zsh." ), ) + zsh_source.add_argument( + "--zsh-bin", + type=Path, + help="Optional prebuilt zsh executable instead of fetching from a manifest.", + ) parser.add_argument( "--codex-command-runner-bin", type=Path, @@ -182,7 +188,7 @@ def main() -> int: entrypoint_bin=source_outputs.entrypoint_bin, code_mode_host_bin=source_outputs.code_mode_host_bin, rg_bin=resolve_rg_bin(spec, args.rg_bin), - zsh_bin=resolve_zsh_bin(spec, args.zsh_manifest), + zsh_bin=resolve_zsh_bin(spec, args.zsh_manifest, zsh_bin=args.zsh_bin), bwrap_bin=source_outputs.bwrap_bin, codex_command_runner_bin=source_outputs.codex_command_runner_bin, codex_windows_sandbox_setup_bin=source_outputs.codex_windows_sandbox_setup_bin, diff --git a/scripts/codex_package/test_layout.py b/scripts/codex_package/test_layout.py index 157bce5e9c..b0810cac99 100644 --- a/scripts/codex_package/test_layout.py +++ b/scripts/codex_package/test_layout.py @@ -15,6 +15,56 @@ from codex_package.targets import TARGET_SPECS class PackageLayoutTest(unittest.TestCase): + def test_macos_package_preserves_prebuilt_resource_binaries(self) -> None: + for variant_name in ("codex", "codex-app-server"): + for target in ("aarch64-apple-darwin", "x86_64-apple-darwin"): + with self.subTest(variant=variant_name, target=target): + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + package_dir = root / "package" + package_dir.mkdir() + rg_bin = touch_executable(root / "signed-rg") + zsh_bin = touch_executable(root / "signed-zsh") + rg_bin.write_bytes(b"signed ripgrep binary") + zsh_bin.write_bytes(b"signed zsh binary") + variant = PACKAGE_VARIANTS[variant_name] + spec = TARGET_SPECS[target] + inputs = PackageInputs( + entrypoint_bin=touch_executable( + root / variant.executable_stem + ), + code_mode_host_bin=touch_executable( + root / "codex-code-mode-host" + ), + rg_bin=rg_bin, + zsh_bin=zsh_bin, + bwrap_bin=None, + codex_command_runner_bin=None, + codex_windows_sandbox_setup_bin=None, + ) + + build_package_dir(package_dir, "1.2.3", variant, spec, inputs) + validate_package_dir( + package_dir, variant, spec, include_zsh=True + ) + + self.assertEqual( + { + "rg": (package_dir / "codex-path" / "rg").read_bytes(), + "zsh": ( + package_dir + / "codex-resources" + / "zsh" + / "bin" + / "zsh" + ).read_bytes(), + }, + { + "rg": b"signed ripgrep binary", + "zsh": b"signed zsh binary", + }, + ) + def test_app_server_package_places_code_mode_host_beside_entrypoint(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) diff --git a/scripts/codex_package/test_zsh.py b/scripts/codex_package/test_zsh.py index b4f536cfc2..86d3c9fea8 100644 --- a/scripts/codex_package/test_zsh.py +++ b/scripts/codex_package/test_zsh.py @@ -16,6 +16,20 @@ from codex_package.zsh import resolve_zsh_bin class ResolveZshBinTest(unittest.TestCase): + def test_uses_prebuilt_executable_override(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + signed_zsh = Path(temp_dir) / "signed-zsh" + signed_zsh.write_bytes(b"signed zsh binary") + signed_zsh.chmod(0o755) + + with patch("codex_package.zsh.fetch_dotslash_executable") as fetch: + zsh_bin = resolve_zsh_bin( + TARGET_SPECS["aarch64-apple-darwin"], zsh_bin=signed_zsh + ) + + self.assertEqual(zsh_bin, signed_zsh.resolve()) + fetch.assert_not_called() + def test_uses_manifest_override(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) diff --git a/scripts/codex_package/zsh.py b/scripts/codex_package/zsh.py index 4ec2db502c..35c62a3854 100644 --- a/scripts/codex_package/zsh.py +++ b/scripts/codex_package/zsh.py @@ -5,6 +5,7 @@ from pathlib import Path from .dotslash import fetch_dotslash_executable from .targets import REPO_ROOT from .targets import TargetSpec +from .targets import resolve_input_path ZSH_MANIFEST = REPO_ROOT / "scripts" / "codex_package" / "codex-zsh" @@ -14,7 +15,12 @@ ZSH_RESOURCE_PATH = Path("zsh") / "bin" / "zsh" def resolve_zsh_bin( spec: TargetSpec, manifest_path: Path | None = None, + *, + zsh_bin: Path | None = None, ) -> Path | None: + if zsh_bin is not None: + return resolve_input_path(zsh_bin, "zsh executable", "--zsh-bin") + return fetch_dotslash_executable( spec, manifest_path=manifest_path or ZSH_MANIFEST,