From 122da6fcff2fec00b4a87de59d4b5c2cf8f23d42 Mon Sep 17 00:00:00 2001 From: winewill <324803222+winewill@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:10:29 +0800 Subject: [PATCH] perf(windows): isolate packaging temporary files --- .github/workflows/windows-package.yml | 5 ++++ apps/desktop/packaging/README.i18n.yaml | 4 +-- apps/desktop/packaging/README.md | 2 +- apps/desktop/packaging/README.zh.md | 2 +- .../scripts/windows-build-workspace.d.mts | 4 +-- .../scripts/windows-build-workspace.mjs | 9 ++++--- scripts/windows-build-workspace.spec.ts | 5 +++- scripts/windows-package-workflow.spec.ts | 27 +++++++++++++++++++ 8 files changed, 47 insertions(+), 11 deletions(-) diff --git a/.github/workflows/windows-package.yml b/.github/workflows/windows-package.yml index e61bef0640..36dcc0a85d 100644 --- a/.github/workflows/windows-package.yml +++ b/.github/workflows/windows-package.yml @@ -171,7 +171,12 @@ jobs: working-directory: ${{ steps.build_workspace.outputs.directory }} env: PACKAGE_VERSION: ${{ steps.settings.outputs.version }} + PACKAGE_BUILD_TEMP: ${{ steps.build_workspace.outputs.temporaryDirectory }} run: | + if ($env:PACKAGE_BUILD_TEMP) { + $env:TEMP = $env:PACKAGE_BUILD_TEMP + $env:TMP = $env:PACKAGE_BUILD_TEMP + } $packageArgs = @('run', 'package:desktop:win:x64', '--build-version', $env:PACKAGE_VERSION) if ($env:PACKAGE_SIGNING -eq 'unsigned') { $packageArgs += '--unsigned' } pnpm @packageArgs diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index e432323ac3..3aea527fec 100644 --- a/apps/desktop/packaging/README.i18n.yaml +++ b/apps/desktop/packaging/README.i18n.yaml @@ -24,8 +24,8 @@ en: 5b4f0f80a117f202 zh: ff05be196c181087 /desktop-packaging/windows-actions-packaging: - en: 68d800ed10cac4a6 - zh: f72ac7e939af7ad5 + en: faf2b4cffb576757 + zh: b9c807eea360d53d /desktop-packaging/macos-self-hosted-packaging: en: a3af9523fdc60036 zh: 5e18d4740e9b7f57 diff --git a/apps/desktop/packaging/README.md b/apps/desktop/packaging/README.md index ccba7ac5ff..211dac96e5 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -113,7 +113,7 @@ Select the workflow in Actions, choose the source branch, preset, signing mode a Open the completed run in Actions and download its Artifacts. `windows-x64-signed---` and `windows-x64-unsigned---` contain the corresponding EXE; `windows-package-logs--` contains packaging diagnostics. The upload steps are `Retain verified signed installer`, `Retain verified unsigned installer`, and `Retain packaging diagnostics`; their step durations and timestamped logs show upload time. These artifacts are not a Release or download-site publication. -For path-sensitive Windows build measurements, `build_root` optionally selects an absolute directory outside existing Git checkouts. Each run clones the validated source commit into a new `windows-package--/source` directory; ignored files and previous build outputs are not copied. Dependency and signing caches retain their existing locations. Successful runs remove only their owned checkout after artifact uploads; failed runs retain it for inspection. Leave the input empty to build in the original Actions checkout. Include checkout creation and cleanup in the complete Job timing. +For path-sensitive Windows build measurements, `build_root` optionally selects an absolute directory outside existing Git checkouts. Each run clones the validated source commit into a new `windows-package--/source` directory; ignored files and previous build outputs are not copied. Only the packaging process uses a sibling run-owned `temp` directory through `TEMP` and `TMP`; Actions steps and post-job hooks retain their original temporary directories. Dependency and signing caches retain their existing locations. Successful runs remove only their owned checkout after artifact uploads; failed runs retain it for inspection. Leave the input empty to build in the original Actions checkout. Include checkout creation and cleanup in the complete Job timing. For cache measurements, `cache_namespace` selects a retained subdirectory for both dependency stores and upstream downloads; leave it empty for normal shared caches. Use a new name for a cold sample and repeat that name for warm samples. Names contain 1–64 lowercase letters, digits or hyphens, start with a letter or digit, and cannot be Windows device names. This does not reset the separate signature cache or OS file cache; report those states and Job start-to-completion time alongside phase timings and artifact size. diff --git a/apps/desktop/packaging/README.zh.md b/apps/desktop/packaging/README.zh.md index c07f58026c..154aac9ca4 100644 --- a/apps/desktop/packaging/README.zh.md +++ b/apps/desktop/packaging/README.zh.md @@ -113,7 +113,7 @@ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-s 在 Actions 打开完成的运行并下载 Artifacts。`windows-x64-signed---` 与 `windows-x64-unsigned---` 包含对应 EXE;`windows-package-logs--` 包含打包诊断。上传步骤名为 `Retain verified signed installer`、`Retain verified unsigned installer` 和 `Retain packaging diagnostics`;步骤耗时与带时间戳的日志显示上传用时。这些产物不等于发布 Release 或下载站内容。 -测量 Windows 构建路径对性能的影响时,可用 `build_root` 指定现有 Git checkout 之外的绝对目录。每轮任务将已验证的源码提交克隆到新的 `windows-package--/source` 目录;不会复制忽略文件或此前的构建输出。依赖缓存和签名缓存沿用原有位置。成功任务在产物上传后仅删除本轮所属的 checkout;失败任务保留该目录供检查。输入留空时使用原 Actions checkout。完整 Job 耗时须包含 checkout 创建和清理。 +测量 Windows 构建路径对性能的影响时,可用 `build_root` 指定现有 Git checkout 之外的绝对目录。每轮任务将已验证的源码提交克隆到新的 `windows-package--/source` 目录;不会复制忽略文件或此前的构建输出。仅打包进程通过 `TEMP` 和 `TMP` 使用同轮所属的同级 `temp` 目录;Actions 步骤及 Job 后置钩子保留原临时目录。依赖缓存和签名缓存沿用原有位置。成功任务在产物上传后仅删除本轮所属的 checkout;失败任务保留该目录供检查。输入留空时使用原 Actions checkout。完整 Job 耗时须包含 checkout 创建和清理。 测量缓存时,`cache_namespace` 为两个依赖 store 和上游下载选择保留的子目录;留空使用正常共享缓存。新名称用于冷缓存采样,再次使用同名用于热缓存采样。名称由 1–64 个小写字母、数字或连字符组成,以字母或数字开头,且不能为 Windows 设备名。这不会重置独立的签名缓存或操作系统文件缓存;应将其状态、Job 开始至完成的总耗时与阶段耗时和产物体积一并报告。 diff --git a/apps/desktop/scripts/windows-build-workspace.d.mts b/apps/desktop/scripts/windows-build-workspace.d.mts index a9037d78c1..92f3a3c704 100644 --- a/apps/desktop/scripts/windows-build-workspace.d.mts +++ b/apps/desktop/scripts/windows-build-workspace.d.mts @@ -9,9 +9,9 @@ export interface WindowsBuildWorkspaceOptions { /** * Clone the clean selected commit into a new run-owned directory. * @param options - Original checkout and Actions run identity. - * @returns Checkout to use for packaging. + * @returns Checkout and temporary directory owned by this run. */ -export function prepareWindowsBuildWorkspace(options: WindowsBuildWorkspaceOptions): { directory: string } +export function prepareWindowsBuildWorkspace(options: WindowsBuildWorkspaceOptions): { directory: string; temporaryDirectory: string } /** * Remove the directory only when its ownership record matches the run. * @param options - Original checkout and Actions run identity. diff --git a/apps/desktop/scripts/windows-build-workspace.mjs b/apps/desktop/scripts/windows-build-workspace.mjs index a15ade90dd..c4048bf377 100644 --- a/apps/desktop/scripts/windows-build-workspace.mjs +++ b/apps/desktop/scripts/windows-build-workspace.mjs @@ -30,7 +30,7 @@ function locations(options) { } const owner = join(root, `windows-package-${runId}-${runAttempt}`) assertUnlinkedAncestors(owner) - return { root, owner, directory: join(owner, 'source') } + return { root, owner, directory: join(owner, 'source'), temporaryDirectory: join(owner, 'temp') } } function git(directory, args) { @@ -44,7 +44,7 @@ function ownership(options, paths) { /** * Clone the validated commit into a new run-owned checkout; failures retain diagnostics. * @param {{sourceDirectory: string, buildRoot: string, runId: string, runAttempt: string, sourceSha: string}} options - Source and run identity. - * @returns {{directory: string}} Checkout to use for packaging. + * @returns {{directory: string, temporaryDirectory: string}} Checkout and temporary directory owned by this run. */ export function prepareWindowsBuildWorkspace(options) { const paths = locations(options) @@ -54,12 +54,13 @@ export function prepareWindowsBuildWorkspace(options) { mkdirSync(paths.root, { recursive: true }) mkdirSync(paths.owner) writeFileSync(join(paths.owner, 'owner.json'), ownership(options, paths), { flag: 'wx' }) + mkdirSync(paths.temporaryDirectory) git(paths.owner, ['clone', '--no-hardlinks', '--no-checkout', resolve(options.sourceDirectory), paths.directory]) git(paths.directory, ['checkout', '--detach', options.sourceSha]) if (git(paths.directory, ['rev-parse', 'HEAD']) !== options.sourceSha || git(paths.directory, ['status', '--porcelain'])) { throw new Error('Isolated packaging checkout does not match the selected commit') } - return { directory: paths.directory } + return { directory: paths.directory, temporaryDirectory: paths.temporaryDirectory } } /** @@ -89,6 +90,6 @@ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1] else { if (process.argv.length !== 2 || !process.env.GITHUB_OUTPUT) throw new Error('Expected Actions output file and no arguments') const result = prepareWindowsBuildWorkspace(options) - appendFileSync(process.env.GITHUB_OUTPUT, `directory=${result.directory}\n`) + appendFileSync(process.env.GITHUB_OUTPUT, `directory=${result.directory}\ntemporaryDirectory=${result.temporaryDirectory}\n`) } } diff --git a/scripts/windows-build-workspace.spec.ts b/scripts/windows-build-workspace.spec.ts index f67ee577d5..aac6bf182f 100644 --- a/scripts/windows-build-workspace.spec.ts +++ b/scripts/windows-build-workspace.spec.ts @@ -40,7 +40,9 @@ it('clones the exact clean commit and removes only its run-owned checkout', () = stdio: 'pipe', }) const directory = join(options.buildRoot, 'windows-package-123-1', 'source') - expect(readFileSync(output, 'utf8')).toBe(`directory=${directory}\n`) + const temporaryDirectory = join(options.buildRoot, 'windows-package-123-1', 'temp') + expect(readFileSync(output, 'utf8')).toBe(`directory=${directory}\ntemporaryDirectory=${temporaryDirectory}\n`) + writeFileSync(join(temporaryDirectory, 'temporary.txt'), 'owned temporary data') expect(readFileSync(join(directory, 'source.txt'), 'utf8')).toBe('selected source') expect(existsSync(join(directory, 'private.env'))).toBe(false) expect(() => prepareWindowsBuildWorkspace(options)).toThrow() @@ -52,6 +54,7 @@ it('clones the exact clean commit and removes only its run-owned checkout', () = symlinkSync(external, join(directory, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir') removeWindowsBuildWorkspace(options) expect(existsSync(directory)).toBe(false) + expect(existsSync(temporaryDirectory)).toBe(false) expect(readFileSync(join(external, 'keep.txt'), 'utf8')).toBe('keep') expect(readFileSync(join(sourceDirectory, 'source.txt'), 'utf8')).toBe('selected source') const linked = join(root, 'linked') diff --git a/scripts/windows-package-workflow.spec.ts b/scripts/windows-package-workflow.spec.ts index bf2109c6b4..59d01fb894 100644 --- a/scripts/windows-package-workflow.spec.ts +++ b/scripts/windows-package-workflow.spec.ts @@ -160,6 +160,33 @@ it('routes all packaging consumers to the selected workspace and removes it only } }) +it('scopes the optional temporary directory to installer building without changing Actions or later steps', () => { + const build = job.steps.find(step => step.name === 'Build installer and verify packaged runtime')! + expect(build.env?.PACKAGE_BUILD_TEMP).toBe('${{ steps.build_workspace.outputs.temporaryDirectory }}') + expect(build.run).toContain([ + 'if ($env:PACKAGE_BUILD_TEMP) {', + ' $env:TEMP = $env:PACKAGE_BUILD_TEMP', + ' $env:TMP = $env:PACKAGE_BUILD_TEMP', + '}', + ].join('\n')) + expect(build.run!.indexOf('if ($env:PACKAGE_BUILD_TEMP)')).toBeLessThan(build.run!.indexOf('$packageArgs =')) + expect(build.run).not.toContain('GITHUB_ENV') + expect(build.run).not.toContain('RUNNER_TEMP') + expect(job.env).not.toHaveProperty('TEMP') + expect(job.env).not.toHaveProperty('TMP') + expect(job.env).not.toHaveProperty('PACKAGE_BUILD_TEMP') + for (const step of job.steps.filter(step => step !== build)) { + expect(step.env ?? {}).not.toHaveProperty('PACKAGE_BUILD_TEMP') + expect(step.env ?? {}).not.toHaveProperty('TEMP') + expect(step.env ?? {}).not.toHaveProperty('TMP') + expect(step.run ?? '').not.toMatch(/\$env:(?:TEMP|TMP)\s*=/u) + } + const selected = job.steps.find(step => step.id === 'build_workspace')! + const defaultBranch = selected.run!.split('} else {')[0]! + expect(defaultBranch).toContain('"directory=$env:GITHUB_WORKSPACE" >> $env:GITHUB_OUTPUT') + expect(defaultBranch).not.toContain('temporaryDirectory') +}) + it.skipIf(process.platform !== 'win32')('keeps the dependency store stable across run-specific pnpm homes', () => { const directory = mkdtempSync(resolve(tmpdir(), 'windows-package-store-')) try {