Files
zoitechat/packaging/macos/build.py
T
2026-10-14 17:54:45 -06:00

72 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""Compile, test, stage and package ZoiteChat on the host Mac."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import platform
import shutil
import subprocess
import sys
from bundle import ROOT, package, run
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--build-dir", type=Path, default=Path("build-macos"))
parser.add_argument("--output", type=Path, default=Path("dist/ZoiteChat.app"))
parser.add_argument("--build-number", default="1")
parser.add_argument("--store-experimental", action="store_true", help="Build a separate sandbox test variant without loadable plugins; not submission approval")
args = parser.parse_args()
if platform.system() != "Darwin":
parser.error("Build on macOS; cross-compiling from Linux is not supported")
if args.output.exists():
parser.error("Output already exists; use a new --output or explicitly remove the old app")
build = args.build_dir.resolve()
if build == ROOT or ROOT.is_relative_to(build):
parser.error("Build directory must not contain the source checkout")
run("xcrun", "--find", "clang")
env = os.environ.copy()
prefixes = [Path(run("brew", "--prefix", name)) for name in ("gettext", "openssl@3", "libarchive", "lua", "gtk+3", "gtk-mac-integration")]
brew = Path(run("brew", "--prefix"))
# Keg-only gettext, OpenSSL and libarchive are not necessarily in PATH/pkg-config.
env["PATH"] = os.pathsep.join([str(p / "bin") for p in prefixes] + [str(brew / "bin"), env.get("PATH", "")])
env["PKG_CONFIG_PATH"] = os.pathsep.join([str(p / "lib/pkgconfig") for p in prefixes] + [str(brew / "lib/pkgconfig"), str(brew / "share/pkgconfig"), env.get("PKG_CONFIG_PATH", "")])
# Native dependency bottles determine the true deployment target; the bundle
# auditor derives the final minimum rather than promising an arbitrary old OS.
env.setdefault("MACOSX_DEPLOYMENT_TARGET", "11.0")
backend = run("pkg-config", "--variable=targets", "gtk+-3.0", env=env).split()
if "quartz" not in backend:
parser.error(f"GTK must provide the native Quartz backend, got {backend}")
# Do not silently select an XQuartz GTK stack or mix Intel/ARM Homebrew prefixes.
machine = platform.machine()
if machine not in ("arm64", "x86_64"):
parser.error(f"Unsupported architecture: {machine}")
options = [
"--prefix=/", "--libdir=lib", "--buildtype=release", "-Dgtk-frontend=true",
"-Dtext-frontend=false", "-Dmac-integration=enabled", "-Ddbus=disabled",
"-Dlibcanberra=disabled", "-Dappindicator=disabled", "-Dinstall-appdata=false",
"-Dwith-sysinfo=false", "-Dwith-python=false", "-Dwith-perl=false",
"-Dwith-lua=lua", "-Dwith-fishlim=true", "-Dwith-checksum=true",
"-Dplugin=" + ("false" if args.store_experimental else "true"),
]
if (build / "meson-private/coredata.dat").exists():
options.insert(0, "--reconfigure")
subprocess.run(["meson", "setup", str(build), str(ROOT), *options], env=env, check=True)
subprocess.run(["meson", "compile", "-C", str(build)], env=env, check=True)
subprocess.run(["meson", "test", "-C", str(build), "--print-errorlogs"], env=env, check=True)
stage = build / "macos-stage"
if stage.exists():
shutil.rmtree(stage)
subprocess.run(["meson", "install", "-C", str(build), "--destdir", str(stage)], env=env, check=True)
package(stage, args.output.resolve(), args.build_number)
if __name__ == "__main__":
try:
main()
except (RuntimeError, subprocess.CalledProcessError, OSError) as error:
sys.exit(f"macOS build failed: {error}")