mirror of
https://github.com/ihmily/StreamCap.git
synced 2026-09-03 06:48:00 +08:00
223 lines
7.7 KiB
Python
223 lines
7.7 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import platform
|
|
import plistlib
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
APP_NAME = "StreamCap"
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
FLET_ARCHIVE_DIR = ROOT / "build" / "flet_desktop_app"
|
|
VENDOR_DIR = ROOT / "vendor"
|
|
|
|
|
|
def detect_target_platform() -> str:
|
|
system = platform.system()
|
|
if system == "Darwin":
|
|
return "macos"
|
|
if system == "Windows":
|
|
return "windows"
|
|
raise SystemExit(f"Unsupported build platform: {system}")
|
|
|
|
|
|
def get_flet_version() -> str:
|
|
try:
|
|
import flet_desktop.version
|
|
except ImportError as exc:
|
|
raise SystemExit("flet_desktop is not installed in the current Python environment.") from exc
|
|
return flet_desktop.version.version
|
|
|
|
|
|
def artifact_name(target_platform: str) -> str:
|
|
if target_platform == "macos":
|
|
return "flet-macos.tar.gz"
|
|
if target_platform == "windows":
|
|
return "flet-windows.zip"
|
|
raise ValueError(target_platform)
|
|
|
|
|
|
def prepare_flet_archive(target_platform: str, refresh: bool) -> Path:
|
|
version = get_flet_version()
|
|
name = artifact_name(target_platform)
|
|
target_dir = FLET_ARCHIVE_DIR / target_platform
|
|
archive_path = target_dir / name
|
|
legacy_archive_path = FLET_ARCHIVE_DIR / name
|
|
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
if archive_path.exists() and not refresh:
|
|
print(f"Using cached Flet desktop archive: {archive_path}")
|
|
return target_dir
|
|
|
|
if legacy_archive_path.exists() and not refresh:
|
|
shutil.copy2(legacy_archive_path, archive_path)
|
|
print(f"Copied cached Flet desktop archive: {archive_path}")
|
|
return target_dir
|
|
|
|
url = f"https://github.com/flet-dev/flet/releases/download/v{version}/{name}"
|
|
print(f"Downloading {url}")
|
|
urllib.request.urlretrieve(url, archive_path)
|
|
print(f"Saved Flet desktop archive: {archive_path}")
|
|
return target_dir
|
|
|
|
|
|
def add_data_arg(source: str, dest: str, target_platform: str) -> str:
|
|
separator = ";" if target_platform == "windows" else ":"
|
|
return f"{source}{separator}{dest}"
|
|
|
|
|
|
def bundled_ffmpeg_source(target_platform: str) -> Path:
|
|
if target_platform == "macos":
|
|
return VENDOR_DIR / "ffmpeg" / "macos" / "ffmpeg"
|
|
if target_platform == "windows":
|
|
return VENDOR_DIR / "ffmpeg" / "windows" / "ffmpeg.exe"
|
|
raise ValueError(target_platform)
|
|
|
|
|
|
def bundled_node_source(target_platform: str) -> Path:
|
|
if target_platform == "macos":
|
|
return VENDOR_DIR / "node" / "macos" / "node"
|
|
if target_platform == "windows":
|
|
return VENDOR_DIR / "node" / "windows" / "node.exe"
|
|
raise ValueError(target_platform)
|
|
|
|
|
|
def pyinstaller_command(args: argparse.Namespace, target_platform: str) -> list[str]:
|
|
command = [
|
|
sys.executable,
|
|
"-m",
|
|
"PyInstaller",
|
|
"--onedir",
|
|
"-w",
|
|
"--contents-directory",
|
|
".",
|
|
"--add-data",
|
|
add_data_arg(str(ROOT / "config"), "config", target_platform),
|
|
"--add-data",
|
|
add_data_arg(str(ROOT / "locales"), "locales", target_platform),
|
|
"--add-data",
|
|
add_data_arg(str(ROOT / "assets"), "assets", target_platform),
|
|
"--hidden-import",
|
|
"pyexpat",
|
|
"--hidden-import",
|
|
"_multiprocessing",
|
|
"--hidden-import",
|
|
"tzdata",
|
|
"--collect-data",
|
|
"streamget",
|
|
"--name",
|
|
APP_NAME,
|
|
"--icon",
|
|
str(ROOT / ("assets/icons/Appicon.icns" if target_platform == "macos" else "assets/icon.ico")),
|
|
"--noconfirm",
|
|
]
|
|
|
|
ffmpeg_source = bundled_ffmpeg_source(target_platform)
|
|
if args.bundle_ffmpeg and ffmpeg_source.is_file():
|
|
command.extend(["--add-data", add_data_arg(str(ffmpeg_source), "ffmpeg", target_platform)])
|
|
elif args.bundle_ffmpeg:
|
|
print(f"Bundled FFmpeg not found, skipping: {ffmpeg_source}")
|
|
|
|
node_source = bundled_node_source(target_platform)
|
|
if args.bundle_node and node_source.is_file():
|
|
command.extend(["--add-data", add_data_arg(str(node_source), "node", target_platform)])
|
|
elif args.bundle_node:
|
|
print(f"Bundled Node.js not found, skipping: {node_source}")
|
|
|
|
if args.clean:
|
|
command.append("--clean")
|
|
|
|
if target_platform == "macos":
|
|
command.extend(
|
|
[
|
|
"--hidden-import",
|
|
"plyer.platforms.macosx.notification",
|
|
"--osx-bundle-identifier",
|
|
"io.github.ihmily.streamcap",
|
|
]
|
|
)
|
|
if args.target_arch:
|
|
command.extend(["--target-arch", args.target_arch])
|
|
else:
|
|
command.extend(["--hidden-import", "plyer.platforms.win.notification"])
|
|
version_file = ROOT / "version_info.txt"
|
|
if version_file.is_file():
|
|
command.extend(["--version-file", str(version_file)])
|
|
|
|
command.append(str(ROOT / "main.py"))
|
|
return command
|
|
|
|
|
|
def run_build(args: argparse.Namespace) -> None:
|
|
target_platform = args.platform or detect_target_platform()
|
|
host_platform = detect_target_platform()
|
|
if target_platform != host_platform:
|
|
raise SystemExit(
|
|
f"PyInstaller does not support cross-compiling from {host_platform} to {target_platform}. "
|
|
f"Run this script on {target_platform} instead."
|
|
)
|
|
|
|
flet_view_path = prepare_flet_archive(target_platform, args.refresh_flet)
|
|
|
|
env = os.environ.copy()
|
|
env["FLET_VIEW_PATH"] = str(flet_view_path)
|
|
|
|
command = pyinstaller_command(args, target_platform)
|
|
print("Running:")
|
|
print(" ".join(command))
|
|
subprocess.run(command, cwd=ROOT, env=env, check=True)
|
|
|
|
if target_platform == "macos":
|
|
app_path = ROOT / "dist" / f"{APP_NAME}.app"
|
|
hide_launcher_dock_icon(app_path)
|
|
print(f"Built app: {app_path}")
|
|
print(f"Run with: open {app_path}")
|
|
else:
|
|
print(f"Built app: {ROOT / 'dist' / APP_NAME / f'{APP_NAME}.exe'}")
|
|
|
|
|
|
def hide_launcher_dock_icon(app_path: Path) -> None:
|
|
info_plist = app_path / "Contents" / "Info.plist"
|
|
if not info_plist.is_file():
|
|
return
|
|
|
|
with info_plist.open("rb") as file:
|
|
info = plistlib.load(file)
|
|
info["LSUIElement"] = True
|
|
with info_plist.open("wb") as file:
|
|
plistlib.dump(info, file)
|
|
|
|
codesign = shutil.which("codesign")
|
|
if codesign:
|
|
subprocess.run([codesign, "--force", "--deep", "--sign", "-", str(app_path)], check=True)
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Build StreamCap desktop packages with PyInstaller.")
|
|
parser.add_argument("--platform", choices=("macos", "windows"), help="Target platform. Defaults to the host OS.")
|
|
parser.add_argument("--target-arch", choices=("arm64", "x86_64", "universal2"), help="macOS target architecture.")
|
|
parser.add_argument("--refresh-flet", action="store_true", help="Re-download the Flet desktop archive.")
|
|
parser.add_argument(
|
|
"--no-bundle-ffmpeg",
|
|
dest="bundle_ffmpeg",
|
|
action="store_false",
|
|
help="Do not bundle vendor FFmpeg even if present.",
|
|
)
|
|
parser.add_argument(
|
|
"--no-bundle-node",
|
|
dest="bundle_node",
|
|
action="store_false",
|
|
help="Do not bundle vendor Node.js even if present.",
|
|
)
|
|
parser.add_argument("--no-clean", dest="clean", action="store_false", help="Do not pass --clean to PyInstaller.")
|
|
parser.set_defaults(bundle_ffmpeg=True, bundle_node=True, clean=True)
|
|
return parser.parse_args()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run_build(parse_args())
|