Files
free-claude-code/scripts/validate_release.py
Ali Khokhar c5239619be patch: Fix macOS app menu-bar launch identity (#2095)
## Why

Fixes #2092. Launching the macOS app through its shell wrapper replaces
the app executable with Python. AppKit then sees Python's bundle instead
of Free Claude Code, and the menu-bar item can have a zero-height window
even though pystray reports it visible.

## How

- Run the installed uv-managed Python runtime inside a native app
executable, preserving the app's bundle identity and main thread. Enable
UTF-8 and retain the tool environment for imports and child processes.
- Build arm64 and x86_64 launchers during release staging and include
both in the existing wheel and source distribution. Select the launcher
matching the installed Python architecture.
- Create and ad-hoc sign the app through a Python installer helper.
Validate its runtime, stage replacements, preserve foreign apps and
Desktop entries, and recognize existing FCC ownership markers during
upgrades and uninstall.
- Add native macOS coverage for bundle identity, visible menu-bar
geometry, server lifecycle, duplicate launches, child processes, and
reinstall. Add Windows and Linux source-distribution rebuild checks and
require these jobs in CI.




<!-- greptile_comment -->

<!-- greptile_summary -->

<h2><a
href="https://app.greptile.com/api/retrigger?id=81926736"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/RetriggerDark.svg?v=2"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=2"><img
alt="Retrigger"
src="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=2"
align="right"></picture></a><a
href="https://app.greptile.com/alishahryar1/github/Alishahryar1/free-claude-code/-/pull/2095"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewInGreptileDark.svg?v=2"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewInGreptile.svg?v=2"><img
alt="View in Greptile"
src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewInGreptile.svg?v=2"
align="right"></picture></a>Confidence Score: 5/5</h2>

<!-- greptile-impact-verdict --> No outstanding findings prevent
merging.

<details open><summary><h3>Summary</h3></summary>

The PR replaces the macOS shell launcher with a native executable that
keeps Free Claude Code's app identity while running its installed Python
environment.
- The macOS app keeps its identity while running the desktop app.
- Release packages carry native launchers for both Mac architectures.
- CI checks the native Mac app on Intel and Apple silicon.
</details>

<!-- greptile_confidence_score:5 -->

<sub>Reviews (2) · Last reviewed commit: ["Restore POSIX uv reuse
regression
covera..."](b50bb8449e)
· [Reviewed by
Greptile](https://www.greptile.com/?utm_source=greptile_review&utm_medium=github&utm_campaign=review_public&utm_content=alishahryar1/free-claude-code)</sub>

<!-- /greptile_comment -->
2026-10-15 18:24:22 -07:00

138 lines
4.8 KiB
Python

"""Check the exact FCC distributions that will be published."""
import argparse
import configparser
import re
import subprocess
import tarfile
import tomllib
import zipfile
from email.parser import BytesParser
from pathlib import Path
MACOS_TEMPLATES = {
f"src/free_claude_code/assets/macos/fcc-desktop-{architecture}"
for architecture in ("arm64", "x86_64")
}
def require_files(names: set[str], expected: set[str]) -> None:
missing = expected - names
if missing:
raise ValueError("Missing release files: " + ", ".join(sorted(missing)))
def validate(directory: Path, version: str) -> None:
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))[
"project"
]
name = project["name"]
if not re.fullmatch(
r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)", version
):
raise ValueError("Expected a stable MAJOR.MINOR.PATCH release version")
if "version" in project or "version" not in project.get("dynamic", []):
raise ValueError("Release source must use dynamic version metadata")
stem = f"{name.replace('-', '_')}-{version}"
wheel_path = directory / f"{stem}-py3-none-any.whl"
sdist_path = directory / f"{stem}.tar.gz"
# uv build creates a .gitignore alongside its distributions.
artifacts = {path for path in directory.iterdir() if path.name != ".gitignore"}
if artifacts != {wheel_path, sdist_path}:
raise ValueError(
"Release directory must contain only the current wheel and sdist"
)
tracked = subprocess.run(
["git", "ls-files", "-z", "src/free_claude_code"],
check=True,
capture_output=True,
text=True,
encoding="utf-8",
).stdout
sources = set(filter(None, tracked.split("\0")))
if "src/free_claude_code/__init__.py" not in sources:
raise ValueError("Missing tracked FCC package sources")
sources |= MACOS_TEMPLATES
with zipfile.ZipFile(wheel_path) as archive:
wheel = {
file: archive.read(file)
for file in archive.namelist()
if not file.endswith("/")
}
with tarfile.open(sdist_path) as archive:
source = {}
for file in archive:
stream = archive.extractfile(file) if file.isfile() else None
if stream is not None:
source[file.name] = stream.read()
wheel_info = f"{stem}.dist-info"
updater_names = {"fcc-update", "fcc-update.cmd"}
require_files(
set(wheel),
{file.removeprefix("src/") for file in sources}
| {f"{stem}.data/scripts/{script}" for script in updater_names}
| {f"{wheel_info}/METADATA", f"{wheel_info}/entry_points.txt"},
)
require_files(
set(source),
{f"{stem}/{file}" for file in sources}
| {f"{stem}/scripts/update/{script}" for script in updater_names}
| {
f"{stem}/{file}"
for file in (
"pyproject.toml",
"README.md",
"LICENSE",
"PKG-INFO",
"scripts/build_macos_launcher.py",
"scripts/macos/launcher.m",
)
},
)
for template in MACOS_TEMPLATES:
data = wheel[template.removeprefix("src/")]
if not data or data != source[f"{stem}/{template}"]:
raise ValueError(
f"Native template differs between wheel and sdist: {template}"
)
for data in (wheel[f"{wheel_info}/METADATA"], source[f"{stem}/PKG-INFO"]):
metadata = BytesParser().parsebytes(data)
if metadata["Name"] != name or metadata["Version"] != version:
raise ValueError(
"Release metadata does not match the project name and version"
)
entrypoints = configparser.ConfigParser()
entrypoints.read_string(wheel[f"{wheel_info}/entry_points.txt"].decode())
for project_key, section in (
("scripts", "console_scripts"),
("gui-scripts", "gui_scripts"),
):
actual = dict(entrypoints[section]) if section in entrypoints else {}
if actual != project.get(project_key, {}):
raise ValueError(f"Release entry points do not match {project_key}")
print(f"Validated {wheel_path.name} and {sdist_path.name}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("directory", type=Path)
parser.add_argument("--version", required=True)
args = parser.parse_args()
try:
validate(args.directory, args.version)
except (
ValueError,
KeyError,
OSError,
zipfile.BadZipFile,
tarfile.TarError,
subprocess.CalledProcessError,
) as error:
print(f"Release validation failed: {error}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())