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:
@@ -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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user