bundle-mac: refuse an app that draws Lucide but has lost its notice
Since c24df35 the notice is copied only when the app has LICENSE.lucide, so an app that draws Lucide and loses the file would ship without the notice it owes, and nothing would say so (Claude Fable 5.1 on #10). Now bundle-mac refuses before building when any Swift source or SVG under apple/Sources, ios or macos names Lucide and the file is missing, naming those files. APPLE.md says the notice is bundled when the app has it. Docs, Calls and Memoria: none of their sources names Lucide, so none is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+33
−3
APPLE.md+3 −1tools/bundle-mac.py+14 −2tools/tests/test_bundle_mac.py+16 −0
CI failed — run details
CI log
=== test === swift test Fetching https://github.com/sparkle-project/Sparkle from cache Fetched https://github.com/sparkle-project/Sparkle from cache (0.55s) Computing version for https://github.com/sparkle-project/Sparkle Computed https://github.com/sparkle-project/Sparkle at 2.9.6 (0.83s) Creating working copy for https://github.com/sparkle-project/Sparkle Working copy of https://github.com/sparkle-project/Sparkle resolved at 2.9.6 Fetching binary artifact https://github.com/sparkle-project/Sparkle/releases/download/2.9.6/Sparkle-for-Swift-Package-Manager.zip from cache error: failed validating archive from 'https://github.com/sparkle-project/Sparkle/releases/download/2.9.6/Sparkle-for-Swift-Package-Manager.zip' which is required by binary target 'Sparkle': could not find executable for 'unzip' error: fatalError make: *** [Makefile:4: test] Error 1 amadan: step test failed: exit status 2 amadan: job failed at step test — its output is above, not at the tail of this log
diff --git a/APPLE.md b/APPLE.md| index 32c1319..6122d26 100644 |
| --- a/APPLE.md |
| +++ b/APPLE.md |
| @@ -15,7 +15,9 @@ There is no cross-origin cookie jar, HTTP cache or external redirect following. |
| `tools/bundle-mac.py <native-directory> <Name>` builds a SwiftPM executable |
| `<Name>Mac`, derives a padded rounded Mac icon from the iOS master, bundles the |
| -Lucide notice, and signs the development app ad hoc. It does not notarize it. |
| +Lucide notice when the app has `LICENSE.lucide` (and refuses to bundle an app |
| +whose Swift sources or SVGs name Lucide without it), and signs the development |
| +app ad hoc. It does not notarize it. |
| Run `make ci` for Swift unit tests, the existing Mac/iOS companion builds, |
| and Go authentication tests with the race detector. |
diff --git a/tools/bundle-mac.py b/tools/bundle-mac.py| index ea99032..69c78d2 100644 |
| --- a/tools/bundle-mac.py |
| +++ b/tools/bundle-mac.py |
| @@ -13,6 +13,18 @@ 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") |
| @@ -30,8 +42,8 @@ for resource in products.glob("*.bundle"): |
| 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. |
| -lucide = root / "LICENSE.lucide" |
| +# (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" |
diff --git a/tools/tests/test_bundle_mac.py b/tools/tests/test_bundle_mac.py| index 2e6f7f9..f1010fd 100644 |
| --- a/tools/tests/test_bundle_mac.py |
| +++ b/tools/tests/test_bundle_mac.py |
| @@ -63,6 +63,22 @@ print(text) |
| self.assertEqual(output.strip(), "portable native resources") |
| subprocess.run(["codesign", "--verify", "--strict", "--deep", str(app)], check=True) |
| + def test_an_app_that_draws_lucide_cannot_lose_its_notice(self): |
| + """A notice owed and missing stops the bundle rather than shipping without it.""" |
| + tool = Path(__file__).resolve().parents[1] / "bundle-mac.py" |
| + with tempfile.TemporaryDirectory(prefix="native-bundle-lucide-") as temporary: |
| + root = Path(temporary) |
| + source = root / "apple/Sources/Probe" |
| + source.mkdir(parents=True) |
| + (root / "apple/Package.swift").write_text("// swift-tools-version: 5.9\n") |
| + # A Lucide drawing in the app's sources, and no LICENSE.lucide. |
| + (source / "Icons.swift").write_text("// Path drawn from Lucide's \"file-text\" icon.\nlet x = 1\n") |
| + result = subprocess.run(["python3", str(tool), str(root), "Probe"], capture_output=True, text=True) |
| + self.assertNotEqual(result.returncode, 0) |
| + self.assertIn("LICENSE.lucide", result.stderr) |
| + self.assertIn("Icons.swift", result.stderr) |
| + self.assertFalse((root / "build").exists(), "nothing is built before the refusal") |
| + |
| if __name__ == "__main__": |
| unittest.main() |