perf(windows): prepare Electron and primary runtime concurrently
This commit is contained in:
1 parent
eda31bfc29
commit
73574aff59
5 files changed
+113
-7
No files matched your search
@@ -48,8 +48,8 @@
|
||||
en: d87a53ab0f8b0db9
|
||||
zh: c3b2b96085f29d3b
|
||||
/desktop-packaging/desktop-runtime-preparation:
|
||||
en: bce2ee195a876985
|
||||
zh: ae45df21c23e86c2
|
||||
en: 59783a64552d4464
|
||||
zh: d7876d51b4015578
|
||||
/desktop-packaging/installation-and-release-verification:
|
||||
en: eb35eb973b796643
|
||||
zh: c1c52311632b7296
|
||||
|
||||
@@ -270,6 +270,8 @@ NSIS removes its temporary extraction tree during installation, before the compl
|
||||
|
||||
## Desktop runtime preparation
|
||||
|
||||
Windows prepares Electron and primary-runtime concurrently; macOS prepares them serially. CLI preparation and `versions.json` publication require both payloads to succeed. If either Windows payload fails, preparation waits for the other to settle before reporting failure and publishes neither CLI resources nor versions. Nested preparation timings overlap and must not be added together.
|
||||
|
||||
`DSH_DESKTOP_PACKAGE_CACHE_DIR` in the packaging process environment selects a nonempty absolute directory for shared `downloads` and `pnpm-store` caches; omission uses `.desktop-build`. Temporary installation projects, lockfiles and configuration remain separate per run. Runtime installation verifies store integrity and copies package files so later signing cannot modify cached bytes; cleanup retains these caches. Windows Actions uses `runner.tool_cache/dsh-desktop-package-cache`, while repository dependency installation uses a separate `.pnpm-store` at the workspace drive root. Both installation logs identify their store and report package reuse. A fresh runtime lockfile still resolves dependencies on every run.
|
||||
|
||||
Desktop [patch policy](../scripts/runtime-patch-policy.ts) explicitly classifies every workspace patch as shared, workspace-only, or runtime-only. Shared entries reuse the root patch bytes and verify their hash against the root lockfile; runtime-only entries use separate repository files without changing workspace installation. Workspace-only patches belong to build tools or dependencies already embedded in client bundles. Unclassified or stale entries, missing files, changed hashes, incompatible resolved versions, and workspace-only packages entering the runtime stop packaging. The [preparation script](../scripts/prepare-runtime-patches.ts) writes only to the temporary project; pnpm applies the selected patches and rejects application failures.
|
||||
|
||||
@@ -270,6 +270,8 @@ NSIS 在安装阶段清理临时解压目录,完成后才显示完成页或自
|
||||
|
||||
## Desktop 运行时准备
|
||||
|
||||
Windows 并行准备 Electron 和 primary-runtime;macOS 串行准备二者。只有两份运行时资源均成功,才会准备 CLI 并写入 `versions.json`。任一 Windows 运行时准备失败时,流程会等待另一路结束后再报告失败,且不写入 CLI 资源或版本记录。嵌套准备阶段的计时存在重叠,不能直接相加。
|
||||
|
||||
打包进程环境中的 `DSH_DESKTOP_PACKAGE_CACHE_DIR` 为共享的 `downloads` 和 `pnpm-store` 缓存指定非空绝对目录;省略时使用 `.desktop-build`。临时安装项目、lockfile 和配置仍逐次独立创建。运行时安装校验 store 完整性并复制包文件,防止后续签名修改缓存字节;清理时保留这些缓存。Windows Actions 使用 `runner.tool_cache/dsh-desktop-package-cache`,仓库依赖安装则使用工作区盘根目录下独立的 `.pnpm-store`。两次安装日志均标明各自的 store 并报告包复用情况。每次运行仍通过新的运行时 lockfile 解析依赖。
|
||||
|
||||
Desktop [补丁策略](../scripts/runtime-patch-policy.ts) 将每份工作区补丁明确归为共享、仅工作区或仅运行时。共享项复用根目录补丁字节,并对照根锁文件校验 hash;仅运行时项使用仓库内的独立文件,不改变工作区安装。仅工作区补丁用于构建工具或已嵌入客户端 bundle 的依赖。未分类或过期的条目、文件缺失、hash 改变、解析版本不兼容,以及仅工作区包进入运行时,都会阻止打包。[准备脚本](../scripts/prepare-runtime-patches.ts) 只写入临时项目;pnpm 负责应用选中的补丁,应用失败时会报错。
|
||||
|
||||
@@ -53,9 +53,18 @@ async function main(): Promise<void> {
|
||||
const target = desktopTargetPlatform(resolveDesktopBuildTarget())
|
||||
rmSync(RUNTIME_ROOT, { recursive: true, force: true })
|
||||
mkdirSync(RUNTIME_ROOT, { recursive: true })
|
||||
const nodeVersion = await prepareElectron(target)
|
||||
await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:primary-runtime',
|
||||
const preparePrimary = () => packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:primary-runtime',
|
||||
() => preparePrimaryRuntime({ deferSmoke: values['defer-primary-runtime-smoke'] }))
|
||||
let nodeVersion: string
|
||||
if (target.platform === 'win32') {
|
||||
const [electron, primary] = await Promise.allSettled([prepareElectron(target), preparePrimary()])
|
||||
if (electron.status === 'rejected') throw electron.reason
|
||||
if (primary.status === 'rejected') throw primary.reason
|
||||
nodeVersion = electron.value
|
||||
} else {
|
||||
nodeVersion = await prepareElectron(target)
|
||||
await preparePrimary()
|
||||
}
|
||||
const { pnpm } = await readPrimaryRuntime(join(RUNTIME_ROOT, 'primary-runtime'))
|
||||
if (pnpm === undefined) throw new Error('desktop runtime: primary-runtime manifest has no pnpm version')
|
||||
await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:cli', async () => { prepareCli(target) })
|
||||
|
||||
@@ -12,6 +12,7 @@ const state = vi.hoisted(() => ({
|
||||
pnpm: '11.7.0' as string | undefined,
|
||||
failure: undefined as 'electron' | 'primary' | 'cli' | undefined,
|
||||
preparePrimary: vi.fn(),
|
||||
extractElectron: vi.fn(),
|
||||
commandLink: vi.fn(),
|
||||
}))
|
||||
vi.mock('../scripts/desktop-build-paths.mjs', async importOriginal => ({
|
||||
@@ -22,9 +23,7 @@ vi.mock('../scripts/desktop-build-paths.mjs', async importOriginal => ({
|
||||
}),
|
||||
}))
|
||||
vi.mock('@electron/get', () => ({ downloadArtifact: async () => 'electron.zip' }))
|
||||
vi.mock('extract-zip', () => ({ default: async () => {
|
||||
if (state.failure === 'electron') throw new Error('Electron extraction failed')
|
||||
} }))
|
||||
vi.mock('extract-zip', () => ({ default: state.extractElectron }))
|
||||
vi.mock('node:child_process', async importOriginal => ({
|
||||
...await importOriginal<typeof import('node:child_process')>(),
|
||||
execFileSync: (file: string) => file === '/usr/libexec/PlistBuddy' ? '12.0\n' : '24.18.1\n',
|
||||
@@ -54,6 +53,9 @@ beforeEach(() => {
|
||||
state.failure = undefined
|
||||
process.argv = [process.execPath, 'prepare-runtime.ts']
|
||||
vi.stubEnv('DSH_DESKTOP_PACKAGING_RUN_DIR', undefined)
|
||||
state.extractElectron.mockImplementation(async () => {
|
||||
if (state.failure === 'electron') throw new Error('Electron extraction failed')
|
||||
})
|
||||
state.preparePrimary.mockImplementation(async () => {
|
||||
if (state.failure === 'primary') throw new Error('primary preparation failed')
|
||||
const root = join(state.root, 'runtime', 'primary-runtime')
|
||||
@@ -118,3 +120,94 @@ it.each([
|
||||
await expect(import('../scripts/prepare-runtime.ts')).rejects.toThrow(error)
|
||||
expect(existsSync(join(state.root, 'runtime', 'versions.json'))).toBe(false)
|
||||
})
|
||||
|
||||
it.each(['electron', 'primary'] as const)('overlaps Windows payloads and waits for %s to finish last before publishing CLI and versions', async (last) => {
|
||||
state.target = 'win-x64'
|
||||
const electron = Promise.withResolvers<undefined>()
|
||||
const primary = Promise.withResolvers<undefined>()
|
||||
const started = new Set<string>()
|
||||
const finished = new Set<string>()
|
||||
const preparePrimary = state.preparePrimary.getMockImplementation()!
|
||||
state.extractElectron.mockImplementation(async () => {
|
||||
started.add('electron')
|
||||
await electron.promise
|
||||
finished.add('electron')
|
||||
})
|
||||
state.preparePrimary.mockImplementation(async () => {
|
||||
started.add('primary')
|
||||
await primary.promise
|
||||
await preparePrimary()
|
||||
finished.add('primary')
|
||||
})
|
||||
const outcome = import('../scripts/prepare-runtime.ts').then(() => undefined, (error: unknown) => error)
|
||||
try {
|
||||
await vi.waitFor(() => { expect([...started].sort()).toEqual(['electron', 'primary']) }, { timeout: 5000 })
|
||||
;(last === 'electron' ? primary : electron).resolve(undefined)
|
||||
await vi.waitFor(() => { expect(finished.size).toBe(1) }, { timeout: 5000 })
|
||||
expect(existsSync(join(state.root, 'runtime', 'cli'))).toBe(false)
|
||||
expect(existsSync(join(state.root, 'runtime', 'versions.json'))).toBe(false)
|
||||
;(last === 'electron' ? electron : primary).resolve(undefined)
|
||||
expect(await outcome).toBeUndefined()
|
||||
expect(existsSync(join(state.root, 'runtime', 'cli', 'command-manager.js'))).toBe(true)
|
||||
expect(existsSync(join(state.root, 'runtime', 'versions.json'))).toBe(true)
|
||||
} finally {
|
||||
electron.resolve(undefined)
|
||||
primary.resolve(undefined)
|
||||
await outcome
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['electron', 'primary'] as const)('drains the sibling after Windows %s failure without publishing CLI or versions', async (failed) => {
|
||||
state.target = 'win-x64'
|
||||
const electron = Promise.withResolvers<undefined>()
|
||||
const primary = Promise.withResolvers<undefined>()
|
||||
const started = new Set<string>()
|
||||
const completed = new Set<string>()
|
||||
const failure = new Error(`${failed} payload failed`)
|
||||
const prepare = async (name: 'electron' | 'primary', barrier: Promise<undefined>) => {
|
||||
started.add(name)
|
||||
try { await barrier } finally { completed.add(name) }
|
||||
}
|
||||
state.extractElectron.mockImplementation(() => prepare('electron', electron.promise))
|
||||
state.preparePrimary.mockImplementation(() => prepare('primary', primary.promise))
|
||||
const outcome = import('../scripts/prepare-runtime.ts').then(
|
||||
() => ({ error: undefined, completed: [...completed].sort() }),
|
||||
(error: unknown) => ({ error, completed: [...completed].sort() }),
|
||||
)
|
||||
try {
|
||||
await vi.waitFor(() => { expect([...started].sort()).toEqual(['electron', 'primary']) }, { timeout: 5000 })
|
||||
;(failed === 'electron' ? electron : primary).reject(failure)
|
||||
await vi.waitFor(() => { expect(completed.has(failed)).toBe(true) }, { timeout: 5000 })
|
||||
expect(existsSync(join(state.root, 'runtime', 'cli'))).toBe(false)
|
||||
expect(existsSync(join(state.root, 'runtime', 'versions.json'))).toBe(false)
|
||||
;(failed === 'electron' ? primary : electron).resolve(undefined)
|
||||
expect(await outcome).toEqual({ error: failure, completed: ['electron', 'primary'] })
|
||||
expect(existsSync(join(state.root, 'runtime', 'cli'))).toBe(false)
|
||||
expect(existsSync(join(state.root, 'runtime', 'versions.json'))).toBe(false)
|
||||
} finally {
|
||||
electron.resolve(undefined)
|
||||
primary.resolve(undefined)
|
||||
await outcome
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['mac-arm64', 'mac-x64'] as const)('keeps %s primary preparation after Electron extraction', async (target) => {
|
||||
state.target = target
|
||||
const electron = Promise.withResolvers<undefined>()
|
||||
let extracting = false
|
||||
state.extractElectron.mockImplementation(async () => {
|
||||
extracting = true
|
||||
await electron.promise
|
||||
})
|
||||
const outcome = import('../scripts/prepare-runtime.ts').then(() => undefined, (error: unknown) => error)
|
||||
try {
|
||||
await vi.waitFor(() => { expect(extracting).toBe(true) }, { timeout: 5000 })
|
||||
expect(state.preparePrimary).not.toHaveBeenCalled()
|
||||
electron.resolve(undefined)
|
||||
expect(await outcome).toBeUndefined()
|
||||
expect(state.preparePrimary).toHaveBeenCalledOnce()
|
||||
} finally {
|
||||
electron.resolve(undefined)
|
||||
await outcome
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user