feat(build-harness): implement node_modules pruning and add exclusion configuration
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / node 22.19 (push) Has been cancelled
CI / node 26 (push) Has been cancelled
CI / python 3.10 / keyless SDK (push) Has been cancelled
CI / windows node 24 / wine blocking (push) Has been cancelled
CI / wine apt cache (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
E2E (real DeepSeek API) / e2e (push) Has been cancelled
Release (vendor) / Pack npm tarballs (push) Has been cancelled
Release (vendor) / Publish to npm (push) Has been cancelled
Release (dsh) / Pack npm tarballs (push) Has been cancelled
Release (dsh) / Publish to npm (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Has been cancelled
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / node 22.19 (push) Has been cancelled
CI / node 26 (push) Has been cancelled
CI / python 3.10 / keyless SDK (push) Has been cancelled
CI / windows node 24 / wine blocking (push) Has been cancelled
CI / wine apt cache (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
E2E (real DeepSeek API) / e2e (push) Has been cancelled
Release (vendor) / Pack npm tarballs (push) Has been cancelled
Release (vendor) / Publish to npm (push) Has been cancelled
Release (dsh) / Pack npm tarballs (push) Has been cancelled
Release (dsh) / Publish to npm (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -10,10 +10,17 @@
|
||||
* native, apps/cli, apps/web — plus the platform Node binary, into
|
||||
* `build/harness`, which electron-builder ships as `extraResources`.
|
||||
*
|
||||
* The packaged harness must stay fully self-contained: the app is not a mere
|
||||
* launcher, it must let users in the most barren environments use dsh entirely
|
||||
* through the app. We therefore copy the whole workspace node_modules and then
|
||||
* prune a curated, VERIFIED-safe exclusion list (see pruneNodeModules below and
|
||||
* `scripts/harness-excludes.json`). Nothing is excluded unless it is provably
|
||||
* outside the dsh runtime dependency closure.
|
||||
*
|
||||
* Run from the repository root before `desktop:pack`.
|
||||
*/
|
||||
|
||||
import { cpSync, chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { cpSync, chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
@@ -37,6 +44,12 @@ for (const [src, dst] of extraDirs) {
|
||||
cpSync(join(root, src), join(out, dst), { recursive: true })
|
||||
}
|
||||
|
||||
// Prune verified-safe build/packaging tooling out of the copied node_modules.
|
||||
// This is the ONLY sanctioned way to shrink the self-contained runtime: every
|
||||
// exclusion is validated against the dependency closure of the kept packages
|
||||
// and refused if any kept package depends on it.
|
||||
pruneNodeModules(join(out, 'node_modules'))
|
||||
|
||||
// Bundle the platform Node binary for the harness child. Windows needs a
|
||||
// `.exe` extension so both the child spawn and the `dsh.cmd` shim can execute
|
||||
// it; POSIX uses a bare `node`.
|
||||
@@ -84,4 +97,152 @@ if (process.platform === 'win32') {
|
||||
chmodSync(dshLauncher, 0o755)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the curated build/packaging-time packages from a copied pnpm
|
||||
* `node_modules`. Safe by construction:
|
||||
*
|
||||
* 1. Reads `scripts/harness-excludes.json` — a visible, configurable list of
|
||||
* package names + reasons (users add/remove entries without touching the
|
||||
* copy logic).
|
||||
* 2. For each exclusion, walks every KEPT `.pnpm/<pkg>@<ver>` entry and checks
|
||||
* whether any kept package symlinks this name as a dependency. If so, the
|
||||
* exclusion is REFUSED (throws) — the runtime must stay whole.
|
||||
* 3. If safe, deletes the `.pnpm/<name>@*` entries, the top-level
|
||||
* `node_modules/<name>` link, and any `.bin` shims that pointed into them,
|
||||
* reporting bytes freed per entry and a total.
|
||||
*/
|
||||
function pruneNodeModules(nmDir) {
|
||||
const cfgPath = join(root, 'apps/desktop/scripts/harness-excludes.json')
|
||||
if (!existsSync(cfgPath)) return
|
||||
const { excludes } = JSON.parse(readFileSync(cfgPath, 'utf8'))
|
||||
if (!Array.isArray(excludes) || excludes.length === 0) return
|
||||
|
||||
// De-dupe by name so a duplicate config entry can't double-report or double-count.
|
||||
const unique = new Map()
|
||||
for (const e of excludes) if (e && e.name && !unique.has(e.name)) unique.set(e.name, e)
|
||||
const list = [...unique.values()]
|
||||
|
||||
const pnpmDir = join(nmDir, '.pnpm')
|
||||
if (!existsSync(pnpmDir)) return
|
||||
const entries = readdirSync(pnpmDir)
|
||||
|
||||
// All packages that will be removed — used to ignore cross-excluded refs
|
||||
// during the safety check (a dependency among two removed packages is fine).
|
||||
const excludedSet = new Set(list.map((e) => e.name))
|
||||
|
||||
// Package name of a .pnpm entry. Entries look like "name@1.0.0" for a bare
|
||||
// package or "@scope+name@1.0.0" for a scoped one, but peer-dependency
|
||||
// suffixes add further "@"s (e.g. "dmg-builder@25.1.8_peer@25.1.8"). Parse
|
||||
// from the FIRST "@" — the base name never contains one after the version
|
||||
// separator — so peer suffixes can't leak into the parsed name.
|
||||
const entryPkg = (entry) => {
|
||||
const at = entry.indexOf('@')
|
||||
if (at === -1) return entry
|
||||
if (at === 0) {
|
||||
const rest = entry.slice(1) // "@scope+name@ver..." -> "scope+name@ver..."
|
||||
const sep = rest.indexOf('@')
|
||||
const name = sep === -1 ? rest : rest.slice(0, sep)
|
||||
return '@' + name.replace('+', '/')
|
||||
}
|
||||
return entry.slice(0, at)
|
||||
}
|
||||
|
||||
let totalFreed = 0
|
||||
|
||||
for (const ex of list) {
|
||||
const name = ex.name
|
||||
if (excludedSet.size === 0) continue
|
||||
|
||||
// --- 2. Safety: does any KEPT package depend on this package? ---
|
||||
const dependents = []
|
||||
for (const entry of entries) {
|
||||
const ep = entryPkg(entry)
|
||||
if (ep === name || excludedSet.has(ep)) continue // itself or also excluded
|
||||
const depDir = join(pnpmDir, entry, 'node_modules')
|
||||
if (!existsSync(depDir)) continue
|
||||
if (readdirSync(depDir).includes(name)) dependents.push(ep)
|
||||
}
|
||||
if (dependents.length > 0) {
|
||||
throw new Error(
|
||||
`[harness-excludes] REFUSING to exclude "${name}": kept packages depend on it ` +
|
||||
`(${dependents.join(', ')}). The runtime must stay whole — remove it from ` +
|
||||
`${cfgPath} or it is not actually safe to exclude.`,
|
||||
)
|
||||
}
|
||||
|
||||
// --- 3. Delete the .pnpm store entries for this package ---
|
||||
const matching = entries.filter((e) => entryPkg(e) === name)
|
||||
for (const m of matching) {
|
||||
const p = join(pnpmDir, m)
|
||||
const sz = dirSize(p)
|
||||
rmSync(p, { recursive: true, force: true })
|
||||
totalFreed += sz
|
||||
console.log(` [exclude] ${name} — removed .pnpm/${m} (${fmtSize(sz)})`)
|
||||
}
|
||||
|
||||
// --- 4. Delete the top-level link/dir (node_modules/@scope/name too) ---
|
||||
const top = join(nmDir, ...name.split('/'))
|
||||
if (existsSync(top)) {
|
||||
const sz = dirSize(top)
|
||||
rmSync(top, { recursive: true, force: true })
|
||||
totalFreed += sz
|
||||
console.log(` [exclude] ${name} — removed top-level link (${fmtSize(sz)})`)
|
||||
}
|
||||
|
||||
// --- 5. Drop .bin shims that pointed into the removed store entries ---
|
||||
removeBinShims(join(nmDir, '.bin'), matching, pnpmDir)
|
||||
}
|
||||
|
||||
if (totalFreed > 0) {
|
||||
console.log(
|
||||
`\n[harness-excludes] freed ${fmtSize(totalFreed)} by excluding ` +
|
||||
`${list.map((e) => e.name).join(', ')} (config: ${cfgPath})`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Recursive size of a dir or link target. Symlinks are counted as 0 bytes and
|
||||
* never followed (a pnpm store is full of links; following would loop). */
|
||||
function dirSize(p) {
|
||||
let st
|
||||
try {
|
||||
st = lstatSync(p)
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
if (st.isSymbolicLink() || st.isFile()) return st.size
|
||||
if (!st.isDirectory()) return 0
|
||||
let total = 0
|
||||
for (const child of readdirSync(p)) total += dirSize(join(p, child))
|
||||
return total
|
||||
}
|
||||
|
||||
/** Remove `.bin` shims whose resolved target lives inside any removed store
|
||||
* entry, so no dangling executables remain. */
|
||||
function removeBinShims(binDir, removedEntries, pnpmDir) {
|
||||
if (!existsSync(binDir)) return
|
||||
const removedStoreDirs = removedEntries.map((e) => join(pnpmDir, e))
|
||||
for (const shim of readdirSync(binDir)) {
|
||||
const p = join(binDir, shim)
|
||||
let target
|
||||
try {
|
||||
target = readlinkSync(p)
|
||||
} catch {
|
||||
continue // not a symlink (e.g. a real .cmd file) — leave it
|
||||
}
|
||||
const resolved = resolve(binDir, target)
|
||||
if (removedStoreDirs.some((d) => resolved.startsWith(d))) {
|
||||
rmSync(p, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Human-readable byte size, e.g. "553 MiB". */
|
||||
function fmtSize(bytes) {
|
||||
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(2)} GiB`
|
||||
if (bytes >= 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(1)} MiB`
|
||||
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KiB`
|
||||
return `${bytes} B`
|
||||
}
|
||||
|
||||
console.log(`assembled self-contained harness at ${out}`)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"$schema": "harness-excludes.schema.json",
|
||||
"version": 1,
|
||||
"description": "Packages excluded from the packaged harness node_modules (.pnpm store). The app must stay fully self-contained — it is not a launcher, it lets users in the most barren environments use dsh entirely through the app. So an entry here is a HARD guarantee that the package is NOT part of the dsh runtime dependency closure: the build refuses to exclude anything a kept workspace package actually depends on (see build-harness.mjs pruneNodeModules). Add entries only after verifying the package is build-time / packaging-time tooling that the harness never imports at runtime.",
|
||||
"excludes": [
|
||||
{
|
||||
"name": "electron",
|
||||
"reason": "Electron shell runtime. electron-builder ships it separately into resources/app; the dsh harness runs under the bundled system-Node child and never imports Electron. Verified: only apps/desktop/package.json depends on it, only apps/desktop/lib/types/{main,preload}.js import it (those are the shell, already packaged)."
|
||||
},
|
||||
{
|
||||
"name": "electron-builder",
|
||||
"reason": "Packaging toolchain (group). The harness is pre-built and distributed, so nothing packages installers at runtime. The electron-builder toolchain is mutually interdependent, so it is excluded as a group; build-harness.mjs refuses any exclusion a kept package still depends on, which is how this group stays coherent."
|
||||
},
|
||||
{
|
||||
"name": "app-builder-lib",
|
||||
"reason": "electron-builder toolchain (group): app-builder-lib is packaging logic only, never imported by the dsh runtime."
|
||||
},
|
||||
{
|
||||
"name": "builder-util",
|
||||
"reason": "electron-builder toolchain (group): build utilities (spawns app-builder-bin, 7zip-bin). Build-time only."
|
||||
},
|
||||
{
|
||||
"name": "builder-util-runtime",
|
||||
"reason": "electron-builder toolchain (group): runtime helpers used only by the builder at package time, not by dsh."
|
||||
},
|
||||
{
|
||||
"name": "dmg-builder",
|
||||
"reason": "electron-builder toolchain (group): mac .dmg packaging; not present on this Windows build and never used at runtime."
|
||||
},
|
||||
{
|
||||
"name": "electron-builder-squirrel-windows",
|
||||
"reason": "electron-builder toolchain (group): Squirrel.Windows packaging; build-time only."
|
||||
},
|
||||
{
|
||||
"name": "app-builder-bin",
|
||||
"reason": "electron-builder's build binary (7za/signtool/app-builder). Build-time only, never imported by the dsh runtime. Depended on by builder-util, which is itself excluded in this group."
|
||||
},
|
||||
{
|
||||
"name": "electron-publish",
|
||||
"reason": "electron-builder toolchain (group): publishing (upload to GitHub etc.); never used by the dsh runtime."
|
||||
},
|
||||
{
|
||||
"name": "7zip-bin",
|
||||
"reason": "electron-builder toolchain (group): 7-zip binary used to compress installers at package time; not a dsh runtime dependency."
|
||||
},
|
||||
{
|
||||
"name": "@electron/asar",
|
||||
"reason": "electron-builder toolchain (group): asar archive packing; build-time only (the desktop app ships asar:false anyway)."
|
||||
},
|
||||
{
|
||||
"name": "@electron/notarize",
|
||||
"reason": "electron-builder toolchain (group): macOS notarization; build-time only, and irrelevant on this Windows build."
|
||||
},
|
||||
{
|
||||
"name": "@electron/osx-sign",
|
||||
"reason": "electron-builder toolchain (group): macOS code signing; build-time only."
|
||||
},
|
||||
{
|
||||
"name": "@electron/universal",
|
||||
"reason": "electron-builder toolchain (group): macOS universal binaries; build-time only."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://deepseek-harness.dev/schemas/harness-excludes.schema.json",
|
||||
"title": "Harness dependency exclusion list",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["version", "description", "excludes"],
|
||||
"properties": {
|
||||
"version": { "type": "integer", "const": 1 },
|
||||
"description": { "type": "string" },
|
||||
"excludes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "reason"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Package name to exclude, e.g. \"electron\" or \"@scope/name\". Matches pnpm store entries by unscoped/scoped base name.",
|
||||
"minLength": 1
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "Why this package is safely outside the dsh runtime dependency closure. Required so the exclusion stays auditable."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user