fix(desktop): prepare and sign usable bundled runtimes
This commit is contained in:
1 parent
5105c77706
commit
9413105c31
16 files changed
+171
-48
No files matched your search
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.md
|
||||
2026-09-14-desktop-primary-runtime.md: bf9e8a66a563d632477ff3bb88ab7a8754325113
|
||||
2026-09-14-desktop-primary-runtime.zh.md: 4caa7242d3b31efdb9dd502d0cf3ee791f5c9852
|
||||
2026-09-14-desktop-primary-runtime.md: 54dfba5d786b0e557f3e52b104a89e53fb091ecb
|
||||
2026-09-14-desktop-primary-runtime.zh.md: f57f2dbf5efdda9c09aefa3fd78cb9f0f2a7b8ea
|
||||
@@ -14,6 +14,10 @@ Desktop ships Python, Node.js, pnpm, numpy and pandas as one release-bound paylo
|
||||
|
||||
The application version and component versions live in `runtime.json`, not the directory name. Installation publishes a completed staged copy and retains the previous directory until replacement succeeds. Matching releases reuse installed files; upgrades replace user-added Python dependencies inside the managed tree. The Desktop single-instance owner and the tool's shared installation promise serialize normal installation requests.
|
||||
|
||||
Node downloads and hash-verifies the complete locked wheel set and unpacks these library-only archives into site-packages. This avoids build-host Python and pip version selection without implementing dependency resolution or general wheel installation. Wheels with `.data` installation directories are rejected; command-line entry-point wrappers are outside this library payload. Native smoke executes the final payload after temporary files are removed, so interpreter links must survive relocation.
|
||||
|
||||
macOS grants `com.apple.security.cs.allow-jit` only to the standalone Node executable. Hardened-runtime signing without that entitlement prevents V8 from allocating its code region. Interpreter and library smoke checks run after signing as well as after staging cleanup; a valid signature alone does not establish executable behavior.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**System interpreters only.** They do not provide predictable availability or preinstalled numpy and pandas.
|
||||
|
||||
@@ -14,6 +14,10 @@ Desktop 将 Python、Node.js、pnpm、numpy 和 pandas 作为绑定应用版本
|
||||
|
||||
应用版本和组件版本记录在 `runtime.json` 中,不放在目录名里。安装发布完整的暂存副本,并在替换成功前保留之前的目录。同版本复用已安装文件;升级替换受管目录内用户添加的 Python 依赖。Desktop 单实例所有者和工具共享的安装 Promise 串行处理正常安装请求。
|
||||
|
||||
Node 下载并校验完整锁定 wheel 集的哈希,将这些仅含库的压缩包解压到 site-packages。这避免选择构建主机的 Python 和 pip 版本,也无需实现依赖解析或通用 wheel 安装。含 `.data` 安装目录的 wheel 会被拒绝;命令行入口包装器不属于该库产物。本机 smoke 在临时文件删除后执行最终产物,因此解释器链接必须在迁移后仍有效。
|
||||
|
||||
macOS 仅向独立 Node 可执行文件授予 `com.apple.security.cs.allow-jit`。缺少此权限的强化运行时签名会阻止 V8 分配代码区域。解释器和库的 smoke 检查在签名后以及暂存清理后执行;签名有效本身不能证明程序可运行。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**只使用系统解释器。** 无法保证可用性或预装 numpy 和 pandas。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write apps/desktop/README.md
|
||||
README.md: 957f8362c9bf3ff24e16088a58de8ad9234fd95d
|
||||
README.zh.md: 55986ddf0cfeaacd6592d8653b6c132bc6f0a149
|
||||
README.md: a03acd3f4bfc717c6c4daa212d990e635bf15180
|
||||
README.zh.md: e0262df89cf36ee60ad801f2b6739d4dc5c97111
|
||||
@@ -20,7 +20,7 @@ The payload follows the Desktop release. `runtime.json` records the Desktop vers
|
||||
|
||||
This tool does not change PATH, environment variables or user package-manager configuration. pnpm retains its own defaults and user settings for global packages, executable entries and its store, including native errors when the environment does not support global installation. There is no separate dependency updater. [The primary-runtime decision](../../.agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.md) records these choices.
|
||||
|
||||
Build preparation uses system Python with pip (`python` on Windows, `python3` on macOS) to install target wheels; that build interpreter is not shipped. It pins interpreter archive hashes in [the download lock](scripts/primary-runtime-lock.json) and Python wheel hashes in [the requirements lock](scripts/primary-runtime-requirements.txt); pnpm follows the Desktop build dependency lock. Native-target preparation checks interpreter execution and numpy/pandas operations. Cross-target execution and signed installation require the target release host. Both `dev:desktop` and `start:desktop` prepare `.desktop-build/targets/<target>/runtime/primary-runtime` before launching Electron; first use may download locked dependencies.
|
||||
Node prepares the bundled interpreters and Python libraries without a system Python or pip. [The download lock](scripts/primary-runtime-lock.json) pins interpreter archives and target-specific wheel URLs and hashes; pnpm follows the Desktop build dependency lock. The supported library wheels unpack directly into site-packages; wheels requiring other installation directories are rejected, and package command-line wrappers are not generated. Native-target checks execute the bundled interpreters and numpy/pandas operations after staging cleanup and again after macOS signing. The standalone Node executable receives the JIT entitlement required by V8. Cross-target execution and signed installation require the target release host. Both `dev:desktop` and `start:desktop` prepare `.desktop-build/targets/<target>/runtime/primary-runtime` before launching Electron; first use may download locked dependencies.
|
||||
|
||||
| Decision | Why | Direct consequence |
|
||||
|---|---|---|
|
||||
|
||||
@@ -20,7 +20,7 @@ Desktop 携带独立的 Python、Node.js 和 pnpm 分发包,并在 Python 的
|
||||
|
||||
该工具不修改 PATH、环境变量或用户包管理器配置。pnpm 的全局包、命令入口和 store 保留自身默认值及用户设置,包括环境不支持全局安装时的原生错误。不提供独立依赖更新器。[第一方 Runtime 决策](../../.agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.zh.md)记录这些选择。
|
||||
|
||||
构建准备使用带 pip 的系统 Python(Windows 为 `python`,macOS 为 `python3`)安装目标 wheel,该构建解释器不随包交付。它通过[下载锁](scripts/primary-runtime-lock.json)固定解释器压缩包哈希,通过[依赖锁](scripts/primary-runtime-requirements.txt)固定 Python wheel 哈希;pnpm 使用 Desktop 构建依赖锁。本机目标的准备流程检查解释器执行及 numpy/pandas 运算。跨目标执行和签名安装需要对应的发布主机。`dev:desktop` 和 `start:desktop` 都会在启动 Electron 前准备 `.desktop-build/targets/<target>/runtime/primary-runtime`;首次准备可能需要下载锁定的依赖。
|
||||
Node 准备内置解释器和 Python 库,无需系统 Python 或 pip。[下载锁](scripts/primary-runtime-lock.json)固定解释器压缩包及目标平台 wheel 的 URL 和哈希;pnpm 使用 Desktop 构建依赖锁。支持的库 wheel 直接解压到 site-packages;需要其他安装目录的 wheel 会被拒绝,不生成包的命令行包装器。本机目标检查在清理暂存目录后以及 macOS 签名后执行内置解释器及 numpy/pandas 运算。独立 Node 可执行文件获得 V8 所需的 JIT 权限。跨目标执行和签名安装需要对应的发布主机。`dev:desktop` 和 `start:desktop` 都会在启动 Electron 前准备 `.desktop-build/targets/<target>/runtime/primary-runtime`;首次准备可能需要下载锁定的依赖。
|
||||
|
||||
| 决策 | 原因 | 直接结果 |
|
||||
|---|---|---|
|
||||
|
||||
@@ -32,7 +32,9 @@ export async function signMacOSRuntime(root: string, appId: string, expected: Ma
|
||||
const path = files[next++]
|
||||
if (path === undefined) return
|
||||
const identifier = `${appId}.runtime.${createHash('sha256').update(path).digest('hex')}`
|
||||
await signMacOSRuntimeCode(join(root, path), identifier, expected)
|
||||
const entitlements = path === 'dependencies/node/bin/node'
|
||||
? join(import.meta.dirname, 'node-entitlements.plist') : undefined
|
||||
await signMacOSRuntimeCode(join(root, path), identifier, expected, entitlements)
|
||||
verifyMacOSRuntimeCode(join(root, path), expected)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict><key>com.apple.security.cs.allow-jit</key><true/></dict></plist>
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
readDesktopCorePackageSet,
|
||||
verifyDesktopCoreLockfile,
|
||||
} from '../src/core-package-set.ts'
|
||||
import { smokePrimaryRuntime } from './prepare-primary-runtime.ts'
|
||||
import { smokeDesktopRuntime } from './smoke-runtime.ts'
|
||||
import { writeDesktopRuntime, verifyDesktopRuntime } from '../src/runtime-tree.ts'
|
||||
import {
|
||||
@@ -140,6 +141,7 @@ async function main(): Promise<void> {
|
||||
await signMacOSRuntime(DSH_OUTPUT_ROOT, resolveDesktopAppId(process.env), resolveMacOSSigningEnvironment(process.env))
|
||||
await signMacOSRuntime(join(RUNTIME_ROOT, 'primary-runtime'), resolveDesktopAppId(process.env), resolveMacOSSigningEnvironment(process.env))
|
||||
}
|
||||
smokePrimaryRuntime(join(RUNTIME_ROOT, 'primary-runtime'))
|
||||
writeDesktopRuntime(DSH_OUTPUT_ROOT, release, packageSet.packages.map(entry => entry.name), target)
|
||||
const descriptor = await verifyDesktopRuntime(DSH_OUTPUT_ROOT, release.version, target)
|
||||
await new Promise<void>((accept, reject) => {
|
||||
|
||||
@@ -3,17 +3,24 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { cp } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import extractZip from 'extract-zip'
|
||||
import { x as extractTar } from 'tar'
|
||||
import { workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../../desktop-host/src/primary-runtime.ts'
|
||||
import { resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
|
||||
import lock from './primary-runtime-lock.json' with { type: 'json' }
|
||||
|
||||
async function download(url: string, sha256: string, cache: string): Promise<string> {
|
||||
/**
|
||||
* Download or reuse an archive only when its bytes match the release lock.
|
||||
* @param url - Locked archive URL.
|
||||
* @param sha256 - Expected SHA-256 digest.
|
||||
* @param cache - Download cache directory.
|
||||
* @returns Verified local archive path.
|
||||
*/
|
||||
export async function downloadPrimaryRuntimeAsset(url: string, sha256: string, cache: string): Promise<string> {
|
||||
const destination = join(cache, sha256)
|
||||
let bytes: Buffer
|
||||
try { bytes = readFileSync(destination) } catch (error) {
|
||||
@@ -30,15 +37,31 @@ async function download(url: string, sha256: string, cache: string): Promise<str
|
||||
async function pythonArchive(target: keyof typeof lock.targets, cache: string): Promise<string> {
|
||||
const artifact = lock.targets[target]
|
||||
const filename = `cpython-${lock.pythonVersion}+${lock.pythonRelease}-${artifact.pythonTarget}-install_only_stripped.tar.gz`
|
||||
return download(`https://github.com/astral-sh/python-build-standalone/releases/download/${lock.pythonRelease}/${encodeURIComponent(filename)}`, artifact.pythonSha256, cache)
|
||||
return downloadPrimaryRuntimeAsset(`https://github.com/astral-sh/python-build-standalone/releases/download/${lock.pythonRelease}/${encodeURIComponent(filename)}`, artifact.pythonSha256, cache)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpack a locked library wheel whose files all belong in site-packages.
|
||||
* @param archive - Hash-verified wheel archive.
|
||||
* @param destination - Absolute site-packages directory.
|
||||
* @returns Resolves after extraction; rejects wheels requiring installation into other directories.
|
||||
*/
|
||||
export async function unpackPrimaryRuntimeWheel(archive: string, destination: string): Promise<void> {
|
||||
await extractZip(archive, {
|
||||
dir: destination,
|
||||
onEntry: (entry) => {
|
||||
if (entry.fileName.split('/')[0]?.endsWith('.data')) {
|
||||
throw new Error(`primary runtime: wheel requires unsupported installation paths: ${entry.fileName}`)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialize the selected Desktop target's primary runtime in its build resources.
|
||||
* @param python - Build-host Python for wheel installation; defaults to the system command and is never copied into the payload.
|
||||
* @returns Resolves after dependency installation and native-target execution checks.
|
||||
*/
|
||||
export async function preparePrimaryRuntime(python?: string): Promise<void> {
|
||||
export async function preparePrimaryRuntime(): Promise<void> {
|
||||
const target = resolveDesktopBuildTarget()
|
||||
const paths = resolveDesktopTargetBuildPaths()
|
||||
const artifact = lock.targets[target]
|
||||
@@ -50,7 +73,7 @@ export async function preparePrimaryRuntime(python?: string): Promise<void> {
|
||||
const dependencies = join(output, 'dependencies')
|
||||
mkdirSync(dependencies, { recursive: true })
|
||||
const nodeFilename = `node-v${lock.nodeVersion}-${artifact.nodeArchive}`
|
||||
const nodeArchive = await download(`https://nodejs.org/dist/v${lock.nodeVersion}/${nodeFilename}`, artifact.nodeSha256, paths.downloads)
|
||||
const nodeArchive = await downloadPrimaryRuntimeAsset(`https://nodejs.org/dist/v${lock.nodeVersion}/${nodeFilename}`, artifact.nodeSha256, paths.downloads)
|
||||
const unpackedNode = join(staging, 'node')
|
||||
mkdirSync(unpackedNode)
|
||||
if (target === 'win-x64') await extractZip(nodeArchive, { dir: unpackedNode })
|
||||
@@ -66,7 +89,7 @@ export async function preparePrimaryRuntime(python?: string): Promise<void> {
|
||||
const require = createRequire(import.meta.url)
|
||||
const pnpmManifest = require.resolve('pnpm')
|
||||
const pnpm = JSON.parse(readFileSync(pnpmManifest, 'utf8')) as { version: string }
|
||||
cpSync(dirname(pnpmManifest), join(dependencies, 'pnpm'), { recursive: true, dereference: true })
|
||||
await cp(dirname(pnpmManifest), join(dependencies, 'pnpm'), { recursive: true, dereference: true })
|
||||
const desktop = JSON.parse(readFileSync(join(import.meta.dirname, '..', 'package.json'), 'utf8')) as { version: string }
|
||||
const manifest: PrimaryRuntimeManifest = {
|
||||
desktopVersion: desktop.version,
|
||||
@@ -78,28 +101,31 @@ export async function preparePrimaryRuntime(python?: string): Promise<void> {
|
||||
},
|
||||
}
|
||||
const entries = workspaceDependencyPaths(output, manifest)
|
||||
const buildPython = python ?? (process.platform === 'win32' ? 'python' : 'python3')
|
||||
const nativeTarget = resolveDesktopBuildTarget({}, process.platform, process.arch)
|
||||
const installedPackages = join(staging, 'python-packages')
|
||||
execFileSync(buildPython, ['-I', '-m', 'pip', '--isolated', 'install', '--disable-pip-version-check', '--no-compile',
|
||||
'--only-binary=:all:', '--require-hashes', '--platform', artifact.wheelPlatform, '--python-version', '3.12', '--implementation', 'cp', '--abi', 'cp312',
|
||||
'--target', installedPackages, '-r', join(import.meta.dirname, 'primary-runtime-requirements.txt')], { stdio: 'inherit' })
|
||||
cpSync(installedPackages, entries.pythonPackages, { recursive: true })
|
||||
writeFileSync(join(output, 'runtime.json'), `${JSON.stringify(manifest, undefined, 2)}\n`)
|
||||
if (nativeTarget === target) {
|
||||
execFileSync(entries.python, ['-I', '-c', 'import numpy, pandas; assert numpy.arange(4).sum() == 6; assert pandas.DataFrame({"n": [1, 2]}).n.sum() == 3'], { stdio: 'inherit' })
|
||||
execFileSync(entries.node, ['-e', `if (process.versions.node !== ${JSON.stringify(lock.nodeVersion)}) process.exit(1)`], { stdio: 'inherit' })
|
||||
execFileSync(entries.node, [entries.pnpm, '--version'], { stdio: 'inherit' })
|
||||
for (const wheel of [...artifact.wheels, ...lock.wheels]) {
|
||||
await unpackPrimaryRuntimeWheel(await downloadPrimaryRuntimeAsset(wheel.url, wheel.sha256, paths.downloads), entries.pythonPackages)
|
||||
}
|
||||
writeFileSync(join(output, 'runtime.json'), `${JSON.stringify(manifest, undefined, 2)}\n`)
|
||||
const destination = join(paths.runtime, 'primary-runtime')
|
||||
rmSync(destination, { recursive: true, force: true })
|
||||
cpSync(output, destination, { recursive: true, dereference: true })
|
||||
await cp(output, destination, { recursive: true, dereference: true })
|
||||
} finally {
|
||||
rmSync(staging, { recursive: true, force: true })
|
||||
}
|
||||
smokePrimaryRuntime(join(paths.runtime, 'primary-runtime'))
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const { values } = parseArgs({ options: { python: { type: 'string' } } })
|
||||
await preparePrimaryRuntime(values.python)
|
||||
/**
|
||||
* Execute the native payload's interpreters, package manager and Python libraries.
|
||||
* @param root - Final payload directory, including any platform signatures.
|
||||
*/
|
||||
export function smokePrimaryRuntime(root: string): void {
|
||||
const manifest = JSON.parse(readFileSync(join(root, 'runtime.json'), 'utf8')) as PrimaryRuntimeManifest
|
||||
if (manifest.platform !== process.platform || manifest.arch !== process.arch) return
|
||||
const entries = workspaceDependencyPaths(root, manifest)
|
||||
const options = { stdio: 'inherit', timeout: 120_000 } as const
|
||||
execFileSync(entries.python, ['-I', '-c', 'import numpy, pandas; assert numpy.arange(4).sum() == 6; assert pandas.DataFrame({"n": [1, 2]}).n.sum() == 3'], options)
|
||||
execFileSync(entries.node, ['-e', `if (process.versions.node !== ${JSON.stringify(manifest.components.node)}) process.exit(1)`], options)
|
||||
execFileSync(entries.node, [entries.pnpm, '--version'], options)
|
||||
}
|
||||
|
||||
if (import.meta.main) await preparePrimaryRuntime()
|
||||
@@ -10,21 +10,62 @@
|
||||
"nodeSha256": "158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541",
|
||||
"pythonTarget": "x86_64-pc-windows-msvc",
|
||||
"pythonSha256": "7c45c9622400d578709a9b2cddbe8124cc21d382409d9f13406d706d28e31b14",
|
||||
"wheelPlatform": "win_amd64"
|
||||
"wheels": [
|
||||
{
|
||||
"url": "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl",
|
||||
"sha256": "86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa"
|
||||
},
|
||||
{
|
||||
"url": "https://files.pythonhosted.org/packages/75/08/67cc404b3a966b6df27b38370ddd96b3b023030b572283d035181854aac5/pandas-3.0.1-cp312-cp312-win_amd64.whl",
|
||||
"sha256": "536232a5fe26dd989bd633e7a0c450705fdc86a207fec7254a55e9a22950fe43"
|
||||
}
|
||||
]
|
||||
},
|
||||
"mac-arm64": {
|
||||
"nodeArchive": "darwin-arm64.tar.gz",
|
||||
"nodeSha256": "bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057",
|
||||
"pythonTarget": "aarch64-apple-darwin",
|
||||
"pythonSha256": "81a359f1cfadd4da11766534c5913791cea55f26e1bb902cacd2a531bb1e4b2b",
|
||||
"wheelPlatform": "macosx_11_0_arm64"
|
||||
"wheels": [
|
||||
{
|
||||
"url": "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl",
|
||||
"sha256": "ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769"
|
||||
},
|
||||
{
|
||||
"url": "https://files.pythonhosted.org/packages/7c/f1/e2567ffc8951ab371db2e40b2fe068e36b81d8cf3260f06ae508700e5504/pandas-3.0.1-cp312-cp312-macosx_11_0_arm64.whl",
|
||||
"sha256": "0ab749dfba921edf641d4036c4c21c0b3ea70fea478165cb98a998fb2a261955"
|
||||
}
|
||||
]
|
||||
},
|
||||
"mac-x64": {
|
||||
"nodeArchive": "darwin-x64.tar.gz",
|
||||
"nodeSha256": "1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097",
|
||||
"pythonTarget": "x86_64-apple-darwin",
|
||||
"pythonSha256": "65b195c9cedc1fef6767f044f9822069adbd1bd9204d424ece4628776fdc04bb",
|
||||
"wheelPlatform": "macosx_10_13_x86_64"
|
||||
"wheels": [
|
||||
{
|
||||
"url": "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl",
|
||||
"sha256": "74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e"
|
||||
},
|
||||
{
|
||||
"url": "https://files.pythonhosted.org/packages/37/51/b467209c08dae2c624873d7491ea47d2b47336e5403309d433ea79c38571/pandas-3.0.1-cp312-cp312-macosx_10_13_x86_64.whl",
|
||||
"sha256": "476f84f8c20c9f5bc47252b66b4bb25e1a9fc2fa98cead96744d8116cb85771d"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"wheels": [
|
||||
{
|
||||
"url": "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl",
|
||||
"sha256": "a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
|
||||
},
|
||||
{
|
||||
"url": "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl",
|
||||
"sha256": "4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"
|
||||
},
|
||||
{
|
||||
"url": "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl",
|
||||
"sha256": "1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
numpy==2.3.5 \
|
||||
--hash=sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e \
|
||||
--hash=sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769 \
|
||||
--hash=sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa
|
||||
pandas==3.0.1 \
|
||||
--hash=sha256:476f84f8c20c9f5bc47252b66b4bb25e1a9fc2fa98cead96744d8116cb85771d \
|
||||
--hash=sha256:0ab749dfba921edf641d4036c4c21c0b3ea70fea478165cb98a998fb2a261955 \
|
||||
--hash=sha256:536232a5fe26dd989bd633e7a0c450705fdc86a207fec7254a55e9a22950fe43
|
||||
python-dateutil==2.9.0.post0 --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
|
||||
six==1.17.0 --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274
|
||||
tzdata==2025.2 --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8
|
||||
@@ -19,12 +19,14 @@ export function assertMacOSRuntimeSignatureDetails(details: string, expected: Ma
|
||||
* @param path - Writable standalone Mach-O file.
|
||||
* @param identifier - Stable code-signing identifier derived from the release app ID and CAS digest.
|
||||
* @param expected - Public release identity.
|
||||
* @param entitlements - Optional entitlement plist for this executable.
|
||||
* @returns Resolves after codesign exits successfully.
|
||||
*/
|
||||
export function signMacOSRuntimeCode(
|
||||
path: string,
|
||||
identifier: string,
|
||||
expected: MacOSSigningEnvironment,
|
||||
entitlements?: string,
|
||||
): Promise<void>
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,15 +111,17 @@ function runCodeSign(args) {
|
||||
* @param {string} path - Writable standalone Mach-O file.
|
||||
* @param {string} identifier - Stable code-signing identifier derived from the release app ID and CAS digest.
|
||||
* @param {{ signingIdentity: string, teamId: string }} expected - Public release identity.
|
||||
* @param {string | undefined} entitlements - Optional entitlement plist for this executable.
|
||||
* @returns {Promise<void>} Resolves after codesign exits successfully.
|
||||
*/
|
||||
export async function signMacOSRuntimeCode(path, identifier, expected) {
|
||||
export async function signMacOSRuntimeCode(path, identifier, expected, entitlements) {
|
||||
await runAppleCommandAsync('/usr/bin/codesign', [
|
||||
'--force',
|
||||
'--sign', expected.signingIdentity,
|
||||
'--identifier', identifier,
|
||||
'--timestamp',
|
||||
'--options', 'runtime',
|
||||
...(entitlements === undefined ? [] : ['--entitlements', entitlements]),
|
||||
path,
|
||||
], 'codesign')
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
@@ -22,7 +22,7 @@ it('signs Mach-O files in their final locations and verifies each signature', as
|
||||
writeFileSync(join(path, 'addon.node'), Buffer.from('cffaedfe00000000', 'hex'))
|
||||
writeFileSync(join(path, 'source.js'), 'export {}')
|
||||
await expect(signMacOSRuntime(path, 'com.example.app', identity)).resolves.toBe(1)
|
||||
expect(signMacOSRuntimeCode).toHaveBeenCalledWith(join(path, 'addon.node'), expect.stringMatching(/^com\.example\.app\.runtime\.[a-f0-9]{64}$/u), identity)
|
||||
expect(signMacOSRuntimeCode).toHaveBeenCalledWith(join(path, 'addon.node'), expect.stringMatching(/^com\.example\.app\.runtime\.[a-f0-9]{64}$/u), identity, undefined)
|
||||
expect(verifyMacOSRuntimeCode).toHaveBeenCalledWith(join(path, 'addon.node'), identity)
|
||||
})
|
||||
it('awaits other signers before rejecting and permitting output cleanup', async () => {
|
||||
@@ -46,3 +46,15 @@ it('awaits other signers before rejecting and permitting output cleanup', async
|
||||
expect(await result).toBeInstanceOf(AggregateError)
|
||||
expect(verifyMacOSRuntimeCode).toHaveBeenCalledWith(join(path, 'b.node'), identity)
|
||||
})
|
||||
|
||||
it('grants JIT only to the standalone Node interpreter', async () => {
|
||||
const path = root()
|
||||
mkdirSync(join(path, 'dependencies/node/bin'), { recursive: true })
|
||||
const node = join(path, 'dependencies/node/bin/node')
|
||||
const addon = join(path, 'addon.node')
|
||||
for (const file of [node, addon]) writeFileSync(file, Buffer.from('cffaedfe00000000', 'hex'))
|
||||
await signMacOSRuntime(path, 'com.example.app', identity)
|
||||
expect(signMacOSRuntimeCode).toHaveBeenCalledWith(node, expect.any(String), identity,
|
||||
join(import.meta.dirname, '../scripts/node-entitlements.plist'))
|
||||
expect(signMacOSRuntimeCode).toHaveBeenCalledWith(addon, expect.any(String), identity, undefined)
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { expect, it } from 'vitest'
|
||||
import { downloadPrimaryRuntimeAsset, unpackPrimaryRuntimeWheel } from '../scripts/prepare-primary-runtime.ts'
|
||||
|
||||
const libraryWheel = Buffer.from('UEsDBAoAAAAAAASeLl0sYMPjDAAAAAwAAAAJAAAAc2FtcGxlLnB5c2FtcGxlID0gNDIKUEsBAh4DCgAAAAAABJ4uXSxgw+MMAAAADAAAAAkAAAAAAAAAAQAAAKSBAAAAAHNhbXBsZS5weVBLBQYAAAAAAQABADcAAAAzAAAAAAA=', 'base64')
|
||||
const relocatedWheel = Buffer.from('UEsDBAoAAAAAAASeLl3x0Nj9FAAAABQAAAAeAAAAc2FtcGxlLTEuMC5kYXRhL3NjcmlwdHMvc2FtcGxlcmVxdWlyZXMgcmVsb2NhdGlvbgpQSwECHgMKAAAAAAAEni5d8dDY/RQAAAAUAAAAHgAAAAAAAAABAAAApIEAAAAAc2FtcGxlLTEuMC5kYXRhL3NjcmlwdHMvc2FtcGxlUEsFBgAAAAABAAEATAAAAFAAAAAAAA==', 'base64')
|
||||
|
||||
it('extracts a hash-verified cached library without a Python installer or network request', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'desktop-wheel-'))
|
||||
try {
|
||||
const hash = createHash('sha256').update(libraryWheel).digest('hex')
|
||||
const archive = join(root, hash)
|
||||
await writeFile(archive, libraryWheel)
|
||||
expect(await downloadPrimaryRuntimeAsset('https://unused.invalid/library.whl', hash, root)).toBe(archive)
|
||||
await unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))
|
||||
expect(await readFile(join(root, 'site-packages/sample.py'), 'utf8')).toBe('sample = 42\n')
|
||||
await writeFile(archive, 'corrupt archive')
|
||||
await expect(downloadPrimaryRuntimeAsset('https://unused.invalid/library.whl', hash, root)).rejects.toThrow('checksum mismatch')
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects wheels that need installation outside site-packages', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'desktop-wheel-'))
|
||||
try {
|
||||
const archive = join(root, 'relocated.whl')
|
||||
await writeFile(archive, relocatedWheel)
|
||||
await expect(unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))).rejects.toThrow('unsupported installation paths')
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user