fix(desktop): start Gauzy Server from a read-only install and load sharp on Linux
Summary
Two Linux start-up failures of the desktop apps:
- Gauzy Server no longer needs to write into its install directory to tell its web UI which API to call. A .deb install (root-owned
/opt/Gauzy Server) or an AppImage (read-only mount) now gets through the setup; the UI server inserts the API URL intoindex.htmlas it serves it. - sharp loads on Linux in Ever Gauzy Desktop, Gauzy Server and Gauzy API Server: sharp and its versioned libvips are unpacked from
app.asar.
Why
- #7735:
ServerConfig.update()rewroteresources/data/ui/index.htmlin the install directory.ReadWriteFile.read()answersundefinedwhen that directory is not writable, andupdate()then failed withCannot read properties of undefined (reading 'replace'), rethrown byinitializeConfig, which stopped the setup. - #9167: electron-builder's smart unpack only takes a package out of
app.asarwhen one of its files ends in.dll/.exe/.dylib/.so/.node. sharp's Linux libvips is versioned (@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.42), so it stayed insideapp.asarwhile sharp's.nodeaddon was unpacked, and the addon could notdlopenit (ERR_DLOPEN_FAILED). macOS (.dylib) and Windows (.dll) were already unpacked. The desktop timer does not package sharp, which is why it did not reproduce there.
What changed
packages/desktop-lib
- New
config/ui-runtime-config.ts: plain string functions with no import.buildUiRuntimeConfigScript(apiUrl)builds thewindow._env = { api: ⦠}script, with the URL written as a JSON string and<escaped, so no value can end the string or the script early (before, the URL was interpolated raw between single quotes).applyUiRuntimeConfig(html, apiUrl)removes any earlier config, in the format of any release and including the duplicates older releases could leave, and inserts the current one before</body>. Applying it twice gives the same page. ServerConfig.update()uses it. A writable install still gets the script written into its file, as before (kept as the fallback). An unwritable one is skipped with a warning, an unchanged file is not rewritten, and an error here is logged instead of stopping the start, because the UI server inserts the script anyway. The three private helpers it replaces are gone;update()keeps its signature.UiServicehands the script to the UI server process inUI_RUNTIME_CONFIG_SCRIPT, built fromconfig.apiUrlat every start and restart, so a changed API port or host reaches the UI after "Save", the same wayUI_HOSTdoes.IReadWriteFile.read()is typedstring | undefined, which is what it already returned.
apps/server (Gauzy Server's UI server, preload/)
- New
index-html.ts:isIndexHtmlRequest(GET/HEAD of/or/index.html, path compared as sent, so..and escapes stay with serve-static's checks),insertIntoIndexHtml(does not add a second copy when a writable install's file already has the script) andserveIndexHtml. createStaticFileHandler(root, { indexHtmlSnippet }): with a snippet, the start page is served with it andCache-Control: no-cache, and the file on disk is only read. Every other file, and the start page when it cannot be read (404 when missing), still goes through serve-static exactly as before.StaticFileServerreads the snippet fromUI_RUNTIME_CONFIG_SCRIPT.- The UI server runs in a plain Node process forked from Electron's, which cannot load
@gauzy/desktop-lib(it needs Electron). So desktop-lib builds the script and this side only places it, and the variable's name and the script's placement are pinned in both packages' specs.
apps/desktop, apps/server, apps/server-api (src/package.json)
build.asarUnpackgains**/node_modules/sharp/**/*and**/node_modules/@img/**/*, the patterns sharp's Electron docs give (the**/prefix also covers a nested copy).desktop-timer,agentandserver-mcpdo not package sharp and are unchanged.
tools/electron/asar-unpack.test.js (new) + .github/workflows/static-checks.yml
- A
node:testcheck, needing no install, that finds every electron-builder app whose packaged dependencies reach sharp, directly or through a workspace package (@gauzy/core,@gauzy/plugin-docs), and fails when itsasarUnpacklacks the patterns. A future native module with the same problem is one more entry inNATIVE_UNPACK_RULES. A control case fails if the scan stops finding desktop/server/server-api. It runs as a step of the static checks job, next to the othertools/electroncases.
Flags/settings and their defaults
None added. Writable installs keep their file rewrite. The one visible difference for them is that index.html (only that file) is now served with Cache-Control: no-cache instead of max-age=3600, so a changed API URL shows on the next page load instead of up to an hour later.
Tests
All run on Windows, Node 26.10.0:
NODE_OPTIONS=--experimental-vm-modules npx jest -c packages/desktop-lib/jest.config.ts --rootDir packages/desktop-lib: 15 suites, 215 tests passed. New:ui-runtime-config.spec.ts(escaping verified by running the script innode:vm, a legacy-format and duplicate replacement, idempotence, URL change, other inline scripts kept, no</body>),ui-service.spec.ts(script handed over at start, follows the host and port, re-read on restart), andserver-config.spec.tsServerConfig.update(unwritable directory: no throw and no write; writable install still written, as a control; unchanged file not rewritten; other errors logged, not thrown).npx jest -c apps/server/jest.config.ts --rootDir apps/server --selectProjects server-preload: 2 suites, 33 tests passed. New:index-html.spec.ts, plus HTTP cases instatic-file-server.spec.ts:/,/index.htmland/?lang=frserved with the script andno-cache; HEAD; the file on disk unchanged; other files and a sub-directoryindex.htmluntouched;..and NUL still refused; a missing start page still 404. The existing traversal/NUL cases pass unchanged.node --test tools/electron/asar-unpack.test.js tools/electron/app-builder-lib-patch.test.js: 7 passed, 1 skipped (the patch case that skips from app-builder-lib 26.18.0 on). Negative control: withapps/server/src/package.jsonatdevelop, the new check fails and names the missing patterns.npx tsc -p packages/desktop-lib/tsconfig.lib.json --noEmit: clean. Theapps/server/src/preloadsources and specs type-check with the options oftsconfig.electron.json.tsc -p apps/server/tsconfig.electron.jsonreports only the missing generatedenvironments/environment.ts, the same as ondevelop.- ESLint on the touched files: no new findings. The remaining ones in touched files were already there (
no-inferrable-typesonUiService's constructor, unusedcatch (e)inReadWriteFile). - Smoke test of the real UI server entry (
preload/ui-server.jscompiled,index.htmlmarked read-only,UI_RUNTIME_CONFIG_SCRIPTbuilt by desktop-lib forhttp://192.168.1.20:3000):GET /returned 200 with the script before</body>andCache-Control: no-cache,main.jskeptmax-age=3600, the file on disk was unchanged, and "Listening on port" (the success messageUiServicewaits for) was printed.
Manual verification still needed
I could not run these here (Windows workstation, no Linux packaging):
- #7735: install the Gauzy Server .deb on Ubuntu (or run the AppImage), go through the setup, and confirm the server starts and the web UI at
:4200calls the configured API. Also confirm a Windows/macOS install, where the file is writable, still behaves as before. - #9167: build Ever Gauzy Desktop for Linux (e.g.
electron-builder --linux dir) and check thatresources/app.asar.unpacked/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.*andā¦/@img/sharp-linux-x64/lib/*.nodeexist, then start it and confirm the API boots withoutERR_DLOPEN_FAILED. Same for Gauzy Server / API Server. - The later Ubuntu 24.04 AppArmor/user-namespace error in #7735 is expected to be covered by electron-builder 26's .deb post-install AppArmor profile; worth confirming during the same .deb test.
Risk and rollback
- #7735: low. Writable installs keep their behaviour (file rewritten, now only when it changes). The request-time insertion only touches
/and/index.htmland reads a fixed path. To roll back, revert the first commit; the UI server then serves the file as it is on disk again. - #9167: low. On macOS and Windows these packages were already unpacked by the smart unpack (their libraries end in
.dylib/.dll), so only Linux gains unpacked files, plus sharp's JS and@img/coloureverywhere (a few hundred KB). To roll back, revert the second commit.
š¤ Generated with Claude Code
Summary by cubic
Fixes two Linux start-up failures of the desktop apps: Gauzy Server no longer needs to write into its install directory to point its web UI at the API, and sharp loads on Linux in Ever Gauzy Desktop, Gauzy Server, and Gauzy API Server. Writable installs keep their previous file rewrite; the visible change for them is that index.html is served with Cache-Control: no-cache instead of max-age=3600.
Bug Fixes
- Injecting the API URL script as
/and/index.htmlare served (instead of rewriting the file in the install directory) lets.deband AppImage installs get through setup, which previously failed withCannot read properties of undefined (reading 'replace')(#7735). Unwritable and unchanged files are skipped, and errors are logged instead of stopping startup. - Unpacking
sharpand@imgfromapp.asarfixesERR_DLOPEN_FAILEDon Linux: electron-builder's smart unpack missed sharp's versioned libvips (libvips-cpp.so.42), which stayed insideapp.asarwhile the.nodeaddon was unpacked (#9167). macOS and Windows were already unpacked. - A new
node:testcheck in the static-checks workflow fails if any desktop app packaging sharp lacks the unpack patterns, so a future native module only needs one more rule entry.