#!/usr/bin/env python3 """Packs builds into the files people download, in dist/: Fry3DX--linux-x64.tar.gz from build/portable (cmake --preset portable) Fry3DX--linux-x64.AppImage the same, as one file (needs appimagetool: $APPIMAGETOOL or on PATH) Fry3DX--windows-x64.zip from build/windows, or build/ given with --windows-bin Fry3DX--macos.zip from build/macos-arm64 and build/macos-x64 (on a Mac): one Fry3DX.app for Apple silicon and Intel python3 tools/package.py linux|appimage|windows|macos [--bin DIR] Each unpacks to one Fry3DX/ folder: the program, its content (engine/, Fry3DX/; on a Mac inside the app), LICENSE and a README.txt. (The Android APK: android/build_apk.py.) """ import json import os import shutil import subprocess import sys import tarfile import zipfile ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) DIST = os.path.join(ROOT, "dist") README = """Fry3DX {version} - Frying 3D Extreme ===================================== A 3D modeller that does what you'd expect: pick a tool, click, drag. Made by Nexoniarz - https://nexoniarz.github.io/Fry3DX/ Start: {start} Needs {needs}. F1 in the app lists every key. Your settings and autosaves: {prefs} Open source under the Apache License 2.0 (LICENSE). Sky pictures: Poly Haven, CC0. """ def version(): with open(os.path.join(ROOT, "project.json")) as f: # (the version and the rest live there) return json.load(f)["version"] PC_NEEDS = "a graphics card with Vulkan (or OpenGL 4.6)" def readme(folder, start, prefs, needs=PC_NEEDS, crlf=False): shutil.copy2(os.path.join(ROOT, "LICENSE"), folder) with open(os.path.join(folder, "README.txt"), "w", newline="\r\n" if crlf else "\n") as f: f.write(README.format(version=version(), start=start, prefs=prefs, needs=needs)) def stage(bin_dir, exe, extra, start, prefs): out = os.path.join(DIST, "stage", "Fry3DX") shutil.rmtree(os.path.dirname(out), ignore_errors=True) os.makedirs(out) for name in [exe, *extra]: shutil.copy2(os.path.join(bin_dir, name), out) for d in ("engine", "Fry3DX"): shutil.copytree(os.path.join(bin_dir, d), os.path.join(out, d)) readme(out, start, prefs, crlf=exe.endswith(".exe")) return out def linux(bin_dir): out = stage(bin_dir, "fry3dx", [], "./fry3dx (or double-click it)", "~/.local/share/Nexoniarz/Fry3DX") subprocess.run(["strip", os.path.join(out, "fry3dx")], check=True) path = os.path.join(DIST, f"Fry3DX-{version()}-linux-x64.tar.gz") with tarfile.open(path, "w:gz") as t: t.add(out, "Fry3DX") return path def appimage(bin_dir): tool = os.environ.get("APPIMAGETOOL") or shutil.which("appimagetool") if not tool: sys.exit("No appimagetool (set APPIMAGETOOL)") out = stage(bin_dir, "fry3dx", [], "the AppImage itself", "~/.local/share/Nexoniarz/Fry3DX") app = os.path.join(DIST, "stage", "Fry3DX.AppDir") # The program and its content where it looks for them (next to it), in usr/bin. shutil.copytree(out, os.path.join(app, "usr", "bin")) subprocess.run(["strip", os.path.join(app, "usr", "bin", "fry3dx")], check=True) os.symlink("usr/bin/fry3dx", os.path.join(app, "AppRun")) shutil.copy2(os.path.join(ROOT, "branding", "logo.png"), os.path.join(app, "fry3dx.png")) with open(os.path.join(app, "fry3dx.desktop"), "w") as f: f.write("[Desktop Entry]\nType=Application\nName=Fry3DX\nComment=A 3D modeller that does what you'd expect\n" "Exec=fry3dx %f\nIcon=fry3dx\nCategories=Graphics;3DGraphics;\nTerminal=false\n") path = os.path.join(DIST, f"Fry3DX-{version()}-linux-x64.AppImage") subprocess.run([tool, "--no-appstream", app, path], check=True, env={**os.environ, "ARCH": "x86_64"}) return path def windows(bin_dir): out = stage(bin_dir, "fry3dx.exe", ["SDL3.dll"], "fry3dx.exe", "%APPDATA%\\Nexoniarz\\Fry3DX") path = os.path.join(DIST, f"Fry3DX-{version()}-windows-x64.zip") with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z: for r, _, names in os.walk(out): for n in names: full = os.path.join(r, n) z.write(full, os.path.join("Fry3DX", os.path.relpath(full, out))) return path def macos(bin_dir): """One Fry3DX.app for both kinds of Mac: the Apple silicon build's app, its program joined (lipo) with the Intel build's; signed ad hoc (no Apple account: enough to run, with a right-click Open the first time).""" arm = os.path.join(bin_dir, "Fry3DX.app") intel = os.path.join(ROOT, "build", "macos-x64", "bin", "Fry3DX.app") out = os.path.join(DIST, "stage", "Fry3DX") shutil.rmtree(os.path.dirname(out), ignore_errors=True) os.makedirs(out) app = os.path.join(out, "Fry3DX.app") shutil.copytree(arm, app, symlinks=True) exe = os.path.join("Contents", "MacOS", "Fry3DX") if os.path.exists(os.path.join(intel, exe)): subprocess.run(["lipo", "-create", os.path.join(arm, exe), os.path.join(intel, exe), "-output", os.path.join(app, exe)], check=True) kind = "macos" else: print("(no build/macos-x64: Apple silicon only)") kind = "macos-arm64" subprocess.run(["strip", "-x", os.path.join(app, exe)], check=True) subprocess.run(["codesign", "--force", "--deep", "--sign", "-", app], check=True) readme(out, "Fry3DX.app (move it to Applications if you like). The first time: right-click it, Open,\n" " as it isn't from the App Store", "~/Library/Application Support/Nexoniarz/Fry3DX", "macOS 13.3 or newer; Vulkan through MoltenVK (inside the app), else OpenGL 4.1") path = os.path.join(DIST, f"Fry3DX-{version()}-{kind}.zip") # (ditto keeps what a Mac app needs: links, permissions, the signature) subprocess.run(["ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", out, path], check=True) return path def main(): args = sys.argv[1:] bin_dir = None if "--bin" in args: i = args.index("--bin") bin_dir = os.path.abspath(args[i + 1]) del args[i:i + 2] os.makedirs(DIST, exist_ok=True) for what in args: default = {"linux": "build/portable/bin", "appimage": "build/portable/bin", "windows": "build/windows/bin", "macos": "build/macos-arm64/bin"}[what] path = {"linux": linux, "appimage": appimage, "windows": windows, "macos": macos}[what](bin_dir or os.path.join(ROOT, default)) print(f"{os.path.relpath(path, ROOT)}: {os.path.getsize(path) / 1048576:.1f} MB") shutil.rmtree(os.path.join(DIST, "stage"), ignore_errors=True) if __name__ == "__main__": main()