Skip to content

Commit 8438bf1

Browse files
romgrkclaude
andcommitted
docs: HANDOFF.md — Windows/macOS bundle platform-module handoff
Everything the next agent needs to extend `node-gtk bundle` beyond Linux: the platform-module interface to plug into, the ported-from- proven-code plan for Windows (ntldd closure, bare-name loader cache, .cmd launcher — scripts/windows-bundle-runtime.sh is the reference), the two-layer macOS relocation design (otool walk + install_name_tool -change with ad-hoc re-signing, DYLD_FALLBACK_LIBRARY_PATH for the absolute paths brew bakes into typelibs), CI hooks, and the validation bar the Linux implementation was held to. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e9b9c4e commit 8438bf1

1 file changed

Lines changed: 164 additions & 0 deletions

File tree

HANDOFF.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# HANDOFF — `node-gtk bundle` for Windows and macOS
2+
3+
**Mission**: implement the Windows and macOS platform modules for
4+
`node-gtk bundle`, so the portable-bundle story covers all three desktop
5+
platforms. Linux (`bundle` + `flatpak`) shipped in PR #489 (master `e9b9c4e`);
6+
read `doc/bundling.md` first — its Roadmap section is this document's summary.
7+
8+
Everything below was learned or designed while building the Linux path;
9+
Windows/macOS module drafts existed during that work and were validated in
10+
design (not in execution), so treat the specifics as strong hints, not gospel.
11+
12+
## Architecture you are plugging into
13+
14+
`tools/bundle.js` orchestrates; per-OS work lives behind one interface in
15+
`tools/bundle/platform-<os>.js`:
16+
17+
```js
18+
module.exports = {
19+
NODE_BINARY, // 'node' | 'node.exe'
20+
assembleRuntime(ctx), // libraries + typelibs + schemas/icons/loaders → ctx.runtimeDir
21+
writeLauncher(ctx), // returns launcher path
22+
archive(ctx), // .tar.gz / .zip / .dmg
23+
}
24+
```
25+
26+
Register the module in the `PLATFORMS` map in `tools/bundle.js`. Everything
27+
else is shared and already works cross-platform:
28+
29+
- `tools/bundle/app-tree.js` — app files + production node_modules
30+
(pnpm-safe), node-gtk trimmed to `package.json` + `lib/` + the one
31+
`lib/binding/node-v<abi>-<platform>-<arch>` matching the bundling node.
32+
`ctx.bindingPath` is the copied `.node` — seed your closure walk from it.
33+
- `tools/bundle/seeds.js` — the GI namespace libraries to bundle, **already
34+
carrying the win32/darwin file names** (they mirror
35+
`windows/runtime-libraries.txt`). Every seed is skip-if-missing.
36+
- Config (`loadConfig`), output layout, marker/overwrite safety, node-binary
37+
copy + strip, size report, `bundle.json` manifest.
38+
- The launcher must include `--import node-gtk/register` when
39+
`config.register` (default true) — see the Linux/flatpak launchers.
40+
41+
The `ctx` fields you get: `config, appDir, outBase, runtimeDir, appOutDir,
42+
bindingName, bindingPath, log` (+ on darwin you must set/use `ctx.contentsDir`
43+
`tools/bundle.js` already computes the `.app/Contents` layout for darwin;
44+
check the `process.platform === 'darwin'` branch).
45+
46+
## Windows (`platform-win32.js`) — mostly a port of proven code
47+
48+
`scripts/windows-bundle-runtime.sh` is the **CI-proven reference** (it makes
49+
the npm prebuilt self-contained; test-windows-prebuilt.yaml validated a
50+
no-MSYS2 clean machine). Port its logic to JS; the app bundler is a superset
51+
(adds node.exe + app tree + launcher).
52+
53+
- **Environment**: runs inside an MSYS2 MINGW64 shell (where GTK is installed
54+
and node-gtk was built) but under the *Windows* node. Resolve the prefix:
55+
`cygpath -m ${MINGW_PREFIX:-/mingw64}``C:/msys64/mingw64`; error with a
56+
"run from MINGW64" message if absent.
57+
- **DLL closure**: `ntldd -R <file>` per entry (the `.node` + seeds from
58+
`seedNames('win32', config.libraries, config.gtk)` resolved against
59+
`<prefix>/bin` + the gdk-pixbuf loader DLLs). Parse `=> C:\...` lines, keep
60+
only paths under the prefix (System32 stays on the host), copy to
61+
`runtime/lib` keyed by lowercased basename.
62+
- **Typelibs**: `<prefix>/lib/girepository-1.0/*.typelib``runtime/lib/girepository-1.0`.
63+
- **gdk-pixbuf loaders**: `<prefix>/lib/gdk-pixbuf-2.0/2.10.0/loaders/*.dll`;
64+
rewrite `loaders.cache` loader paths to **bare file names**
65+
(`.replace(/^"[^"]*[\\/]([^"\\/]+\.dll)"/gm, '"$1"')`) and put the loaders
66+
dir on PATH in the launcher — this is exactly what `lib/native.js` +
67+
`windows-bundle-runtime.sh` already do for the prebuilt.
68+
- **Runtime data**: `<prefix>/share/{glib-2.0/schemas/gschemas.compiled,icons/Adwaita,icons/hicolor}`.
69+
- **node**: copy `process.execPath``runtime/node.exe` (MSVC node + MinGW
70+
GTK DLLs interop fine — C ABI; the prebuilt already proves it).
71+
- **Launcher** `<Name>.cmd` (CRLF):
72+
```
73+
@echo off
74+
setlocal
75+
set "HERE=%~dp0"
76+
set "RT=%HERE%runtime"
77+
set "PATH=%RT%\lib;%RT%\lib\gdk-pixbuf-2.0\2.10.0\loaders;%PATH%"
78+
set "GI_TYPELIB_PATH=%RT%\lib\girepository-1.0"
79+
set "XDG_DATA_DIRS=%RT%\share"
80+
set "GSETTINGS_SCHEMA_DIR=%RT%\share\glib-2.0\schemas"
81+
set "GDK_PIXBUF_MODULE_FILE=%RT%\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache"
82+
cd /d "%HERE%app"
83+
"%RT%\node.exe" --import node-gtk/register ".\<entry>" %*
84+
```
85+
(`cd` into app/ so bare-specifier `--import`s resolve — same reasoning as
86+
the Linux launcher.)
87+
- **Archive**: `powershell.exe -NoProfile -Command Compress-Archive ...` (zip).
88+
- **CI**: main.yaml's `build-windows` job already builds in MINGW64 with GTK4
89+
installed. Add a bundle-smoke step for one node version;
90+
`scripts/bundle-smoke-test.js` already handles win32 (junction symlink,
91+
`cmd.exe /c` launcher spawn) — just delete its early platform gate as you
92+
add support. Runners have a desktop session, no xvfb needed.
93+
94+
## macOS (`platform-darwin.js`) — the genuinely new work
95+
96+
GTK comes from Homebrew (CI installs gtk3 via brew today; gtk4 works too).
97+
Two-layer relocation design:
98+
99+
1. **Closure**: walk `otool -L` transitively from the `.node` + seeds
100+
(`<brew>/lib/<name>`, realpath'd) + pixbuf loaders. Resolve
101+
`@loader_path/...` against the referencing lib's dir; `@rpath/x`
102+
`<brew>/lib/x` heuristic; drop `/usr/lib` + `/System`. Keep only realpaths
103+
under the brew prefix. Copy flat (basename) into `runtime/lib`.
104+
2. **Rewrite** (`install_name_tool`): for every copied dylib, loader, and the
105+
`.node`, `-change <brew-path> <relative>/<basename>` where relative is
106+
`@loader_path` for dylibs (all siblings), `@loader_path/../../..` for
107+
loaders, and `@loader_path/<computed-rel-to-runtime-lib>` for the `.node`
108+
(compute with `path.relative`). Do **not** add LC_RPATH entries —
109+
`-add_rpath` needs header padding and can fail; `-change` to a *shorter*
110+
string always fits. **Re-sign every touched file ad-hoc**
111+
(`codesign --force --sign -`) — mandatory on arm64. Where a rewrite fails,
112+
warn and rely on layer 3.
113+
3. **Launcher env fallback**: `DYLD_FALLBACK_LIBRARY_PATH="$RT/lib:/usr/local/lib:/usr/lib"`.
114+
Crucial detail: brew **typelibs bake absolute dylib paths**; when GI
115+
`g_module_open`s a missing absolute path, dyld retries the leaf name
116+
against the fallback path — that's what makes the bundle work on machines
117+
without brew. SIP note: DYLD_* vars set *inside* the launcher script
118+
survive into our (unprotected) node binary; vars inherited *into* a
119+
protected shell do not — so set them in the script, never rely on the
120+
caller's env.
121+
- **Layout**: `<outBase>/<Name>.app/Contents/{MacOS/<Name>,Info.plist,Resources/{runtime,app}}`
122+
(bundle.js already computes these ctx paths). Minimal Info.plist:
123+
CFBundlePackageType APPL, Name/DisplayName/Identifier/Executable,
124+
ShortVersionString, NSHighResolutionCapable, LSMinimumSystemVersion 11.0.
125+
- **Runtime data**: `<brew>/share/{glib-2.0/schemas,icons/...}`; if
126+
`gschemas.compiled` is missing run `glib-compile-schemas`. pixbuf loaders
127+
under `<brew>/lib/gdk-pixbuf-2.0/2.10.0/loaders` (`.so`/`.dylib`); use the
128+
same `@LOADERS_DIR@` cache-template + launcher-sed trick as Linux
129+
(`platform-linux.js`).
130+
- **Archive**: `hdiutil create -volname <Name> -srcfolder <outBase> -ov -format UDZO <out>.dmg`.
131+
- **node strip**: `strip -x` on darwin (see ci.sh), then ad-hoc re-sign.
132+
- **CI**: the macos job has brew GTK3; the smoke fixture already falls back
133+
Gtk4→Gtk3. Caveat: the runner *has* brew, so the DYLD-fallback path isn't
134+
truly exercised there — a follow-up "consume on clean machine" job (rename
135+
the brew prefix, or a second runner without brew) is the honest test,
136+
mirroring test-windows-prebuilt.yaml's two-job pattern.
137+
- **Distribution reality**: output is unsigned; document that shipping needs
138+
Developer ID signing + notarization (out of scope for the module itself).
139+
- Per-arch bundles (arm64 + x86_64 separately) — don't attempt universal.
140+
141+
## Shared follow-ups (not yours, but don't preclude them)
142+
143+
AppImage wrapper around the Linux tree; shared-runtime install (`runtime/` is
144+
already content-addressed-friendly); Flathub submission of a first real app.
145+
146+
## Validation bar (what "done" meant for Linux)
147+
148+
1. `scripts/bundle-smoke-test.js` green on the target OS in CI (it asserts
149+
the app ran under the **bundled** node).
150+
2. A real app bundled and launched by hand at least once
151+
(`~/src/mariner` is the guinea pig; its node_modules/node-gtk is a symlink
152+
— point it at your worktree).
153+
3. Size report sane; no host-path leakage (Linux used `LD_DEBUG=libs`; use
154+
`DYLD_PRINT_LIBRARIES=1` / Process Monitor equivalents).
155+
4. Docs: extend `doc/bundling.md` (platform sections + caveats), drop the
156+
"Linux today" banners, update the Roadmap.
157+
158+
## Process notes
159+
160+
- Worktrees in `~/worktrees`, `pnpm install` (no node_modules symlinks),
161+
explicit `git add` (never `-A`), Conventional Commits, PR against master,
162+
merge only on `gh run view --json conclusion` == success.
163+
- Project memory (`~/.claude/projects/-home-romgrk-src-node-gtk/memory/`)
164+
has the full Linux war story under `project_bundle_cli.md`.

0 commit comments

Comments
 (0)