From e9e6cf63492da108a3cabaf3cc7b23d0640ef4cf Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 8 Oct 2026 16:59:17 +0000 Subject: [PATCH] Strip symbols from source-built release package binaries (#52178) ## What changed On Linux and macOS, strip the packaged copies of source-built release entrypoint and `codex-code-mode-host` binaries while retaining symbols in the original build outputs. Add `--strip auto|all|none` and `--strip-tool`. The default `auto` preserves prebuilt inputs and other build profiles; `all` also strips those package copies, and `none` disables stripping. Select target-compatible strip tools and fail on missing tools or strip errors. Leave third-party resources unchanged. ## Testing Add a regression test that compiles an executable with debug information, verifies stripped package binaries are smaller and still runnable, and checks that original inputs, preserved package copies, and third-party resources remain byte-for-byte unchanged. GitOrigin-RevId: dc363c04690aa9e772690417a52a8eff31650278 --- scripts/codex_package/README.md | 18 +++++++ scripts/codex_package/cli.py | 26 ++++++++++ scripts/codex_package/symbols.py | 54 ++++++++++++++++++++ scripts/codex_package/test_layout.py | 74 ++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 scripts/codex_package/symbols.py diff --git a/scripts/codex_package/README.md b/scripts/codex_package/README.md index 972659d4e6..88937649b5 100644 --- a/scripts/codex_package/README.md +++ b/scripts/codex_package/README.md @@ -64,6 +64,24 @@ binary instead of rebuilding it. Release jobs should likewise pass `--code-mode-host-bin` so the package contains the signed host executable beside the signed entrypoint. +On Linux and macOS, the builder strips symbols from the **package copies** of +source-built release-profile entrypoint and code-mode host binaries. Cargo outputs +retain their symbols for debugging. The default `--strip auto` also preserves +development/profiling builds and prebuilt inputs +byte-for-byte, including signatures. Use `--strip all` to also strip prebuilt +entrypoint/host copies, or `--strip none` to keep symbols in every package copy. +Strip before production signing; `--strip all` is not appropriate for inputs +whose release signatures must be preserved. Windows MSVC symbols are separate +PDB files and are not included in the package. + +macOS uses `xcrun strip -S -x`, matching the release pipeline and preserving +executable ad-hoc signatures. Linux uses `llvm-strip`, a target-prefixed GNU +strip, or native GNU strip when the host architecture matches. For cross builds, +install `llvm-strip` or pass `--strip-tool /path/to/target-strip`. Missing tools +or strip failures fail the build; use `--strip none` to deliberately opt out. +Third-party resources are copied unchanged. In particular, never strip `bwrap` +after its integrity digest has been embedded in Codex. + Release jobs that already built package resource binaries should also pass the corresponding resource flags: `--bwrap-bin` for Linux packages, and `--codex-command-runner-bin` plus `--codex-windows-sandbox-setup-bin` for diff --git a/scripts/codex_package/cli.py b/scripts/codex_package/cli.py index de081fe148..b08b4a93ad 100644 --- a/scripts/codex_package/cli.py +++ b/scripts/codex_package/cli.py @@ -11,6 +11,7 @@ from .layout import build_package_dir from .layout import prepare_package_dir from .layout import validate_package_dir from .ripgrep import resolve_rg_bin +from .symbols import strip_binary from .targets import PACKAGE_VARIANTS from .targets import TARGET_SPECS from .targets import PackageInputs @@ -109,6 +110,21 @@ def create_parser() -> argparse.ArgumentParser: "Cargo profile for source-built package artifacts. Use release for release packages." ), ) + parser.add_argument( + "--strip", + choices=("auto", "all", "none"), + default="auto", + help=( + "Strip Unix first-party package binaries: auto strips source-built " + "release-profile inputs, all also strips other profiles and prebuilt " + "inputs, none preserves symbols. " + "Original inputs and third-party resources are never modified." + ), + ) + parser.add_argument( + "--strip-tool", + help="Target-compatible strip executable override, for example llvm-strip.", + ) parser.add_argument( "--entrypoint-bin", type=Path, @@ -236,6 +252,16 @@ def assemble_package(args: argparse.Namespace) -> Path: ) prepare_package_dir(package_dir, force=args.force) build_package_dir(package_dir, args.package_version, variant, spec, inputs) + for filename, prebuilt in ( + (variant.entrypoint_name(spec), args.entrypoint_bin), + (f"codex-code-mode-host{spec.exe_suffix}", args.code_mode_host_bin), + ): + if args.strip == "all" or ( + args.strip == "auto" + and args.cargo_profile == "release" + and prebuilt is None + ): + strip_binary(package_dir / "bin" / filename, spec, tool=args.strip_tool) validate_package_dir( package_dir, variant, spec, include_zsh=inputs.zsh_bin is not None ) diff --git a/scripts/codex_package/symbols.py b/scripts/codex_package/symbols.py new file mode 100644 index 0000000000..ec9f87edd8 --- /dev/null +++ b/scripts/codex_package/symbols.py @@ -0,0 +1,54 @@ +"""Strip packaged Unix executables without modifying their build outputs. + +Call only on package copies of first-party binaries. Prebuilt release inputs +may already be signed, and resources such as bwrap have integrity hashes that +must survive staging. Windows MSVC symbols live in separate PDB files. +""" + +import platform +import shutil +import subprocess +from pathlib import Path + +from .targets import TargetSpec +from .targets import normalize_machine + + +def strip_binary(binary: Path, spec: TargetSpec, *, tool: str | None = None) -> None: + if spec.is_windows: + return + + if spec.is_linux: + # A host GNU strip may not understand a cross-compiled ELF architecture. + # LLVM supports both packaged Linux architectures from any host. + machine = spec.target.split("-", 1)[0] + command = ( + tool + or shutil.which("llvm-strip") + or shutil.which(f"{machine}-linux-gnu-strip") + ) + if ( + command is None + and platform.system() == "Linux" + and normalize_machine(platform.machine()) == machine + ): + command = shutil.which("strip") + if command is None: + raise RuntimeError( + f"No strip tool for {spec.target}; install llvm-strip or pass " + "--strip-tool /path/to/target-strip (or --strip none to keep symbols)" + ) + args = [command, "--strip-debug", "--strip-unneeded"] + else: + if tool is None and platform.system() != "Darwin": + raise RuntimeError( + "Stripping a macOS binary requires a Mach-O strip tool; pass " + "--strip-tool /path/to/strip (or --strip none to keep symbols)" + ) + # Match the release pipeline. Apple's strip also updates the ad-hoc + # code signature on Cargo-built ARM64 executables so they remain runnable. + args = [tool] if tool is not None else ["xcrun", "strip"] + args.extend(("-S", "-x")) + + print(f"Stripping package executable {binary}") + subprocess.run([*args, str(binary)], check=True) diff --git a/scripts/codex_package/test_layout.py b/scripts/codex_package/test_layout.py index b2df9601c1..6355d29bd9 100644 --- a/scripts/codex_package/test_layout.py +++ b/scripts/codex_package/test_layout.py @@ -3,6 +3,8 @@ from pathlib import Path import hashlib import json +import shutil +import subprocess import sys import tempfile import unittest @@ -15,9 +17,81 @@ from codex_package.layout import validate_package_dir from codex_package.targets import PACKAGE_VARIANTS from codex_package.targets import PackageInputs from codex_package.targets import TARGET_SPECS +from codex_package.targets import default_target class PackageLayoutTest(unittest.TestCase): + @unittest.skipIf(sys.platform == "win32", "MSVC symbols are separate PDB files") + def test_strip_policy_preserves_inputs_and_runs_packaged_executables(self) -> None: + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + source = root / "input-executable" + # System executables may already be stripped. Compile with debug + # information so a no-op strip cannot pass this regression test. + subprocess.run( + ["cc", "-g", "-O0", "-x", "c", "-", "-o", str(source)], + input=r"""#include +int main(void) { + fputs("package-ok", stdout); + return 0; +} +""", + text=True, + check=True, + capture_output=True, + ) + original = source.read_bytes() + target = default_target() + for mode in ("auto", "all", "none"): + with self.subTest(mode=mode): + package = root / mode + command = [ + sys.executable, + str( + Path(__file__).resolve().parents[1] + / "build_codex_package.py" + ), + "--target", + target, + "--cargo-profile", + "release", + "--package-dir", + str(package), + "--strip", + mode, + ] + flags = [ + "--entrypoint-bin", + "--code-mode-host-bin", + "--rg-bin", + "--zsh-bin", + ] + if "linux" in target: + flags.append("--bwrap-bin") + for flag in flags: + command.extend((flag, str(source))) + if mode != "all": + # Preserved prebuilt inputs must not even need a strip tool. + command.extend(("--strip-tool", str(root / "absent-strip"))) + subprocess.run(command, check=True, capture_output=True) + for name in ("codex", "codex-code-mode-host"): + executable = package / "bin" / name + self.assertEqual( + subprocess.check_output([str(executable)]), + b"package-ok", + ) + if mode != "all": + self.assertEqual(executable.read_bytes(), original) + else: + self.assertLess(executable.stat().st_size, len(original)) + self.assertEqual(source.read_bytes(), original) + for resource in ("codex-path/rg", "codex-resources/zsh/bin/zsh"): + self.assertEqual((package / resource).read_bytes(), original) + if "linux" in target: + self.assertEqual( + (package / "codex-resources/bwrap").read_bytes(), original + ) + def test_winget_preserves_signed_files_and_voice_hashes(self) -> None: for target in ("x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"): with self.subTest(target=target), tempfile.TemporaryDirectory() as temp: