| 1 | """Bundle a SwiftPM app with the suite's padded macOS icon silhouette.""" |
| 2 | from pathlib import Path |
| 3 | import argparse, json, plistlib, shutil, subprocess |
| 4 | from mac_updates import configure, embed_sparkle, sign_bundle |
| 5 | parser = argparse.ArgumentParser() |
| 6 | parser.add_argument("native", type=Path) |
| 7 | parser.add_argument("name") |
| 8 | parser.add_argument("--configuration", choices=["debug", "release"], default="debug") |
| 9 | parser.add_argument("--updates-config", type=Path) |
| 10 | parser.add_argument("--version", default="0.1.0") |
| 11 | parser.add_argument("--build", default="1") |
| 12 | parser.add_argument("--sign-identity", default="-") |
| 13 | args = parser.parse_args() |
| 14 | root = args.native.resolve() |
| 15 | name = args.name |
| 16 | # The notice is keyed on the app having LICENSE.lucide (copied below), so an |
| 17 | # app that draws Lucide and lost the file would ship without the notice it |
| 18 | # owes, silently. Refuse that before building: any Swift source or SVG in |
| 19 | # the app that names Lucide means the app draws it, and must carry it. |
| 20 | lucide = root / "LICENSE.lucide" |
| 21 | if not lucide.is_file(): |
| 22 | drawn = [path for directory in ("apple/Sources", "ios", "macos") if (root / directory).is_dir() |
| 23 | for path in sorted((root / directory).rglob("*")) if path.suffix in (".swift", ".svg") and path.is_file() |
| 24 | and "lucide" in path.read_text(encoding="utf-8", errors="replace").lower()] |
| 25 | if drawn: |
| 26 | raise SystemExit("bundle-mac: " + ", ".join(str(p.relative_to(root)) for p in drawn) |
| 27 | + " draw Lucide, but the app has no LICENSE.lucide to carry its notice; restore it") |
| 28 | subprocess.run(["swift", "build", "--package-path", str(root / "apple"), "--configuration", args.configuration, "--product", f"{name}Mac"], check=True) |
| 29 | manifest = json.loads(subprocess.check_output(["swift", "package", "--package-path", str(root / "apple"), "dump-package"])) |
| 30 | minimum_os = next((p["version"] for p in manifest.get("platforms", []) if p["platformName"] == "macos"), "14.0") |
| 31 | products = Path(subprocess.check_output(["swift", "build", "--package-path", str(root / "apple"), "--configuration", args.configuration, "--show-bin-path"], text=True).strip()) |
| 32 | bundle = root / f"build/Oficina {name}.app/Contents" |
| 33 | if bundle.parent.exists(): |
| 34 | shutil.rmtree(bundle.parent) |
| 35 | for directory in ["MacOS", "Resources"]: |
| 36 | (bundle / directory).mkdir(parents=True, exist_ok=True) |
| 37 | shutil.copy2(products / f"{name}Mac", bundle / f"MacOS/{name}Mac") |
| 38 | for resource in products.glob("*.bundle"): |
| 39 | target = bundle / "Resources" / resource.name |
| 40 | if target.exists(): |
| 41 | shutil.rmtree(target) |
| 42 | shutil.copytree(resource, target) |
| 43 | # Lucide's notice travels with the app only when the app has one to give: |
| 44 | # it attaches to copies of Lucide's drawings, and an app that draws none |
| 45 | # (its icons its own, the rest system symbols) owes no one the notice. An |
| 46 | # app that draws Lucide without the file was refused above. |
| 47 | if lucide.is_file(): |
| 48 | shutil.copy2(lucide, bundle / "Resources/LICENSE.lucide") |
| 49 | icon = root / "build/MacIcon.png" |
| 50 | subprocess.run(["swift", str(Path(__file__).with_name("mac-icon.swift")), str(root / "ios/Assets.xcassets/AppIcon.appiconset/AppIcon.png"), str(icon)], check=True) |
| 51 | iconset = root / "build/AppIcon.iconset" |
| 52 | iconset.mkdir(exist_ok=True) |
| 53 | for size in [16, 32, 128, 256, 512]: |
| 54 | for scale in [1, 2]: |
| 55 | filename = f"icon_{size}x{size}" + ("@2x" if scale == 2 else "") + ".png" |
| 56 | subprocess.run(["sips", "-z", str(size * scale), str(size * scale), str(icon), "--out", str(iconset / filename)], check=True, stdout=subprocess.DEVNULL) |
| 57 | subprocess.run(["iconutil", "-c", "icns", str(iconset), "-o", str(bundle / "Resources/AppIcon.icns")], check=True) |
| 58 | (bundle / "Info.plist").write_bytes(plistlib.dumps({ |
| 59 | "CFBundleName": f"Oficina {name}", "CFBundleDisplayName": f"Oficina {name}", |
| 60 | "CFBundleIdentifier": f"net.amadan.oficina.{name.lower()}", |
| 61 | "CFBundleExecutable": f"{name}Mac", "CFBundlePackageType": "APPL", |
| 62 | "CFBundleIconFile": "AppIcon.icns", "CFBundleShortVersionString": "0.1.0", |
| 63 | "CFBundleVersion": "1", "LSMinimumSystemVersion": minimum_os, |
| 64 | "NSHighResolutionCapable": True, |
| 65 | })) |
| 66 | configure(bundle.parent, args.updates_config, args.version, args.build) |
| 67 | embedded = embed_sparkle(bundle.parent, root / "apple") |
| 68 | if args.updates_config and not embedded: |
| 69 | raise ValueError("configured updater requires an embedded Sparkle framework") |
| 70 | sign_bundle(bundle.parent, args.sign_identity) |
| 71 | print(bundle.parent) |
| 72 | |