Compare commits

...

1 Commits

Author SHA1 Message Date
Michael Bolin
49e38dfc9f scripts: add Makeself Codex dev builder 2026-05-07 23:54:34 -07:00

398
scripts/build_makeself_codex.py Executable file
View File

@@ -0,0 +1,398 @@
#!/usr/bin/env python3
"""Build a self-extracting Codex dev artifact with Makeself."""
from __future__ import annotations
import argparse
import hashlib
import os
import platform
import shutil
import stat
import subprocess
import sys
import tempfile
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
CODEX_RS = REPO_ROOT / "codex-rs"
DEFAULT_PROFILE = "dev-small"
DEFAULT_COMPRESSION = "none"
DEFAULT_CACHE_ROOT = "$HOME/.cache/codex-dev"
COMPRESSION_CHOICES = (
"none",
"gzip",
"pigz",
"zstd",
"bzip2",
"pbzip2",
"bzip3",
"xz",
"lzo",
"lz4",
"compress",
)
COMPLETE_SENTINEL = ".codex-makeself-complete"
RUNNER_NAME = "run-codex"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--profile",
default=DEFAULT_PROFILE,
help=f"Cargo profile to build with. Default: {DEFAULT_PROFILE}.",
)
parser.add_argument(
"--output",
type=Path,
default=None,
help="Path to write the generated Makeself archive. Default: dist/codex-dev/codex-dev.run.",
)
parser.add_argument(
"--compression",
choices=COMPRESSION_CHOICES,
default=DEFAULT_COMPRESSION,
help=(
"Compression mode for the embedded Makeself payload. "
f"Default: {DEFAULT_COMPRESSION}."
),
)
parser.add_argument(
"--cache-root",
default=DEFAULT_CACHE_ROOT,
help=(
"Runtime cache root for extracted builds. Shell variables are preserved "
f"in the generated artifact. Default: {DEFAULT_CACHE_ROOT}."
),
)
parser.add_argument(
"--include-bwrap",
choices=("auto", "always", "never"),
default="auto",
help="Whether to build and bundle bwrap. Default: auto, which includes it on Linux.",
)
parser.add_argument(
"--skip-cargo-build",
action="store_true",
help="Use existing Cargo build outputs instead of invoking cargo build.",
)
parser.add_argument(
"--keep-staging-dir",
action="store_true",
help="Keep the temporary staged payload directory for inspection.",
)
parser.add_argument(
"--makeself",
default="makeself",
help="Path to the makeself executable. Default: makeself from PATH.",
)
parser.add_argument(
"--makeself-header",
type=Path,
default=None,
help="Path to makeself-header.sh. Default: infer from the makeself installation.",
)
return parser.parse_args()
def run_command(cmd: list[str], cwd: Path) -> None:
print("+", " ".join(cmd), flush=True)
subprocess.run(cmd, cwd=cwd, check=True)
def cargo_profile_output_dir(profile_name: str) -> Path:
match profile_name:
case "dev":
profile_dir = "debug"
case "release":
profile_dir = "release"
case _:
profile_dir = profile_name
return CODEX_RS / "target" / profile_dir
def host_executable_name(name: str) -> str:
if os.name == "nt":
return f"{name}.exe"
return name
def should_include_bwrap(mode: str) -> bool:
match mode:
case "always":
return True
case "never":
return False
case "auto":
return platform.system() == "Linux"
case _:
raise ValueError(f"unexpected bwrap mode: {mode}")
def makeself_compression_arg(compression: str) -> str:
if compression == "none":
return "--nocomp"
return f"--{compression}"
def validate_cache_root(cache_root: str) -> None:
normalized = cache_root.rstrip("/")
forbidden_roots = {
"/tmp",
"/private/tmp",
"/var/tmp",
"/var/folders",
"/private/var/folders",
"$TMPDIR",
"${TMPDIR}",
"${TMPDIR:-/tmp}",
"${TMPDIR-/tmp}",
}
system_temp = Path(tempfile.gettempdir()).resolve()
forbidden_roots.add(str(system_temp))
if normalized in forbidden_roots:
raise RuntimeError(f"Refusing to use temp directory as cache root: {cache_root}")
forbidden_prefixes = tuple(f"{root}/" for root in sorted(forbidden_roots))
if normalized.startswith(forbidden_prefixes):
raise RuntimeError(f"Refusing to use temp directory as cache root: {cache_root}")
def build_binaries(profile_name: str, include_bwrap: bool, skip_cargo_build: bool) -> None:
if skip_cargo_build:
return
cmd = ["cargo", "build", "--profile", profile_name, "--bin", "codex"]
if include_bwrap:
cmd.extend(["--bin", "bwrap"])
run_command(cmd, cwd=CODEX_RS)
def require_file(path: Path, description: str) -> None:
if not path.is_file():
raise RuntimeError(f"Missing {description}: {path}")
def stage_payload(build_dir: Path, staging_dir: Path, include_bwrap: bool) -> None:
codex_name = host_executable_name("codex")
codex_src = build_dir / codex_name
require_file(codex_src, "codex binary")
shutil.copy2(codex_src, staging_dir / codex_name)
if include_bwrap:
bwrap_src = build_dir / host_executable_name("bwrap")
require_file(bwrap_src, "bwrap binary")
resources_dir = staging_dir / "codex-resources"
resources_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(bwrap_src, resources_dir / host_executable_name("bwrap"))
runner = staging_dir / RUNNER_NAME
runner.write_text(
"\n".join(
[
"#!/bin/sh",
"set -eu",
f": > {COMPLETE_SENTINEL}",
f'exec ./{codex_name} "$@"',
"",
]
),
encoding="utf-8",
)
runner.chmod(0o755)
def iter_staged_files(staging_dir: Path) -> list[Path]:
return sorted(path for path in staging_dir.rglob("*") if path.is_file())
def hash_staged_tree(staging_dir: Path) -> str:
digest = hashlib.sha256()
for path in iter_staged_files(staging_dir):
relative_path = path.relative_to(staging_dir).as_posix()
mode = stat.S_IMODE(path.stat().st_mode)
digest.update(relative_path.encode("utf-8"))
digest.update(b"\0")
digest.update(f"{mode:o}".encode("ascii"))
digest.update(b"\0")
with path.open("rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
digest.update(chunk)
digest.update(b"\0")
return digest.hexdigest()
def infer_makeself_header(makeself: str) -> Path:
makeself_path = shutil.which(makeself)
if makeself_path is None:
candidate = Path(makeself)
if candidate.is_file():
makeself_path = str(candidate)
else:
raise RuntimeError(f"Unable to find makeself executable: {makeself}")
resolved = Path(makeself_path).resolve()
candidates = [
resolved.parent / "makeself-header.sh",
resolved.parent.parent / "libexec" / "makeself-header.sh",
resolved.parent.parent / "share" / "makeself" / "makeself-header.sh",
Path("/usr/libexec/makeself-header.sh"),
Path("/usr/share/makeself/makeself-header.sh"),
Path("/usr/lib/makeself/makeself-header.sh"),
]
for candidate in candidates:
if candidate.is_file():
return candidate
raise RuntimeError(
"Unable to infer makeself-header.sh. Pass --makeself-header with its path."
)
def write_cached_makeself_header(source_header: Path, output_header: Path) -> None:
header = source_header.read_text(encoding="utf-8")
marker = 'if test x"\\$targetdir" = x.; then'
if marker not in header:
raise RuntimeError(f"Unable to patch Makeself header; marker not found in {source_header}")
header = pass_codex_options_through(header, source_header)
cache_fast_path = f"""
# Codex dev artifacts use content-addressed --target directories. On cache hits,
# run the existing extraction instead of unpacking the payload again.
if test x"\\$keep" = xy -a x"\\$script" != x -a -f "\\$targetdir/{COMPLETE_SENTINEL}"; then
cd "\\$targetdir" || {{
echo "Cannot enter cached target directory \\$targetdir" >&2
exit 1
}}
if test x"\\$quiet" = xn; then
echo "Using cached extraction in \\$targetdir"
fi
res=0
if test x"\\$verbose" = xy; then
MS_Printf "OK to execute: \\$script \\$scriptargs \\$* ? [Y/n] "
read yn
if test x"\\$yn" = x -o x"\\$yn" = xy -o x"\\$yn" = xY; then
eval "\\"\\$script\\" \\$scriptargs "\\\\\\$@""; res=\\$?
fi
else
eval "\\"\\$script\\" \\$scriptargs "\\\\\\$@""; res=\\$?
fi
exit \\$res
fi
"""
output_header.write_text(header.replace(marker, cache_fast_path + marker), encoding="utf-8")
def pass_codex_options_through(header: str, source_header: Path) -> str:
header = header.replace("-h | --help)", "--makeself-help)")
header = header.replace(
"\\$0 --help Print this message",
"\\$0 --makeself-help Print this message",
)
unrecognized_flag_block = """ -*)
\techo Unrecognized flag : "\\$1" >&2
\tMS_Help
\texit 1
\t;;"""
if unrecognized_flag_block not in header:
raise RuntimeError(
f"Unable to patch Makeself option parser; marker not found in {source_header}"
)
return header.replace(
unrecognized_flag_block,
""" -*)
\tbreak
\t;;""",
)
def build_archive(
makeself: str,
compression: str,
header: Path,
staging_dir: Path,
output_path: Path,
target_dir: str,
tree_hash: str,
) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
label = f"Codex dev build {tree_hash[:12]}"
cmd = [
makeself,
makeself_compression_arg(compression),
"--sha256",
"--packaging-date",
f"content-sha256:{tree_hash}",
"--header",
str(header),
"--target",
target_dir,
str(staging_dir),
str(output_path),
label,
f"./{RUNNER_NAME}",
]
run_command(cmd, cwd=REPO_ROOT)
def default_output_path() -> Path:
return REPO_ROOT / "dist" / "codex-dev" / "codex-dev.run"
def main() -> int:
args = parse_args()
validate_cache_root(args.cache_root)
include_bwrap = should_include_bwrap(args.include_bwrap)
build_binaries(args.profile, include_bwrap, args.skip_cargo_build)
build_dir = cargo_profile_output_dir(args.profile)
output_path = args.output or default_output_path()
makeself_header = args.makeself_header or infer_makeself_header(args.makeself)
require_file(makeself_header, "makeself header")
with tempfile.TemporaryDirectory(prefix="codex-makeself-") as temp_root_name:
temp_root = Path(temp_root_name)
staging_dir = temp_root / "payload"
staging_dir.mkdir()
patched_header = temp_root / "makeself-header-codex-cache.sh"
stage_payload(build_dir, staging_dir, include_bwrap)
tree_hash = hash_staged_tree(staging_dir)
target_dir = f"{args.cache_root.rstrip('/')}/sha256-{tree_hash}"
write_cached_makeself_header(makeself_header, patched_header)
build_archive(
args.makeself,
args.compression,
patched_header,
staging_dir,
output_path,
target_dir,
tree_hash,
)
if args.keep_staging_dir:
kept_staging_dir = output_path.parent / f"payload-sha256-{tree_hash[:12]}"
if kept_staging_dir.exists():
shutil.rmtree(kept_staging_dir)
shutil.copytree(staging_dir, kept_staging_dir, copy_function=shutil.copy2)
print(f"Kept staged payload at {kept_staging_dir}")
print(f"Wrote {output_path}")
print(f"Payload sha256: {tree_hash}")
print(f"Runtime cache target: {target_dir}")
if not include_bwrap:
print("bwrap was not bundled; pass --include-bwrap=always to require it.")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except RuntimeError as exc:
print(f"error: {exc}", file=sys.stderr)
sys.exit(1)