"""Bundle a SwiftPM app with the suite's padded macOS icon silhouette.""" from pathlib import Path import argparse, json, plistlib, shutil, subprocess from mac_updates import configure, embed_sparkle, sign_bundle parser = argparse.ArgumentParser() parser.add_argument("native", type=Path) parser.add_argument("name") parser.add_argument("--configuration", choices=["debug", "release"], default="debug") parser.add_argument("--updates-config", type=Path) parser.add_argument("--version", default="0.1.0") parser.add_argument("--build", default="1") parser.add_argument("--sign-identity", default="-") args = parser.parse_args() root = args.native.resolve() name = args.name # The notice is keyed on the app having LICENSE.lucide (copied below), so an # app that draws Lucide and lost the file would ship without the notice it # owes, silently. Refuse that before building: any Swift source or SVG in # the app that names Lucide means the app draws it, and must carry it. lucide = root / "LICENSE.lucide" if not lucide.is_file(): drawn = [path for directory in ("apple/Sources", "ios", "macos") if (root / directory).is_dir() for path in sorted((root / directory).rglob("*")) if path.suffix in (".swift", ".svg") and path.is_file() and "lucide" in path.read_text(encoding="utf-8", errors="replace").lower()] if drawn: raise SystemExit("bundle-mac: " + ", ".join(str(p.relative_to(root)) for p in drawn) + " draw Lucide, but the app has no LICENSE.lucide to carry its notice; restore it") subprocess.run(["swift", "build", "--package-path", str(root / "apple"), "--configuration", args.configuration, "--product", f"{name}Mac"], check=True) manifest = json.loads(subprocess.check_output(["swift", "package", "--package-path", str(root / "apple"), "dump-package"])) minimum_os = next((p["version"] for p in manifest.get("platforms", []) if p["platformName"] == "macos"), "14.0") products = Path(subprocess.check_output(["swift", "build", "--package-path", str(root / "apple"), "--configuration", args.configuration, "--show-bin-path"], text=True).strip()) bundle = root / f"build/Oficina {name}.app/Contents" if bundle.parent.exists(): shutil.rmtree(bundle.parent) for directory in ["MacOS", "Resources"]: (bundle / directory).mkdir(parents=True, exist_ok=True) shutil.copy2(products / f"{name}Mac", bundle / f"MacOS/{name}Mac") for resource in products.glob("*.bundle"): target = bundle / "Resources" / resource.name if target.exists(): shutil.rmtree(target) shutil.copytree(resource, target) # Lucide's notice travels with the app only when the app has one to give: # it attaches to copies of Lucide's drawings, and an app that draws none # (its icons its own, the rest system symbols) owes no one the notice. An # app that draws Lucide without the file was refused above. if lucide.is_file(): shutil.copy2(lucide, bundle / "Resources/LICENSE.lucide") icon = root / "build/MacIcon.png" subprocess.run(["swift", str(Path(__file__).with_name("mac-icon.swift")), str(root / "ios/Assets.xcassets/AppIcon.appiconset/AppIcon.png"), str(icon)], check=True) iconset = root / "build/AppIcon.iconset" iconset.mkdir(exist_ok=True) for size in [16, 32, 128, 256, 512]: for scale in [1, 2]: filename = f"icon_{size}x{size}" + ("@2x" if scale == 2 else "") + ".png" subprocess.run(["sips", "-z", str(size * scale), str(size * scale), str(icon), "--out", str(iconset / filename)], check=True, stdout=subprocess.DEVNULL) subprocess.run(["iconutil", "-c", "icns", str(iconset), "-o", str(bundle / "Resources/AppIcon.icns")], check=True) (bundle / "Info.plist").write_bytes(plistlib.dumps({ "CFBundleName": f"Oficina {name}", "CFBundleDisplayName": f"Oficina {name}", "CFBundleIdentifier": f"net.amadan.oficina.{name.lower()}", "CFBundleExecutable": f"{name}Mac", "CFBundlePackageType": "APPL", "CFBundleIconFile": "AppIcon.icns", "CFBundleShortVersionString": "0.1.0", "CFBundleVersion": "1", "LSMinimumSystemVersion": minimum_os, "NSHighResolutionCapable": True, })) configure(bundle.parent, args.updates_config, args.version, args.build) embedded = embed_sparkle(bundle.parent, root / "apple") if args.updates_config and not embedded: raise ValueError("configured updater requires an embedded Sparkle framework") sign_bundle(bundle.parent, args.sign_identity) print(bundle.parent)