Skip to content

Make packaged macOS liblsl self-contained (afterPack hook)#221

Merged
jdpigeon merged 1 commit into
mainfrom
lsl-packaging-selfcontained
Jul 6, 2026
Merged

Make packaged macOS liblsl self-contained (afterPack hook)#221
jdpigeon merged 1 commit into
mainfrom
lsl-packaging-selfcontained

Conversation

@jdpigeon

@jdpigeon jdpigeon commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

The problem

The packaged macOS app can't load liblsl on a clean machine — item 6 of the LSL test plan fails even on arm64, and not for the documented reason (ASAR paths; asarUnpack already handles that).

patchDeps.mjs symlinks the Homebrew liblsl in for Apple Silicon. electron-builder dereferences the symlink at package time, so the arm64 liblsl.dylib lands as a real file — but the Homebrew build dynamically links /opt/homebrew/opt/pugixml/lib/libpugixml.1.dylib, an absolute path that doesn't exist on a user's Mac. dlopen(liblsl) then fails and LSL silently no-ops.

Found via otool -L on a packaged build (see PR discussion).

The fix

internals/scripts/afterPack.mjs (wired via build.afterPack) walks liblsl's otool -L deps, copies each external (/opt/homebrew, /usr/local, /opt/local) dylib into the unpacked bundle next to liblsl, rewrites the references to @loader_path, and re-signs ad-hoc (install_name_tool invalidates the signature, and arm64 dlopen requires a valid one). Recursive + arch-agnostic.

Verification (arm64 --dir build)

  • otool -L on packaged liblsl.dylib: only @loader_path/* + /usr/lib/*zero /opt/homebrew refs
  • libpugixml.1.dylib bundled alongside, its own id rewritten to @loader_path
  • codesign --verify passes on both dylibs
  • process.dlopen resolves all deps (fails with "Module did not self-register", i.e. reached the loader, not "Library not loaded: /opt/homebrew/…")

Not covered (tracked in TODOS — need the build machines)

  • macOS x64 (Intel) — the hook is arch-agnostic and will bundle whatever deps the x86_64 dylib has; needs an Intel build + otool -L confirm
  • Windows x64 — bundled lsl_amd64.dll is self-contained PE; needs a Windows build to confirm koffi loads it
  • Linux — bundled .so is x86_64 ELF; ldd on Ubuntu + Web-BT smoke

🤖 Generated with Claude Code

Comment thread internals/scripts/afterPack.mjs Outdated
* the dylib resolves its deps from its own directory. Re-signs ad-hoc afterward
* because install_name_tool invalidates the signature.
*
* ponytail: handles the current liblsl→pugixml case; the recursion covers deeper

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove ponytail oparagraph

Comment thread internals/scripts/afterPack.mjs Outdated
* ponytail: handles the current liblsl→pugixml case; the recursion covers deeper
* chains if a future liblsl build adds them. macOS-only (uses install_name_tool
* / codesign). Couples packaging to the build machine having the Homebrew deps —
* same assumption patchDeps already makes for dev.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add section: "Remove when this is true"

Comment thread TODOS.md Outdated
- [ ] **Lesson content from Steve Azeka** — owner: Dano. Set a date for the first sample lesson. Fallback: if no lesson by that date, Dano authors one throwaway stub to unblock the lesson surface.
- [ ] **In-app lesson surface v1** — static markdown render only (no editing, no interactivity). Build against a dummy lesson; tune to real content once a sample lands. Confirm markdown is a rich-enough format with Azeka before building.
- [ ] **Land LSL** — merge `device-lsl` to `main` and QA it. Muse-first, Mac + Windows.
- [ ] **Land LSL** — `device-lsl` merged (PR #204). Remaining: packaging verification per platform.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done

The Homebrew liblsl that patchDeps symlinks in for Apple Silicon is not
self-contained: it dynamically links /opt/homebrew/opt/pugixml/lib/
libpugixml.1.dylib. electron-builder dereferences the symlink so the arm64
liblsl.dylib lands in the app as a real file, but that pugixml path doesn't
exist on a user's Mac — so dlopen(liblsl) fails and LSL silently no-ops in
distributed builds. (The docs flagged ASAR path breakage as the packaging
risk; asarUnpack handles that. This transitive-dep breakage is the actual
blocker, surfaced by otool -L on a packaged build.)

internals/scripts/afterPack.mjs (wired via build.afterPack) walks liblsl's
otool -L deps, copies each external (/opt/homebrew, /usr/local, /opt/local)
dylib next to it in app.asar.unpacked/.../prebuild/, rewrites the references
to @loader_path, and re-signs ad-hoc (install_name_tool invalidates the
signature, and arm64 dlopen requires a valid one). Recursive + arch-agnostic,
so an x64 build or a future liblsl with deeper deps is handled too.

Verified on a packaged arm64 --dir build: otool -L shows zero /opt/homebrew
refs (only @loader_path + /usr/lib), codesign --verify passes on both dylibs,
and process.dlopen resolves all deps (fails with "Module did not self-register"
rather than "Library not loaded: /opt/homebrew/..."). x64-mac / Windows / Linux
packaging verification tracked in TODOS (need those build machines).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNEh5EFg4dSpRrfJ2ae4vL
@jdpigeon jdpigeon force-pushed the lsl-packaging-selfcontained branch from 1daa3e2 to de75e05 Compare July 6, 2026 23:16
@jdpigeon

jdpigeon commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Addressed all three comments and force-pushed (1daa3e2de75e05):

  • afterPack.mjs:19 — remove ponytail paragraph ✅ Removed. Folded the still-relevant bits (recursive scan handles deeper chains; macOS-only; couples packaging to the build machine's Homebrew deps) into the preceding paragraph.
  • afterPack.mjs:22 — add "Remove when this is true" ✅ Added a section: remove this hook + its build.afterPack wiring once node-labstreaminglayer ships (or we vendor) a self-contained arm64 liblsl with no /opt/homebrew deps — verify with otool -L first.
  • TODOS.md:9 — this is done ✅ Moved Land LSL to "Done recently" (dated). The remaining per-platform packaging checks (x64 mac / Windows / Linux) are broken out as a smaller "Cross-platform LSL packaging verification" item so they aren't lost.

@jdpigeon jdpigeon merged commit 75f9e09 into main Jul 6, 2026
10 checks passed
@jdpigeon jdpigeon deleted the lsl-packaging-selfcontained branch July 6, 2026 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant