Created
October 13, 2026 12:38
-
-
Save lydell/5095bf0043752198a980a736bbecec99 to your computer and use it in GitHub Desktop.
Reproduce official Elm macOS ARM binaries
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Rebuild an official Elm macOS arm64 release binary as closely as possible, | |
| and compare the result with the official binary. | |
| Usage (from an elm/compiler checkout): | |
| ./reproduce-elm-mac-arm.py [VERSION] [--official PATH] [--repo PATH] [--out DIR] | |
| What it does: | |
| 1. Downloads the official binary (or uses --official). | |
| 2. Reads from the official binary: | |
| - the exact dependency versions (from the GHC unit IDs embedded in it), | |
| - the builder's HOME directory (embedded via cabal's Paths_* modules), | |
| - the linker version and the code signing timestamp. | |
| 3. Exports the VERSION git tag from the repo (the repo is not modified). | |
| 4. Builds with a fake HOME of the same length as the builder's HOME, so that | |
| cabal uses the same directory layout, with the dependency versions pinned | |
| and the Hackage index state set to the signing time. | |
| 5. Strips the binary, replaces the fake HOME with the builder's HOME (same | |
| length, so nothing moves), copies the official LC_UUID (ld derives it from | |
| the contents before patching, so it necessarily reflects the fake HOME), | |
| and ad-hoc signs it so it can run. | |
| 6. Compares the result with the official binary (signatures removed). If the | |
| build is reproducible, they are byte-for-byte identical. | |
| Requires: ghcup-installed ghc and cabal matching the official build. | |
| Written by Claude Opus 5.5. | |
| """ | |
| import argparse | |
| import datetime | |
| import gzip | |
| import json | |
| import os | |
| import re | |
| import shutil | |
| import subprocess | |
| import sys | |
| import urllib.request | |
| UNIT_ID = re.compile(rb"^([A-Za-z][A-Za-z0-9-]*)-([0-9]+(?:\.[0-9]+)*)-[0-9a-f]{8}$") | |
| CABAL_BIN = re.compile(rb"^(/.+)/\.cabal/bin$") | |
| ABI_TAG = re.compile(rb"/\.cabal/lib/aarch64-osx-(ghc-[0-9.]+-[0-9a-f]+)$") | |
| def log(message): | |
| print(f"\n==> {message}", flush=True) | |
| def warn(message): | |
| print(f"WARNING: {message}", file=sys.stderr, flush=True) | |
| def die(message): | |
| print(f"ERROR: {message}", file=sys.stderr, flush=True) | |
| sys.exit(1) | |
| def run(cmd, env=None, cwd=None, capture=False): | |
| print("$ " + " ".join(cmd), flush=True) | |
| result = subprocess.run( | |
| cmd, env=env, cwd=cwd, text=True, | |
| stdout=subprocess.PIPE if capture else None, | |
| stderr=subprocess.STDOUT if capture else None, | |
| ) | |
| if result.returncode != 0: | |
| if capture: | |
| print(result.stdout) | |
| die(f"Command failed with exit code {result.returncode}") | |
| return result.stdout | |
| def output(cmd, env=None): | |
| result = subprocess.run(cmd, env=env, capture_output=True, text=True) | |
| return result.stdout + result.stderr | |
| def strings(path): | |
| data = subprocess.run(["strings", "-a", path], capture_output=True).stdout | |
| return data.splitlines() | |
| def shorten(name): | |
| # cabal drops lowercase vowels from package names in unit IDs on macOS, | |
| # to keep paths short. | |
| return re.sub("[aeiou]", "", name) | |
| def unit_ids(lines): | |
| return {line.decode() for line in lines if UNIT_ID.match(line)} | |
| def ld_version(path): | |
| match = re.search(r"LC_BUILD_VERSION.*?tool 3\n\s*version (\S+)", output(["otool", "-l", path]), re.S) | |
| return match.group(1) if match else None | |
| def uuid(path): | |
| match = re.search(r"^\s*uuid (\S+)$", output(["otool", "-l", path]), re.M) | |
| return bytes.fromhex(match.group(1).replace("-", "")) if match else None | |
| def build_version(path): | |
| text = output(["otool", "-l", path]) | |
| match = re.search(r"minos (\S+)\n\s*sdk (\S+)", text) | |
| return match.groups() if match else None | |
| def signing_time(path): | |
| text = output(["codesign", "-dvv", path], env={**os.environ, "LC_ALL": "C"}) | |
| match = re.search(r"^Timestamp=(.+)$", text, re.M) | |
| if not match: | |
| return None | |
| try: | |
| local = datetime.datetime.strptime(match.group(1), "%d %b %Y at %H:%M:%S") | |
| except ValueError: | |
| return None | |
| return local.astimezone().astimezone(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") | |
| def pick_developer_dir(wanted_ld): | |
| candidates = [] | |
| if os.environ.get("DEVELOPER_DIR"): | |
| candidates.append(os.environ["DEVELOPER_DIR"]) | |
| candidates.append(output(["xcode-select", "-p"]).strip()) | |
| candidates.append("/Library/Developer/CommandLineTools") | |
| found = [] | |
| for directory in dict.fromkeys(candidates): | |
| if not os.path.isdir(directory): | |
| continue | |
| text = output(["xcrun", "ld", "-v"], env={**os.environ, "DEVELOPER_DIR": directory}) | |
| match = re.search(r"PROJECT:ld-(\S+)", text) | |
| version = match.group(1) if match else "unknown" | |
| found.append(f"{directory} (ld {version})") | |
| if version == wanted_ld: | |
| return directory | |
| warn(f"No developer directory has ld {wanted_ld}. Found: {', '.join(found)}") | |
| return candidates[-1] if os.path.isdir(candidates[-1]) else candidates[0] | |
| def fake_home_for(real_home): | |
| # Must be the same length as the builder's HOME, so the binary can be | |
| # patched without moving anything. | |
| base = "/tmp/elm-reproduce-home" | |
| if len(real_home) < len("/tmp/x"): | |
| die(f"Builder HOME {real_home!r} is too short to mirror under /tmp") | |
| return (base + "x" * len(real_home))[:len(real_home)] | |
| def remove_signature(source, target): | |
| shutil.copyfile(source, target) | |
| run(["codesign", "--remove-signature", target]) | |
| def main(): | |
| parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | |
| parser.add_argument("version", nargs="?", default="0.19.3") | |
| parser.add_argument("--official", help="path to the (gunzipped) official binary; downloaded if omitted") | |
| parser.add_argument("--repo", default=".", help="elm/compiler git checkout (default: current directory)") | |
| parser.add_argument("--out", default="elm-reproduce-out", help="output directory (default: ./elm-reproduce-out)") | |
| args = parser.parse_args() | |
| if os.uname().sysname != "Darwin" or os.uname().machine != "arm64": | |
| die("This script only reproduces the macOS arm64 binary, on macOS arm64.") | |
| out = os.path.abspath(args.out) | |
| os.makedirs(out, exist_ok=True) | |
| repo = os.path.abspath(args.repo) | |
| # 1. Official binary. | |
| official = os.path.join(out, f"elm-{args.version}-mac-arm-official") | |
| if args.official: | |
| shutil.copyfile(args.official, official) | |
| else: | |
| url = f"https://github.com/elm/compiler/releases/download/{args.version}/elm-{args.version}-mac-arm.gz" | |
| log(f"Downloading {url}") | |
| with urllib.request.urlopen(url) as response: | |
| data = gzip.decompress(response.read()) | |
| with open(official, "wb") as file: | |
| file.write(data) | |
| os.chmod(official, 0o755) | |
| # 2. Analyze it. | |
| log("Analyzing the official binary") | |
| official_strings = strings(official) | |
| official_units = unit_ids(official_strings) | |
| homes = {m.group(1) for line in official_strings if (m := CABAL_BIN.match(line))} | |
| abi_tags = {m.group(1).decode() for line in official_strings if (m := ABI_TAG.search(line))} | |
| if len(homes) != 1 or len(abi_tags) != 1: | |
| die(f"Could not determine the builder's HOME / GHC ABI tag (found {homes}, {abi_tags})") | |
| real_home = homes.pop().decode() | |
| abi_tag = abi_tags.pop() | |
| official_ld = ld_version(official) | |
| index_state = signing_time(official) | |
| print(f"Builder HOME: {real_home}") | |
| print(f"GHC ABI tag: {abi_tag}") | |
| print(f"Linker version: {official_ld}") | |
| print(f"minos / sdk: {build_version(official)}") | |
| print(f"Signed at: {index_state}") | |
| print(f"Unit IDs found: {len(official_units)}") | |
| fake_home = fake_home_for(real_home) | |
| developer_dir = pick_developer_dir(official_ld) | |
| env = {**os.environ, "HOME": fake_home, "DEVELOPER_DIR": developer_dir} | |
| for name in ["CABAL_DIR", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME", "XDG_DATA_HOME"]: | |
| env.pop(name, None) | |
| print(f"Fake HOME: {fake_home}") | |
| print(f"DEVELOPER_DIR: {developer_dir}") | |
| os.makedirs(fake_home, exist_ok=True) | |
| if os.path.exists(os.path.join(fake_home, ".cabal")): | |
| die(f"{fake_home}/.cabal exists, which would make cabal use a different layout. Remove it.") | |
| local_abi = run(["cabal", "path", "--compiler-info"], env=env, cwd=fake_home, capture=True) | |
| match = re.search(r"^compiler-abi-tag: (\S+)$", local_abi, re.M) | |
| if not match or match.group(1) != abi_tag: | |
| die(f"Your GHC does not match: need {abi_tag}, have {match and match.group(1)}.\n" | |
| f"Install it with: ghcup install ghc {abi_tag.split('-')[1]}") | |
| # 3. Source. | |
| log(f"Exporting tag {args.version} from {repo}") | |
| src = os.path.join(fake_home, f"src-{args.version}") | |
| os.makedirs(src, exist_ok=True) | |
| archive = subprocess.run(["git", "-C", repo, "archive", f"refs/tags/{args.version}"], capture_output=True) | |
| if archive.returncode != 0: | |
| die(archive.stderr.decode()) | |
| subprocess.run(["tar", "-x", "-C", src], input=archive.stdout, check=True) | |
| # 4. Resolve dependency versions. | |
| log("Updating the package index") | |
| index_args = [f"--index-state={index_state}"] if index_state else [] | |
| run(["cabal", "update"], env=env, cwd=src) | |
| wanted = {} # shortened name -> version | |
| for unit in official_units: | |
| name, version = UNIT_ID.match(unit.encode()).groups() | |
| wanted[name.decode()] = version.decode() | |
| constraints = {} | |
| for attempt in range(5): | |
| log(f"Resolving dependencies (attempt {attempt + 1})") | |
| constraint_args = [f"--constraint=any.{n} =={v}" for n, v in sorted(constraints.items())] | |
| run(["cabal", "build", "elm", "--dry-run", *index_args, *constraint_args], env=env, cwd=src, capture=True) | |
| with open(os.path.join(src, "dist-newstyle", "cache", "plan.json")) as file: | |
| plan = json.load(file) | |
| by_short = {} | |
| for package in plan["install-plan"]: | |
| if package["type"] == "configured" and package.get("style") != "local": | |
| by_short.setdefault(shorten(package["pkg-name"]), set()).add(package["pkg-name"]) | |
| collisions = {k: v for k, v in by_short.items() if len(v) > 1} | |
| if collisions: | |
| die(f"Ambiguous shortened package names: {collisions}") | |
| new = {next(iter(by_short[s])): v for s, v in wanted.items() if s in by_short} | |
| if new == constraints: | |
| break | |
| constraints = new | |
| else: | |
| die("Dependency resolution did not converge") | |
| missing = sorted(set(wanted) - set(by_short)) | |
| if missing: | |
| die(f"Packages in the official binary that are not in the build plan: {missing}") | |
| print("Pinned versions:") | |
| for name, version in sorted(constraints.items()): | |
| print(f" {name} == {version}") | |
| # 5. Build. | |
| log("Building") | |
| constraint_args = [f"--constraint=any.{n} =={v}" for n, v in sorted(constraints.items())] | |
| run(["cabal", "build", "elm", "--ghc-option=-split-sections", *index_args, *constraint_args], env=env, cwd=src) | |
| built = run(["cabal", "list-bin", "elm", *index_args, *constraint_args], env=env, cwd=src, capture=True).strip().splitlines()[-1] | |
| rebuilt = os.path.join(out, f"elm-{args.version}-mac-arm-rebuilt") | |
| shutil.copyfile(built, rebuilt) | |
| os.chmod(rebuilt, 0o755) | |
| run(["strip", rebuilt], env=env) | |
| # 6. Patch HOME. | |
| log(f"Replacing {fake_home} with {real_home}") | |
| with open(rebuilt, "rb") as file: | |
| data = file.read() | |
| fake_count = data.count(fake_home.encode()) | |
| data = data.replace(fake_home.encode(), real_home.encode()) | |
| with open(rebuilt, "wb") as file: | |
| file.write(data) | |
| with open(official, "rb") as file: | |
| real_count = file.read().count(real_home.encode()) | |
| print(f"Replaced {fake_count} occurrences (official binary has {real_count})") | |
| official_uuid, rebuilt_uuid = uuid(official), uuid(rebuilt) | |
| log(f"Replacing LC_UUID {rebuilt_uuid.hex()} with {official_uuid.hex()}") | |
| with open(rebuilt, "rb") as file: | |
| data = file.read() | |
| if data.count(rebuilt_uuid) != 1: | |
| die(f"Expected the UUID exactly once in the rebuilt binary, found it {data.count(rebuilt_uuid)} times") | |
| with open(rebuilt, "wb") as file: | |
| file.write(data.replace(rebuilt_uuid, official_uuid)) | |
| run(["codesign", "--sign", "-", "--force", rebuilt]) | |
| # 7. Compare. | |
| log("Comparing") | |
| problems = [] | |
| rebuilt_units = unit_ids(strings(rebuilt)) | |
| if rebuilt_units != official_units: | |
| problems.append("unit IDs differ:\n" + "\n".join( | |
| [f" only official: {u}" for u in sorted(official_units - rebuilt_units)] + | |
| [f" only rebuilt: {u}" for u in sorted(rebuilt_units - official_units)])) | |
| else: | |
| print(f"All {len(official_units)} unit IDs match") | |
| if ld_version(rebuilt) != official_ld or build_version(rebuilt) != build_version(official): | |
| problems.append(f"toolchain differs: ld {ld_version(rebuilt)} vs {official_ld}, " | |
| f"minos/sdk {build_version(rebuilt)} vs {build_version(official)}") | |
| if fake_count != real_count: | |
| problems.append(f"HOME occurs {fake_count} times in the rebuilt binary but {real_count} times in the official one") | |
| official_unsigned = os.path.join(out, "official-unsigned") | |
| rebuilt_unsigned = os.path.join(out, "rebuilt-unsigned") | |
| remove_signature(official, official_unsigned) | |
| remove_signature(rebuilt, rebuilt_unsigned) | |
| with open(official_unsigned, "rb") as a, open(rebuilt_unsigned, "rb") as b: | |
| a_data, b_data = a.read(), b.read() | |
| if a_data == b_data: | |
| print("Unsigned binaries are byte-for-byte identical") | |
| else: | |
| differing = sum(x != y for x, y in zip(a_data, b_data)) | |
| print(f"Unsigned binaries differ: sizes {len(a_data)} vs {len(b_data)}, {differing} differing bytes") | |
| a_lc = output(["otool", "-l", official_unsigned]).splitlines()[1:] | |
| b_lc = output(["otool", "-l", rebuilt_unsigned]).splitlines()[1:] | |
| for line_a, line_b in zip(a_lc, b_lc): | |
| if line_a != line_b: | |
| print(f" load command: {line_a.strip()!r} vs {line_b.strip()!r}") | |
| problems.append("unsigned binaries are not identical") | |
| log("Result") | |
| print(f"Official binary: {official}") | |
| print(f"Rebuilt binary: {rebuilt}") | |
| if problems: | |
| print("Differences:") | |
| for problem in problems: | |
| print(f"- {problem}") | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment