Source code for scripts.build_pyinstaller

from __future__ import annotations

import argparse
import importlib.util
import os
import platform
import shutil
import subprocess
import sys
import tarfile
import zipfile
from pathlib import Path


def _run(command: list[str], cwd: Path):
    print(" ".join(command))
    completed = subprocess.run(command, cwd=str(cwd), check=False)
    if completed.returncode != 0:
        raise SystemExit(completed.returncode)


def _platform_tag() -> str:
    system = platform.system().lower()
    machine = platform.machine().lower()
    if system == "darwin":
        system = "macos"
    elif system == "windows":
        system = "windows"
    elif system == "linux":
        system = "linux"
    return f"{system}-{machine}"


def _resolve_build_output(dist_dir: Path, app_name: str) -> Path:
    system = platform.system().lower()
    candidates: list[Path] = []
    if system == "darwin":
        candidates.append(dist_dir / f"{app_name}.app")
    elif system == "windows":
        candidates.append(dist_dir / f"{app_name}.exe")
        candidates.append(dist_dir / app_name / f"{app_name}.exe")
        candidates.append(dist_dir / app_name)
    else:
        candidates.append(dist_dir / app_name)
        candidates.append(dist_dir / app_name / app_name)
    for candidate in candidates:
        if candidate.exists():
            return candidate
    raise RuntimeError(
        "Expected build output not found. Checked: "
        + ", ".join(str(path) for path in candidates)
    )


def _archive_folder(source: Path, output_file: Path):
    output_file.parent.mkdir(parents=True, exist_ok=True)
    if output_file.suffix == ".zip":
        with zipfile.ZipFile(output_file, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
            if source.is_file():
                zipf.write(source, source.name)
            else:
                for path in source.rglob("*"):
                    zipf.write(path, path.relative_to(source.parent))
        return
    with tarfile.open(output_file, "w:gz") as tarf:
        tarf.add(source, arcname=source.name)


def _resolve_archive_source(build_output: Path) -> Path:
    if build_output.is_dir():
        return build_output
    if build_output.is_file():
        parent = build_output.parent
        if (parent / "_internal").exists():
            return parent
    return build_output


def _resolve_default_icon(project_root: Path) -> Path | None:
    assets = project_root / "editor" / "ui" / "assets" / "images"
    system = platform.system().lower()
    if system == "darwin":
        icns = assets / "icon.icns"
        return icns if icns.exists() else None
    preferred = [
        assets / "icon_256.ico",
        assets / "icon_128.ico",
        assets / "icon_64.ico",
        assets / "icon_48.ico",
        assets / "icon_32.ico",
        assets / "icon_16.ico",
        assets / "icon.ico",
        assets / "icon.png",
    ]
    for candidate in preferred:
        if candidate.exists():
            return candidate
    return None


def _collectable_packages() -> list[str]:
    candidates = [
        "pygame",
        "PyQt6",
        "PIL",
        "pygbag",
        "PyInstaller",
        "websockets",
        "webview",
        "aiortc",
        "buildozer",
        "qtawesome",
    ]
    available: list[str] = []
    for name in candidates:
        if importlib.util.find_spec(name) is not None:
            available.append(name)
    return available


[docs] def build(app_name: str, entrypoint: str, project_root: Path, icon: str | None = None, version: str | None = None): build_root = project_root / "build" / "pyinstaller" dist_dir = build_root / "dist" work_dir = build_root / "work" spec_dir = build_root / "spec" release_dir = project_root / "build" / "release" if build_root.exists(): shutil.rmtree(build_root) build_root.mkdir(parents=True, exist_ok=True) path_sep = os.pathsep command = [ sys.executable, "-m", "PyInstaller", "--noconfirm", "--clean", "--windowed", "--name", app_name, "--distpath", str(dist_dir), "--workpath", str(work_dir), "--specpath", str(spec_dir), "--paths", str(project_root), "--add-data", f"{project_root / 'core'}{path_sep}core", "--add-data", f"{project_root / 'editor'}{path_sep}editor", "--add-data", f"{project_root / 'export'}{path_sep}export", "--add-data", f"{project_root / 'plugins'}{path_sep}plugins", "--hidden-import", "PyQt6.Qsci", "--hidden-import", "pygame.base", "--hidden-import", "pygame.constants", "--hidden-import", "pygame.rect", "--hidden-import", "pygame.rwobject", "--hidden-import", "pygame.surflock", "--hidden-import", "pygame.color", "--hidden-import", "pygame.bufferproxy", "--hidden-import", "pygame.math", "--hidden-import", "pygame.pixelcopy", "--exclude-module", "PyQt5", "--exclude-module", "PySide2", "--exclude-module", "PySide6", ] for package_name in _collectable_packages(): command.extend(["--collect-all", package_name]) icon_path = Path(icon) if icon else _resolve_default_icon(project_root) if icon_path is not None and icon_path.exists(): command.extend(["--icon", str(icon_path)]) command.append(str(project_root / entrypoint)) _run(command, project_root) built = _resolve_build_output(dist_dir, app_name) platform_tag = _platform_tag() version_suffix = f"-{version}" if version else "" if platform.system().lower() == "windows": archive = release_dir / f"{app_name}{version_suffix}-{platform_tag}.zip" else: archive = release_dir / f"{app_name}{version_suffix}-{platform_tag}.tar.gz" archive_source = _resolve_archive_source(built) print("packaging release archive in progress...") _archive_folder(archive_source, archive) print(f"packaging is complete! The result is available in {archive}")
[docs] def main(): parser = argparse.ArgumentParser() parser.add_argument("--name", default="AxisPyEngine") parser.add_argument("--entrypoint", default="main.py") parser.add_argument("--icon", default=None) parser.add_argument("--version", default=None) args = parser.parse_args() project_root = Path(__file__).resolve().parents[1] build(args.name, args.entrypoint, project_root, icon=args.icon, version=args.version)
if __name__ == "__main__": main()