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
+165
View File
@@ -0,0 +1,165 @@
import {
existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
assertSelfContained, findEscapingLinks, isWithin, probeSymlinkSupport, relinkHarness,
} from '../scripts/harness-relink.mjs'
const dirs: string[] = []
afterEach(() => {
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true })
})
/**
* A repo whose layout mirrors the real one: `out` is the harness copy of the
* dev tree at `<repo>/apps/desktop/build/harness`, and the repo has a package
* the harness mirrors.
*/
function makeTree() {
const root = mkdtempSync(join(tmpdir(), 'dsh-relink-'))
dirs.push(root)
const repo = join(root, 'repo')
const out = join(repo, 'apps/desktop/build/harness')
mkdirSync(join(repo, 'packages/core/dsh-core'), { recursive: true })
writeFileSync(join(repo, 'packages/core/dsh-core/index.js'), 'export const x = 1\n')
mkdirSync(join(out, 'packages/core/dsh-core'), { recursive: true })
writeFileSync(join(out, 'packages/core/dsh-core/index.js'), 'export const x = 1\n')
mkdirSync(join(out, 'node_modules/@deepseek-ai'), { recursive: true })
return { root, repo, out }
}
/** The dir-symlink type to use when pre-creating a fixture link on this host. */
const dirLinkType = () => (process.platform === 'win32' ? 'junction' : 'dir')
/** Whether tests that create symlinks can run on this host. */
const canSymlink = probeSymlinkSupport()
describe('isWithin', () => {
it('accepts a path inside the root and rejects an escape', () => {
expect(isWithin('C:\\a\\b', 'C:\\a\\b\\c')).toBe(true)
expect(isWithin('C:\\a\\b', 'C:\\a\\other')).toBe(false)
expect(isWithin('C:\\a\\b', 'C:\\a\\b')).toBe(true)
})
it('rejects an absolute path on another drive', () => {
expect(isWithin('C:\\a\\b', 'D:\\x')).toBe(false)
})
})
describe.skipIf(!canSymlink)('relinkHarness', () => {
it('leaves a relative in-tree link alone', () => {
const { out } = makeTree()
const link = join(out, 'node_modules/@deepseek-ai/dsh-core')
symlinkSync('../../packages/core/dsh-core', link, 'dir')
const report = relinkHarness(out, dirname(out))
expect(report.unchanged).toBeGreaterThanOrEqual(1)
expect(report.errors).toEqual([])
expect(findEscapingLinks(out)).toEqual([])
})
it('rewrites an absolute link to the dev tree into a relative in-tree link', () => {
const { repo, out } = makeTree()
const link = join(out, 'node_modules/@deepseek-ai/dsh-core')
const devTarget = join(repo, 'packages/core/dsh-core')
symlinkSync(devTarget, link, dirLinkType())
const report = relinkHarness(out, repo)
expect(report.normalized).toBeGreaterThanOrEqual(1)
expect(report.errors).toEqual([])
expect(lstatSync(link).isSymbolicLink()).toBe(true)
const raw = readlinkSync(link)
expect(resolve(dirname(link), raw)).toBe(resolve(join(out, 'packages/core/dsh-core')))
expect(findEscapingLinks(out)).toEqual([])
})
it('mirrors a node_modules/.pnpm store link into the harness', () => {
const { repo, out } = makeTree()
// The store entry the workspace link would point at, in both trees.
for (const base of [repo, out]) {
mkdirSync(join(base, 'node_modules/.pnpm/b@1/node_modules/b'), { recursive: true })
writeFileSync(join(base, 'node_modules/.pnpm/b@1/node_modules/b/index.js'), 'export const b = 2\n')
}
const link = join(out, 'node_modules/@deepseek-ai/a')
symlinkSync(join(repo, 'node_modules/.pnpm/b@1/node_modules/b'), link, dirLinkType())
const report = relinkHarness(out, repo)
expect(report.errors).toEqual([])
expect(resolve(dirname(link), readlinkSync(link)))
.toBe(resolve(join(out, 'node_modules/.pnpm/b@1/node_modules/b')))
expect(findEscapingLinks(out)).toEqual([])
})
it('dereferences a genuinely external target into real content', () => {
const { root, out } = makeTree()
const external = join(root, 'external')
mkdirSync(external, { recursive: true })
writeFileSync(join(external, 'index.js'), 'export const ext = 3\n')
const link = join(out, 'node_modules/external')
symlinkSync(external, link, dirLinkType())
const report = relinkHarness(out, dirname(out))
expect(report.dereferenced).toBeGreaterThanOrEqual(1)
expect(report.errors).toEqual([])
expect(lstatSync(link).isSymbolicLink()).toBe(false)
expect(readFileSync(join(link, 'index.js'), 'utf8')).toContain('ext')
expect(findEscapingLinks(out)).toEqual([])
})
it('drops a link whose target contains the harness (e.g. the desktop shell)', () => {
const { repo, out } = makeTree()
// `repo/apps/desktop` contains `out` (the harness): it is a workspace
// package the harness deliberately does not ship, so the link cannot be
// made self-contained and must be dropped, not copied.
const link = join(out, 'node_modules/desktop-shell')
symlinkSync(join(repo, 'apps/desktop'), link, dirLinkType())
const report = relinkHarness(out, repo)
expect(report.removed).toBeGreaterThanOrEqual(1)
expect(report.errors).toEqual([])
expect(existsSync(link)).toBe(false)
expect(findEscapingLinks(out)).toEqual([])
})
it('drops a link that points at a missing in-tree target (unshipped package)', () => {
const { out } = makeTree()
// A link to `out/python/sdk-runtime` — inside the tree lexically but absent
// (python is not part of the harness): it cannot resolve, so it is dropped.
const link = join(out, 'node_modules/dsh-sdk-runtime')
symlinkSync('../python/sdk-runtime', link, dirLinkType())
const report = relinkHarness(out, dirname(out))
expect(report.removed).toBeGreaterThanOrEqual(1)
expect(report.errors).toEqual([])
expect(existsSync(link)).toBe(false)
expect(findEscapingLinks(out)).toEqual([])
})
it('reports a dangling link as an error and leaves it detectable', () => {
const { root, out } = makeTree()
const link = join(out, 'node_modules/ghost')
symlinkSync(join(root, 'does-not-exist'), link, dirLinkType())
const report = relinkHarness(out, dirname(out))
expect(report.errors.some(e => e.includes('dangling'))).toBe(true)
expect(findEscapingLinks(out)).toContain(link)
})
})
describe.skipIf(!canSymlink)('findEscapingLinks / assertSelfContained', () => {
it('flags an escaping link and assertSelfContained throws', () => {
const { repo, out } = makeTree()
const link = join(out, 'node_modules/outside')
symlinkSync(join(repo, 'packages/core/dsh-core'), link, 'dir')
expect(findEscapingLinks(out)).toContain(link)
expect(() => assertSelfContained(out)).toThrow(/self-contained/)
})
it('assertSelfContained passes on a clean tree', () => {
const { out } = makeTree()
expect(() => assertSelfContained(out)).not.toThrow()
})
})
describe('probeSymlinkSupport', () => {
it('reports a boolean without throwing', () => {
expect(typeof probeSymlinkSupport()).toBe('boolean')
})
})