Since v13 the package bundles a prebuilt binary for all eight platforms
it supports (~17 MB) on top of the SQLite amalgamation needed to compile
from scratch (~10 MB). A packaged artifact loads exactly one binary and
never compiles, so all of that but ~2 MB was dead weight:
v0.104.1 (v12) 11.93 MB deps/ + a compiled build/Release binary
v13, untrimmed 27.0 MB deps/ + eight prebuilds
server, trimmed 4.5 MB lib/ + linux-x64 + linuxmusl-x64
desktop, trimmed 2.2 MB lib/ + linux-x64
That is ~6 MB off each compressed Linux artifact, and leaves us below
what v0.104.1 shipped rather than 15 MB above it. lib/ plus one prebuild
is the complete runtime set -- verified by loading a copy pruned to just
those files and running a query against it.
Two things the trim has to get right, both covered:
Linux keeps the musl build alongside the glibc one. The server's dist is
produced once on a glibc runner and then consumed by both the Debian and
the Alpine images, and better-sqlite3 resolves linuxmusl-* at runtime on
the latter, so dropping it would break the amd64 image at startup. The
Electron artifacts opt out, since Electron ships no musl builds.
The platform and architecture are the ones being built *for*, not the
host's: the macOS runners are arm64 and also package darwin-x64, so
trimming by host arch would strip the binary that build needs.
TARGET_ARCH / MATRIX_ARCH already carry the target in CI.
An unsupported platform/arch throws rather than silently producing an
artifact with no native addon.
Also drops the better-sqlite3 cleanup from flake.nix's server install
phase: it removed deps/sqlite3 and the build/ scaffolding that held
build-time store paths, all of which the build now trims itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
better-sqlite3 v13's bundled N-API prebuilds are linked against a much
newer toolchain than the assets prebuild-install used to fetch, which
would have raised the glibc floor of every native Linux artifact and
dropped distributions we still support:
v0.104.1 (v12 assets) GLIBC_2.29 / GLIBCXX_3.4.20
v13 prebuild, x64 GLIBC_2.34 / GLIBCXX_3.4.29
v13 prebuild, arm64 GLIBC_2.38 / GLIBCXX_3.4.31
The arm64 figure drops Debian 12 "bookworm" -- including Raspberry Pi OS
-- along with Ubuntu 22.04 and RHEL 9; x64 drops Ubuntu 20.04, Debian 11
and RHEL 8. Nothing in better-sqlite3 needs a newer glibc: the addon
picked up GLIBC_2.34 because it was linked where libpthread had merged
into libc, and GLIBC_2.38 because GCC 13+ redirects strtol to the
__isoc23_* symbols. The floor is an artifact of the build host, so build
on an older one and substitute the result for the bundled prebuild.
Compiling in node:22-bullseye restores GLIBC_2.29 / GLIBCXX_3.4.21 --
glibc parity with v0.104.1, and the one-step GLIBCXX move constrains
nothing, since glibc 2.29 already implies a 2019-or-later distro. No
platform is lost.
Notes on the implementation:
- force_build is required. binding.gyp resolves both targets to
`type: none` whenever a prebuild for the host exists, so an ordinary
node-gyp run emits a stamp file and no addon.
- The compiled addon replaces prebuilds/<target>.node rather than
relying on the build/Release fallback, leaving the loader's resolution
order untouched.
- nodeLinker is "hoisted", so every dependant gets its own copy and the
build scripts resolve <app>/node_modules before the root; all copies
are patched. Each is replaced by rename, since these files are
hardlinks into the pnpm store and writing in place would mutate it.
- python3 is apt-installed when absent: buildpack-deps supplies g++ and
make but not python3, which node-gyp needs. Sources fall back to
archive.debian.org so this keeps working once bullseye goes EOL on
2026-08-31 -- an old *build* container is deliberate here, the same
approach manylinux and Node's own release builds take.
Docker images are unaffected: they carry their own glibc.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v13 moves the addon onto N-API and ships prebuilt binaries for every
supported platform/arch inside the package itself. Three parts of the
build assumed the old prebuild-install/node-gyp layout.
Dropped `bindings` and `file-uri-to-path` from copyNodeModules: v13 no
longer depends on either, so the copy aborted the build outright with
"Unable to find any of the paths: .../node_modules/bindings".
Removed the Electron native rebuild. v13's binding.gyp resolves both of
its targets to `type: none` whenever a prebuild for the host exists, so
electron-rebuild.mts produced a stamp file and no addon on every install,
leaving BETTERSQLITE3_NATIVE_PATH pointing at a build/Release binary that
is never created -- `pnpm desktop:start` died on MODULE_NOT_FOUND. The
prebuilds are ABI-stable and load unchanged under Electron 42 (Node-API
10), so the rebuild step, its @electron/rebuild and prebuild-install
devDependencies, and the ELECTRON_NODEDIR plumbing in flake.nix all go.
Rebuilt the Docker images around the same short-circuit: `pnpm rebuild`
in the builder stages was also a no-op, so the images would have shipped
the upstream prebuild, which needs glibc >= 2.34 (x64) / >= 2.38 (arm64)
against bullseye's 2.31. The glibc images move to trixie and lose their
builder stage; the Alpine pair lose theirs too, since the linuxmusl-*
prebuilds only need libstdc++, which node:*-alpine already provides.
Alpine stays pinned to 3.23 for the musl 1.2.5 reason in #10627.
Dockerfile.legacy must keep compiling from source -- 32-bit ARM has no
prebuild at all, and linux/arm/v8 normalizes to arm64, whose prebuild
outruns that image's bullseye glibc. npm_config_force_build=1 restores
the real compile and link rules.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `start-prod`/`start-prod-no-dir` scripts invoked the npm prebuilt
Electron binary directly (`electron dist`), which fails to load on NixOS
because it's dynamically linked against FHS system libraries that don't
exist there (libcups.so.2, libgtk-3, libnss3, ...). The dev launcher
already handles this via electron-start.mts, but prod never did.
Add scripts/electron-run-prod.mts, a build-free launcher that reuses the
same NixOS-aware getElectronPath() (preferring the Nix/nix-develop
Electron over the prebuilt binary) and sets LD_LIBRARY_PATH plus
--no-sandbox on NixOS. On all other platforms it's a transparent
`electron <args>` wrapper, so behaviour is unchanged.
Also extract the shared getNixLdLibraryPath() helper so the dev and prod
launchers stay DRY.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The import.meta.url shim added for the bundled Claude Agent SDK prepends a
`require("node:url")` banner to every buildBackend output. The desktop build
compiled main.ts and preload.ts together, so the banner landed atop
preload.cjs, which runs in Electron's sandboxed renderer where
`require("node:url")` throws. That aborted the preload before it could expose
`electronApi`, leaving window.electronApi undefined: the window rendered a
transparent background with no Mica material behind it and DevTools could not
be opened.
Gate the shim (define + banner) behind a buildBackend option and build the
preload without it; the preload never references import.meta.url.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The desktop build bundles everything into a CJS main.cjs. CJS has no
import.meta, so esbuild rewrites import.meta.url to {} (undefined).
@anthropic-ai/claude-agent-sdk calls createRequire(import.meta.url) at
module top level, so evaluating it threw ERR_INVALID_ARG_VALUE, which
surfaced as "[trilium-app] dispatch failed for GET trilium-app://app/".
Shim import.meta.url to the bundle's own file URL via esbuild define +
banner, so createRequire()/.resolve() anchor at dist/ and still resolve
sibling node_modules packages. Also ship the SDK's per-platform native
claude binary into dist/node_modules, since the JS is bundled but the
binary it spawns at query time cannot be.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>