"""Build the self-contained Unheard City for the current platform (Windows or Linux). python tools/build_app.py [--art FOLDER] -> dist/UnheardCity/ and dist/UnheardCity--.zip|.tar.gz One command does everything: artwork (app/assets, committed; remade from ~/Downloads/Arkham City when missing, or from FOLDER with --art), PyInstaller bundle, THIRD_PARTY_LICENSES.txt, docs, archive. Requirements: the dev environment (`pip install -e .[app,dev]`). All native libraries come from the pinned PyPI wheels (Qt, libsndfile with libvorbis/FLAC/opus/mpg123, soxr); no external programs are bundled or needed (see docs/decisions.md). Build on each target OS (PyInstaller does not cross-compile). """ import os import platform import shutil import sys import tarfile ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, ROOT) sys.path.insert(0, os.path.join(ROOT, "tools")) NAME = "UnheardCity" EXCLUDE = [ "tkinter", "matplotlib", "scipy", "pandas", "IPython", "pytest", "soundcard", "ackit.verify", "PySide6.QtWebEngineCore", "PySide6.QtWebEngineWidgets", "PySide6.QtWebEngineQuick", "PySide6.QtWebChannel", "PySide6.QtWebSockets", "PySide6.QtQml", "PySide6.QtQuick", "PySide6.QtQuickWidgets", "PySide6.QtQuick3D", "PySide6.Qt3DCore", "PySide6.Qt3DRender", "PySide6.QtCharts", "PySide6.QtDataVisualization", "PySide6.QtGraphs", "PySide6.QtPdf", "PySide6.QtPdfWidgets", "PySide6.QtSql", "PySide6.QtBluetooth", "PySide6.QtNfc", "PySide6.QtPositioning", "PySide6.QtLocation", "PySide6.QtSensors", "PySide6.QtSerialPort", "PySide6.QtDesigner", "PySide6.QtHelp", "PySide6.QtNetworkAuth", "PySide6.QtRemoteObjects", "PySide6.QtScxml", "PySide6.QtSpatialAudio", "PySide6.QtTextToSpeech", "PySide6.QtHttpServer", "PySide6.QtVirtualKeyboard", "PySide6.QtTest", ] def main(): from ackit import __version__ os.chdir(ROOT) assets = os.path.join(ROOT, "app", "assets") art = sys.argv[sys.argv.index("--art") + 1] if "--art" in sys.argv else None ico = os.path.join(assets, "app_icon.ico") if art or not os.path.exists(ico): import make_assets src = art or make_assets.DEFAULT_SRC if os.path.exists(os.path.join(src, "bac_logo_full.png")): make_assets.main(src) else: print("NOTE: no artwork in %s - building with the plain icon and no wallpaper" % src) sep = os.pathsep args = [os.path.join("tools", "editor_launcher.py"), "--name", NAME, "--noconfirm", "--clean", "--onedir", "--windowed", "--distpath", "dist", "--workpath", os.path.join("build", "pyinstaller"), "--specpath", os.path.join("build", "pyinstaller"), "--add-data", os.path.join(ROOT, "ackit", "data") + sep + "ackit/data", "--add-data", os.path.join(ROOT, "data", "catalog") + sep + "data/catalog", "--paths", ROOT] if os.path.isdir(assets): args += ["--add-data", assets + sep + "app/assets"] if sys.platform == "win32" and os.path.exists(os.path.join(assets, "app_icon.ico")): args += ["--icon", os.path.join(assets, "app_icon.ico")] for m in EXCLUDE: args += ["--exclude-module", m] import PyInstaller.__main__ PyInstaller.__main__.run(args) out = os.path.join("dist", NAME) import collect_licenses collect_licenses.main(os.path.join(out, "THIRD_PARTY_LICENSES.txt")) shutil.copy("README.md", os.path.join(out, "README.md")) shutil.copy("LICENSE", os.path.join(out, "LICENSE")) docs = os.path.join(out, "docs") os.makedirs(docs, exist_ok=True) for f in ("user-guide.md",): if os.path.exists(os.path.join("docs", f)): shutil.copy(os.path.join("docs", f), docs) if os.path.isdir(os.path.join("docs", "img")): shutil.copytree(os.path.join("docs", "img"), os.path.join(docs, "img"), dirs_exist_ok=True) if sys.platform == "win32": # per-user installer: Install.cmd -> install.ps1 (+ uninstall.ps1) for f in ("Install.cmd", "install.ps1", "uninstall.ps1"): text = open(os.path.join("tools", "installer", f), encoding="utf-8").read().replace("@VERSION@", __version__) with open(os.path.join(out, f), "w", encoding="ascii", newline="\r\n") as fh: fh.write(text) plat = {"win32": "windows", "linux": "linux"}.get(sys.platform, sys.platform) base = os.path.join("dist", "%s-%s-%s-%s" % (NAME, __version__, plat, platform.machine().lower())) if sys.platform == "win32": shutil.make_archive(base, "zip", "dist", NAME) print("built", base + ".zip") else: with tarfile.open(base + ".tar.gz", "w:gz") as t: t.add(out, arcname=NAME) print("built", base + ".tar.gz") if __name__ == "__main__": main()