Files
UnheardCity/tools/build_app.py
2026-10-09 02:57:04 +02:00

94 lines
4.6 KiB
Python

"""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-<ver>-<os>.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"))
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()