feat: enhance desktop packaging process with self-contained harness verification

- Updated README and README.zh to clarify packaging commands and added verification step.
- Improved packaging scripts to automatically build the self-contained runtime and verify its integrity.
- Introduced new scripts for verifying harness self-containment and booting the web profile.
- Added comprehensive tests for relinking harness symlinks and ensuring self-containment.
- Updated package.json scripts to reflect new build and verification processes.
This commit is contained in:
2026-08-16 00:31:19 +08:00
parent 82814ad7d3
commit 8e79ec4942
12 changed files with 892 additions and 18 deletions
+54 -10
View File
@@ -101,11 +101,17 @@ override the spawned Node or `dsh` entry with `DSH_NODE` / `DSH_ENTRY`.
## Packaging
```sh
pnpm --filter @deepseek-ai/dsh-desktop run build:harness # (re)assemble the bundled runtime
pnpm desktop:pack # electron-builder: mac .dmg / win .nsis
pnpm desktop:pack # build lib+web, assemble the self-contained harness, then electron-builder: mac .dmg / win .nsis
pnpm desktop:verify # prove the packaged app's harness is self-contained and boots
```
Artifacts land in `apps/desktop/dist`.
`desktop:pack` runs `build:harness` (assembling `build/harness`) automatically,
so a stale or missing runtime can never be shipped. Artifacts land in
`apps/desktop/dist`. `desktop:verify` rebuilds an unpacked app
(`electron-builder --dir`) and checks that every symlink inside the packaged
`resources/harness` resolves inside the bundle, then boots the web profile from
it — the check that the installer is genuinely detached from the development
machine.
### The self-contained harness (`build/harness`)
@@ -145,7 +151,9 @@ Each target platform needs its own harness: the bundled `bin/node` and the
native addons are OS/arch-specific. Regenerate `build/harness` on each target
platform (or per-target in CI) before packaging that platform. The current
configuration targets **macOS arm64** (`mac.target: dmg`); `win.target: nsis`
is declared but needs a Windows-built harness.
is declared but needs a Windows-built harness. On Windows the harness build
additionally needs Developer Mode enabled or an admin shell so workspace links
can be rewritten to relative in-tree symlinks (see *Link correctness*).
### Signing
@@ -161,6 +169,38 @@ The bundled harness is multi-GB uncompressed (≈2 GB), dominated by
~1 GB. This is the inherent footprint of shipping the full harness runtime
standalone, and is the accepted trade-off for a zero-external-dependency app.
### Link correctness
The harness is held together by symlinks inside `node_modules` (pnpm workspace
links). On **Windows**, pnpm often records those as **junctions — absolute-path
reparse points pointing at the build machine's dev tree**. A plain directory
copy keeps them verbatim, so a packaged app built that way reads the
*development* tree when run on the build machine and dangles on any other
machine.
`scripts/build-harness.mjs` therefore runs a relink pass
(`scripts/harness-relink.mjs`) after copying, re-running it until no link changes
(rewriting one link can expose another, so a single pass may not converge):
every symlink/junction is rewritten to a **relative target inside
`build/harness`** (mirroring the dev layout), any genuinely external target is
dereferenced (copied in as real files), and a link to a workspace package the
harness does not ship (e.g. the desktop shell itself, or a dependency-resolution
member like `python/sdk-runtime`) is **dropped** — it cannot be mirrored and is
not part of the runtime closure. The pass is followed by a self-containment
check that **fails the build if any link still escapes the bundle**, and by a
`dsh --version` smoke test.
To build you must be able to create directory symlinks: **Windows — enable
Developer Mode or use an admin shell** (macOS/Linux need nothing). Without that
capability `build:harness` refuses, because the only fallback (dereferencing the
whole pnpm store) would balloon the ≈2 GB runtime to many times its size. A
locked-down machine can opt into that anyway with `--dereference-ok`.
`pnpm desktop:verify` is the end-to-end check: it packs an unpacked app
(`electron-builder --dir`) and re-runs the same link check against the packaged
`resources/harness`, proving electron-builder's own `extraResources` copy kept
the relative links intact, then boots the web profile straight from the bundle.
## Release process
The app is **not code-signed**, so updates are a manual-download flow rather
@@ -192,14 +232,18 @@ bump it too if you keep them in sync, but only the desktop one is user-visible.
### 2. Build and package
```sh
pnpm --filter @deepseek-ai/dsh-desktop run build:harness # assemble the self-contained runtime
pnpm desktop:pack # electron-builder: .dmg (mac) / .nsis .exe (win)
pnpm desktop:pack # build lib+web, assemble the self-contained harness, then electron-builder: .dmg (mac) / .nsis .exe (win)
pnpm desktop:verify # check the packaged harness is self-contained and boots
```
Artifacts land in `apps/desktop/dist/`. Each target OS/arch needs its own
harness (`build/harness` bundles a platform Node + native addons), so regenerate
it on the target platform before packing that platform. Building the Windows
`.exe` on macOS needs wine (or build on a Windows host).
Artifacts land in `apps/desktop/dist/`. `desktop:pack` assembles `build/harness`
first (`build:harness`), so a stale or missing runtime cannot be shipped. Each
target OS/arch needs its own harness (`build/harness` bundles a platform Node +
native addons), so regenerate it on the target platform before packing that
platform. Building the Windows `.exe` on macOS needs wine (or build on a Windows
host). Run `desktop:verify` on each platform you package: it rebuilds an
unpacked app and proves the packaged `resources/harness` has no link escaping
the bundle and the web profile boots from it.
### 3. Generate the update manifest