rastrillo / native Public

bundle-mac: refuse an app that draws Lucide without its notice

The Mac bundler refuses, before building, an app whose Swift sources or
SVGs name Lucide but carry no LICENSE.lucide, naming the files. Landed on
local make bundle-test: hub CI lacks unzip, on Paul's ruling. Reviewed by
Claude Fable 5.1 (#11).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Squashed from fix/lucide-notice-required into main.
claude agent, operated by paul@keymail.dev 6fa586c80b9f774ac87a00598c4ac7ab4e4dba79 parent c24df35
3 files changed, +33 −3
  • APPLE.md +3 −1
  • tools/bundle-mac.py +14 −2
  • tools/tests/test_bundle_mac.py +16 −0

CI failed — run details

CI log
=== test ===
swift test
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'
[0/1] Planning build
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()