Skip to content

feat: node-gtk bundle for Windows and macOS#492

Open
romgrk wants to merge 2 commits into
masterfrom
bundle-win-mac
Open

feat: node-gtk bundle for Windows and macOS#492
romgrk wants to merge 2 commits into
masterfrom
bundle-win-mac

Conversation

@romgrk

@romgrk romgrk commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Completes the portable-bundle story across all three desktop platforms (Linux shipped in #489; this is the HANDOFF.md mission, now retired).

Windows — tools/bundle/platform-win32.js

A JS port of the CI-proven scripts/windows-bundle-runtime.sh (the logic behind the self-contained npm prebuilt), as a superset that also ships node.exe + the app tree + a launcher:

  • MinGW prefix via cygpath -m $MINGW_PREFIX, with a clear "run from MINGW64" error outside MSYS2. The bundle has no MSYS2 requirement — only the bundling machine.
  • ntldd -R transitive DLL closure seeded from the addon + seeds.js + pixbuf loaders; only prefix-internal DLLs ship (System32 resolves on the host).
  • loaders.cache rewritten to bare DLL names; the loaders dir goes on PATH (same trick as lib/native.js).
  • CRLF <Name>.cmd launcher: PATH, GI_TYPELIB_PATH, XDG_DATA_DIRS, GSETTINGS_SCHEMA_DIR, GDK_PIXBUF_MODULE_FILE, cd into app/, run runtime\node.exe.
  • --archiveCompress-Archive zip.

macOS — tools/bundle/platform-darwin.js

Two-layer relocation of the Homebrew GTK stack into a <Name>.app:

  1. Transitive otool -L closure (resolving @loader_path/@rpath), copied flat into runtime/lib; every copied Mach-O rewritten with install_name_tool -change to @loader_path-relative names (no -add_rpath — needs header padding; -change to a shorter string always fits) and ad-hoc re-signed (mandatory on arm64).
  2. The launcher exports DYLD_FALLBACK_LIBRARY_PATH=$RT/lib:… inside the script (SIP-safe) — this is what resolves the absolute Homebrew dylib paths baked into typelibs on machines without brew, and any load command a -change couldn't rewrite.

Plus minimal Info.plist, hdiutil UDZO .dmg archive, strip -x + re-sign for node. Output is per-arch and ad-hoc-signed; Developer ID + notarization for real distribution is documented as out of scope.

Shared changes

  • copyTypelibs/copyRuntimeData/pkgConfigVar hoisted from platform-linux.js into tools/bundle/runtime-data.js.
  • Node stripping is now a per-platform stripNode hook (GNU --strip-unneeded vs Apple -x+re-sign vs none for MSVC node.exe).
  • app-tree.js fix: the binding dir is trimmed to node_gtk.node only. The self-contained Windows prebuilt keeps its whole GTK runtime (~250MB of DLLs/typelibs/icons) next to the addon, which would otherwise be duplicated wholesale into app/node_modules.

CI

  • build-windows: bundle smoke test on node 24, placed after the prebuilt-bundling step deliberately, so the binding-dir trim is exercised.
  • build: macOS leg now runs the smoke test too (runner has a window server; brew gtk3 + the fixture's Gtk4→Gtk3 fallback).
  • scripts/bundle-smoke-test.js platform gate removed — it already asserts the app ran under the bundled node on every OS.

Honest-caveat follow-up (documented in the roadmap): the macOS runner has Homebrew, so the DYLD-fallback path isn't truly exercised in CI; a clean-machine consume job mirroring test-windows-prebuilt.yaml is future work.

Docs

doc/bundling.md: "Linux today" banner replaced with per-platform notes (bundling-machine requirements, launcher wiring, archive formats, macOS distribution reality); roadmap updated. README updated. HANDOFF.md deleted — mission complete.

Validated locally on Linux (bundle smoke + flatpak generation smoke both PASS); Windows/macOS validate through the new CI steps.

🤖 Generated with Claude Code

romgrk and others added 2 commits July 5, 2026 14:10
Implements the two remaining platform modules behind the bundle
interface (HANDOFF.md mission; Linux shipped in #489):

- platform-win32.js: port of the CI-proven windows-bundle-runtime.sh —
  ntldd -R DLL closure from the MinGW prefix (System32 stays on the
  host), typelibs, pixbuf loaders with bare-name cache rewrite, CRLF
  .cmd launcher wiring PATH/GI/GLib env, Compress-Archive zip. Requires
  an MSYS2 MINGW64 shell at bundle time only.
- platform-darwin.js: otool -L transitive closure from Homebrew
  (@loader_path/@rpath resolution), install_name_tool -change to
  @loader_path-relative names + ad-hoc re-sign (mandatory on arm64, no
  -add_rpath), DYLD_FALLBACK_LIBRARY_PATH launcher as the safety net
  for typelib-baked absolute paths, .app/Info.plist layout, hdiutil
  UDZO dmg, strip -x + re-sign for node.
- shared: copyTypelibs/copyRuntimeData/pkgConfigVar hoisted into
  runtime-data.js; node stripping is now a per-platform stripNode hook;
  app-tree trims the binding dir to node_gtk.node only (the Windows
  self-contained prebuilt carries a whole GTK runtime there — 250MB
  that must not be duplicated into app/).
- CI: bundle smoke test on build-windows (after the prebuilt step, to
  exercise the binding-dir trim) and on the macOS leg; the smoke
  test's platform gate is gone.
- docs: per-platform notes in doc/bundling.md (incl. macOS
  signing/notarization caveat), roadmap updated, HANDOFF.md retired.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Homebrew installs Cellar files read-only (0444). copyFile preserves the
source mode, so a) the pixbuf loaders dir's multiple symlink entries for
one Cellar file made the second copy EACCES on the read-only first one,
and b) install_name_tool/codesign would have failed to rewrite every
copied dylib in place. Copy libraries with 0755 and dedupe the
realpath'd loader list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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