mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-11-15 23:03:40 +08:00
## 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 -->
138 lines
4.8 KiB
Python
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())
|