# -*- mode: python -*- # vi: set ft=python : import os import platform import shlex import subprocess from pathlib import Path import aw_core import flask_restx def build_analysis(name, location, binaries=[], datas=[], hiddenimports=[]): name_py = name.replace("-", "_") location_candidates = [ location / f"{name_py}/__main__.py", location / f"src/{name_py}/__main__.py", ] try: location = next(p for p in location_candidates if p.exists()) except StopIteration: raise Exception(f"Could not find {name} location from {location_candidates}") return Analysis( [location], pathex=[], binaries=binaries, datas=datas, hiddenimports=hiddenimports, hookspath=[], runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, ) def build_collect(analysis, name, console=True, entitlements=None): """Used to build the COLLECT statements for each module""" pyz = PYZ(analysis.pure, analysis.zipped_data) exe = EXE( pyz, analysis.scripts, exclude_binaries=True, name=name, debug=False, strip=False, upx=True, console=console, contents_directory=".", entitlements_file=entitlements or entitlements_file, codesign_identity=codesign_identity, ) return COLLECT( exe, analysis.binaries, analysis.zipfiles, analysis.datas, strip=False, upx=True, name=name, ) # Get the current release version current_release = subprocess.run( shlex.split("git describe --tags --abbrev=0"), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf8", ).stdout.strip() print("bundling activitywatch version " + current_release) # Get entitlements and codesign identity entitlements_file = Path(".") / "scripts" / "package" / "entitlements.plist" app_entitlements_file = Path(".") / "scripts" / "package" / "app-entitlements.plist" codesign_identity = os.environ.get("APPLE_PERSONALID", "").strip() if not codesign_identity: print("Environment variable APPLE_PERSONALID not set. Releases won't be signed.") aw_core_path = Path(os.path.dirname(aw_core.__file__)) restx_path = Path(os.path.dirname(flask_restx.__file__)) aws_location = Path("aw-server") aw_server_rust_location = Path("aw-server-rust") aw_server_rust_bin = aw_server_rust_location / "target/package/aw-server-rust" aw_sync_bin = aw_server_rust_location / "target/package/aw-sync" aw_qt_location = Path("aw-qt") awa_location = Path("aw-watcher-afk") aww_location = Path("aw-watcher-window") awi_location = Path("aw-watcher-input") aw_notify_location = Path("aw-notify") if platform.system() == "Darwin": from PyInstaller.utils import osx as osxutils icon = aw_qt_location / "media/logo/logo.icns" else: icon = aw_qt_location / "media/logo/logo.ico" skip_rust = False if not aw_server_rust_bin.exists(): skip_rust = True print("Skipping Rust build because aw-server-rust binary not found.") aw_qt_a = build_analysis( "aw-qt", aw_qt_location, binaries=[(aw_server_rust_bin, "."), (aw_sync_bin, ".")] if not skip_rust else [], datas=[ (aw_qt_location / "resources/aw-qt.desktop", "aw_qt/resources"), (aw_qt_location / "media", "aw_qt/media"), ], ) aw_server_a = build_analysis( "aw-server", aws_location, datas=[ (aws_location / "aw_server/static", "aw_server/static"), (restx_path / "templates", "flask_restx/templates"), (restx_path / "static", "flask_restx/static"), (aw_core_path / "schemas", "aw_core/schemas"), ], ) aw_watcher_afk_a = build_analysis( "aw_watcher_afk", awa_location, hiddenimports=[ "Xlib.keysymdef.miscellany", "Xlib.keysymdef.latin1", "Xlib.keysymdef.latin2", "Xlib.keysymdef.latin3", "Xlib.keysymdef.latin4", "Xlib.keysymdef.greek", "Xlib.support.unix_connect", "Xlib.ext.shape", "Xlib.ext.xinerama", "Xlib.ext.composite", "Xlib.ext.randr", "Xlib.ext.xfixes", "Xlib.ext.security", "Xlib.ext.xinput", "pynput.keyboard._xorg", "pynput.mouse._xorg", "pynput.keyboard._win32", "pynput.mouse._win32", "pynput.keyboard._darwin", "pynput.mouse._darwin", ], ) aw_watcher_input_a = build_analysis("aw_watcher_input", awi_location) aw_watcher_window_a = build_analysis( "aw_watcher_window", aww_location, binaries=( [ ( aww_location / "aw_watcher_window/aw-watcher-window-macos", "aw_watcher_window", ) ] if platform.system() == "Darwin" else [] ), datas=[ (aww_location / "aw_watcher_window/printAppStatus.jxa", "aw_watcher_window") ], ) # Check if aw-notify is a Python package _notify_candidates = [ aw_notify_location / "aw_notify/__main__.py", aw_notify_location / "src/aw_notify/__main__.py", ] skip_aw_notify = not any(p.exists() for p in _notify_candidates) if skip_aw_notify: print("Skipping aw-notify Python packaging (Rust-based implementation detected)") aw_notify_a = None if skip_aw_notify else build_analysis( "aw_notify", aw_notify_location, hiddenimports=["desktop_notifier.resources"] ) # https://pythonhosted.org/PyInstaller/spec-files.html#multipackage-bundles # MERGE takes a bit weird arguments, it wants tuples which consists of # the analysis paired with the script name and the bin name merge_args = [ (aw_server_a, "aw-server", "aw-server"), (aw_qt_a, "aw-qt", "aw-qt"), (aw_watcher_afk_a, "aw-watcher-afk", "aw-watcher-afk"), (aw_watcher_window_a, "aw-watcher-window", "aw-watcher-window"), (aw_watcher_input_a, "aw-watcher-input", "aw-watcher-input"), ] if aw_notify_a is not None: merge_args.append((aw_notify_a, "aw-notify", "aw-notify")) MERGE(*merge_args) # aw-server aws_coll = build_collect(aw_server_a, "aw-server") # aw-watcher-window aww_coll = build_collect(aw_watcher_window_a, "aw-watcher-window") # aw-watcher-afk awa_coll = build_collect(aw_watcher_afk_a, "aw-watcher-afk") # aw-qt awq_coll = build_collect( aw_qt_a, "aw-qt", console=False if platform.system() == "Windows" else True, entitlements=app_entitlements_file, ) # aw-watcher-input awi_coll = build_collect(aw_watcher_input_a, "aw-watcher-input") # aw-notify (only if Python package exists) aw_notify_coll = build_collect(aw_notify_a, "aw-notify") if aw_notify_a is not None else None if platform.system() == "Darwin": bundle_args = [ awq_coll, aws_coll, aww_coll, awa_coll, awi_coll, ] if aw_notify_coll is not None: bundle_args.append(aw_notify_coll) app = BUNDLE( *bundle_args, name="ActivityWatch.app", icon=icon, bundle_identifier="net.activitywatch.ActivityWatch", version=current_release.lstrip("v"), info_plist={ "NSPrincipalClass": "NSApplication", "CFBundleExecutable": "MacOS/aw-qt", "CFBundleIconFile": "logo.icns", "NSAppleEventsUsageDescription": "Please grant access to use Apple Events", # This could be set to a more specific version string (including the commit id, for example) "CFBundleVersion": current_release.lstrip("v"), # Replaced by the 'version' kwarg above # "CFBundleShortVersionString": current_release.lstrip('v'), }, ) # BUNDLE inherits signing settings from the last COLLECT and deep-signs the # assembled bundle with them. Re-sign the application layer so its responsible # executable retains app-only capabilities without propagating them to helpers. osxutils.sign_binary(app.name, codesign_identity, app_entitlements_file)