diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c3b127463..89d594ec54 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,7 +45,7 @@ Submit only work you have the right to contribute, and record third-party source ## Quick start -Requires Node `>=22.19.0` and npm `11.19.0` (root `package.json`); desktop work needs macOS Apple Silicon. +Requires Node `>=22.19.0` and npm `11.19.0` (root `package.json`). Direct Peer or Peer Mesh Desktop development additionally needs Rust stable 1.98 or newer and Xcode Command Line Tools on macOS, or MSVC Build Tools on Windows. ```sh git clone https://github.com/apache/maka.git diff --git a/CONTRIBUTING.zh-CN.md b/CONTRIBUTING.zh-CN.md index 90371c7205..d077ff46d7 100644 --- a/CONTRIBUTING.zh-CN.md +++ b/CONTRIBUTING.zh-CN.md @@ -45,7 +45,7 @@ ## 快速开始 -需要 Node `>=22.19.0` 和 npm `11.19.0`(见根 `package.json`);桌面端开发需要 macOS Apple Silicon。 +需要 Node `>=22.19.0` 和 npm `11.19.0`(见根 `package.json`)。开发 Desktop Direct Peer 或 Peer Mesh 还需要 Rust stable 1.98 或更高版本,以及 macOS 的 Xcode Command Line Tools 或 Windows 的 MSVC Build Tools。 ```sh git clone https://github.com/apache/maka.git diff --git a/README.md b/README.md index e9d3879701..a49eb3615f 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,15 @@ npm run dev npm run dev:full ``` +Direct Peer and Peer Mesh development additionally requires Rust stable 1.98 or newer and the +platform linker (Xcode Command Line Tools on macOS, MSVC Build Tools on Windows). Use the +peer-enabled entry point so the native addon is built before Desktop starts: + +```sh +npm run dev:peer # HMR +npm run dev:full:peer # full build +``` + If dependencies were installed with `ELECTRON_SKIP_BINARY_DOWNLOAD=1`, install the Electron platform binary before starting: ```sh diff --git a/README.zh-CN.md b/README.zh-CN.md index 75c132d173..d8a3cec264 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -122,6 +122,15 @@ npm run dev npm run dev:full ``` +开发 Direct Peer 和 Peer Mesh 还需要 Rust stable 1.98 或更高版本及平台 linker +(macOS 使用 Xcode Command Line Tools,Windows 使用 MSVC Build Tools)。使用 Peer 开发入口, +Desktop 会在启动前构建原生 addon: + +```sh +npm run dev:peer # HMR +npm run dev:full:peer # 完整构建 +``` + 如果安装时设置过 `ELECTRON_SKIP_BINARY_DOWNLOAD=1`,启动前需要补装 Electron 平台二进制: ```sh diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 464cd0c2d6..3b16e8151b 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -10,8 +10,11 @@ "main": "dist/main/main.js", "scripts": { "start": "node scripts/start-dev-app.mjs", + "start:peer": "npm run prepare:runtime-host-peer && node scripts/start-dev-app.mjs --runtime-host-peer", "prepare:dev-app": "node scripts/prepare-dev-app.mjs", + "prepare:runtime-host-peer": "node ../../native/runtime-host-peer/build.mjs", "dev": "node scripts/dev.mjs", + "dev:peer": "npm run prepare:runtime-host-peer && node scripts/dev.mjs --runtime-host-peer", "dev:hmr": "node scripts/dev.mjs", "storybook": "storybook dev -p 6006 -c .storybook", "build-storybook": "storybook build -c .storybook --output-dir storybook-static", diff --git a/apps/desktop/src/main/__tests__/runtime-host-local-operator.test.ts b/apps/desktop/src/main/__tests__/runtime-host-local-operator.test.ts index 6e848e782b..3fb3caa39c 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-local-operator.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-local-operator.test.ts @@ -18,8 +18,19 @@ */ import assert from 'node:assert/strict'; +import type { spawn } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { EventEmitter } from 'node:events'; +import { PassThrough } from 'node:stream'; import test from 'node:test'; -import { runtimeHostLocalSetupCommand } from '../runtime-host-local-operator.js'; +import { + encodeRuntimeHostSetupFrame, + RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV, +} from '@maka/runtime-host/operator'; +import { + createDesktopRuntimeHostLocalOperator, + runtimeHostLocalSetupCommand, +} from '../runtime-host-local-operator.js'; test('local setup installs one managed service for the Desktop root with Direct peer enabled', () => { assert.deepEqual( @@ -56,3 +67,55 @@ test('local setup installs one managed service for the Desktop root with Direct }, ); }); + +test('local setup forwards the exact development archive evidence', async (t) => { + const archive = '/tmp/maka-agent-development.tgz'; + const archiveBytes = Buffer.from('development package'); + const integrity = `sha512-${createHash('sha512').update(archiveBytes).digest('base64')}`; + let environment: NodeJS.ProcessEnv | undefined; + const spawnProcess = ((_command, _args, options) => { + environment = options?.env; + const child = new EventEmitter() as ReturnType; + const stdout = new PassThrough(); + const stderr = new PassThrough(); + Object.assign(child, { pid: 1234, stdout, stderr, kill: () => true }); + process.nextTick(() => { + stdout.end(encodeRuntimeHostSetupFrame({ + schemaVersion: 1, + sequence: 0, + kind: 'complete', + version: '0.2.0-development', + serviceId: 'b'.repeat(64), + deploymentId: '00000000-0000-4000-8000-000000000001', + operatorPath: '/tmp/maka/operator', + rootPath: '/tmp/maka/root', + rootId: 'a'.repeat(64), + endpoint: 'ws://127.0.0.1:7443/runtime-host', + credentialId: 'credential-1', + credential: 'secret-access-token', + })); + stderr.end(); + child.emit('close', 0, null); + }); + return child; + }) as typeof spawn; + const operator = createDesktopRuntimeHostLocalOperator({ + environment: { PATH: process.env.PATH }, + spawnProcess, + }); + t.after(() => operator.close()); + + await operator.runSetup({ + setupPackage: { kind: 'development_archive', path: archive, integrity }, + clientDataRoot: '/tmp/maka/client', + rootPath: '/tmp/maka/root', + principalId: 'desktop-owner:pairing', + expectedTarget: { + serviceId: 'b'.repeat(64), + rootPath: '/tmp/maka/root', + rootId: 'a'.repeat(64), + }, + }, () => undefined); + + assert.equal(environment?.[RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV], integrity); +}); diff --git a/apps/desktop/src/main/__tests__/runtime-host-peer-client.test.ts b/apps/desktop/src/main/__tests__/runtime-host-peer-client.test.ts new file mode 100644 index 0000000000..88ff2e60f3 --- /dev/null +++ b/apps/desktop/src/main/__tests__/runtime-host-peer-client.test.ts @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import test from 'node:test'; +import { configureDesktopRuntimeHostPeerClient } from '../runtime-host-peer-client.js'; + +test('development uses the native peer addon only for the peer-enabled launch', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'maka-desktop-peer-client-')); + t.after(() => rm(root, { recursive: true, force: true })); + const appPath = join(root, 'apps', 'desktop'); + const nativePath = join( + root, + 'native', + 'runtime-host-peer', + 'target', + 'release', + 'maka_runtime_host_peer.node', + ); + const clientDataRoot = join(root, 'client'); + await mkdir(appPath, { recursive: true }); + await mkdir(dirname(nativePath), { recursive: true }); + await writeFile(nativePath, 'native addon'); + + const ordinaryEnvironment: NodeJS.ProcessEnv = {}; + assert.equal(await configureDesktopRuntimeHostPeerClient({ + isPackaged: false, + appPath, + resourcesPath: join(root, 'resources'), + clientDataRoot, + environment: ordinaryEnvironment, + }), undefined); + assert.equal(ordinaryEnvironment.MAKA_RUNTIME_HOST_PEER_NATIVE_PATH, undefined); + + const peerEnvironment: NodeJS.ProcessEnv = {}; + assert.deepEqual(await configureDesktopRuntimeHostPeerClient({ + isPackaged: false, + enableDevelopmentPeer: true, + appPath, + resourcesPath: join(root, 'resources'), + clientDataRoot, + environment: peerEnvironment, + }), { + nativePath, + keyPath: join(clientDataRoot, 'runtime-host-client.peer.key'), + }); + assert.equal(peerEnvironment.MAKA_RUNTIME_HOST_PEER_NATIVE_PATH, nativePath); +}); diff --git a/apps/desktop/src/main/__tests__/runtime-host-setup-package.test.ts b/apps/desktop/src/main/__tests__/runtime-host-setup-package.test.ts index 6827082f20..432f50d214 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-setup-package.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-setup-package.test.ts @@ -18,14 +18,23 @@ */ import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { test } from 'node:test'; import { createRuntimeHostSetupPackageResolver } from '../runtime-host-setup-package.js'; -test('development setup lazily caches CLI archives by peer target unless overridden', async () => { +const ARCHIVE_BYTES = Buffer.from('development archive'); +const ARCHIVE_INTEGRITY = `sha512-${createHash('sha512').update(ARCHIVE_BYTES).digest('base64')}`; + +test('development setup lazily caches CLI archives by peer target unless overridden', async (t) => { const repoRoot = resolve('/workspace'); - const archive = join(repoRoot, 'packages', 'cli', 'release', 'maka-agent-dev.tgz'); + const directory = await mkdtemp(join(tmpdir(), 'maka-runtime-host-setup-package-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const archive = join(directory, 'maka-agent-dev.tgz'); + await writeFile(archive, ARCHIVE_BYTES); + const canonicalArchive = await realpath(archive); let builds = 0; let closes = 0; const targets: string[] = []; @@ -53,33 +62,59 @@ test('development setup lazily caches CLI archives by peer target unless overrid ]), [ { kind: 'development_archive', - path: archive, + path: canonicalArchive, + integrity: ARCHIVE_INTEGRITY, }, { kind: 'development_archive', - path: archive, + path: canonicalArchive, + integrity: ARCHIVE_INTEGRITY, }, ]); await resolvePackage.resolve('none'); assert.equal(builds, 2); assert.deepEqual(targets, ['linux-x64', 'none']); - const override = join(tmpdir(), 'explicit.tgz'); + const override = join(directory, 'explicit.tgz'); + await writeFile(override, ARCHIVE_BYTES); const resolveOverride = createRuntimeHostSetupPackageResolver({ isPackaged: false, appPath: join(repoRoot, 'apps', 'desktop'), environment: { MAKA_RUNTIME_HOST_SETUP_ARCHIVE: override }, startDevelopmentArchiveBuild: () => assert.fail('override must bypass the local build'), }); - assert.deepEqual(await resolveOverride.resolve('none'), { - kind: 'development_archive', - path: override, + const snapshot = await resolveOverride.resolve('none'); + assert.equal(snapshot.kind, 'development_archive'); + assert.notEqual(snapshot.path, await realpath(override)); + assert.equal(snapshot.integrity, ARCHIVE_INTEGRITY); + await writeFile(override, 'replacement archive'); + assert.deepEqual(await readFile(snapshot.path), ARCHIVE_BYTES); + assert.deepEqual(await resolveOverride.resolve('none'), snapshot); + + await resolveOverride.close(); + await assert.rejects(readFile(snapshot.path), { code: 'ENOENT' }); + + const invalidOverride = join(directory, 'explicit.zip'); + await writeFile(invalidOverride, ARCHIVE_BYTES); + const resolveInvalidOverride = createRuntimeHostSetupPackageResolver({ + isPackaged: false, + appPath: join(repoRoot, 'apps', 'desktop'), + environment: { MAKA_RUNTIME_HOST_SETUP_ARCHIVE: invalidOverride }, }); - await Promise.all([resolvePackage.close(), resolveOverride.close()]); + await assert.rejects(resolveInvalidOverride.resolve('none'), /must be a \.tgz file/u); + await Promise.all([ + resolvePackage.close(), + resolveInvalidOverride.close(), + ]); assert.equal(closes, 2); }); -test('cancelling the last waiter closes its build before a new setup starts', async () => { +test('cancelling the last waiter closes its build before a new setup starts', async (t) => { + const directory = await mkdtemp(join(tmpdir(), 'maka-runtime-host-setup-package-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const freshArchive = join(directory, 'fresh.tgz'); + await writeFile(freshArchive, ARCHIVE_BYTES); + const canonicalFreshArchive = await realpath(freshArchive); const cancelled = new AbortController(); let builds = 0; let rejectBuild!: (error: Error) => void; @@ -99,7 +134,10 @@ test('cancelling the last waiter closes its build before a new setup starts', as startDevelopmentArchiveBuild: () => { builds += 1; if (builds > 1) { - return { result: Promise.resolve('/workspace/fresh.tgz'), close: async () => undefined }; + return { + result: Promise.resolve(freshArchive), + close: async () => undefined, + }; } return { result: new Promise((_resolve, reject) => { @@ -126,7 +164,8 @@ test('cancelling the last waiter closes its build before a new setup starts', as await assert.rejects(first, /setup cancelled/u); assert.deepEqual(await second, { kind: 'development_archive', - path: '/workspace/fresh.tgz', + path: canonicalFreshArchive, + integrity: ARCHIVE_INTEGRITY, }); assert.equal(builds, 2); assert.equal(closes, 1); diff --git a/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts b/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts index 614c6db12c..9f0f32e0c8 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts @@ -18,6 +18,7 @@ */ import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -861,6 +862,7 @@ test('uploads a development release archive before running the same remote setup t.after(() => rm(directory, { recursive: true, force: true })); const archive = join(directory, 'maka-agent-development.tgz'); await writeFile(archive, 'development package'); + const integrity = `sha512-${createHash('sha512').update('development package').digest('base64')}`; const handlers = new Map unknown>(); const launches: Array<{ file: string; args: string[]; pty: FakePty }> = []; const terminal = createDesktopRuntimeHostSshTerminal({ @@ -879,7 +881,11 @@ test('uploads a development release archive before running the same remote setup const setupInput = { destination: 'operator@example.com', - setupPackage: { kind: 'development_archive', path: archive } as const, + setupPackage: { + kind: 'development_archive', + path: archive, + integrity, + } as const, principalId: 'desktop:stable-client', }; const setup = terminal.runSetup(setupInput, () => undefined); @@ -896,6 +902,8 @@ test('uploads a development release archive before running the same remote setup assert.equal(launches[1]?.file, 'ssh'); const remoteCommand = launches[1]?.args.at(-1) ?? ''; assert.match(remoteCommand, /--package.*maka-runtime-host-setup-.+\.tgz/u); + assert.match(remoteCommand, /MAKA_RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY=/u); + assert.ok(remoteCommand.includes(integrity)); assert.match(remoteCommand, /--defer-pairing-commit/u); assert.match(remoteCommand, /cd.*\$HOME/u); assert.match(remoteCommand, /rm -f/u); diff --git a/apps/desktop/src/main/__tests__/runtime-host-wsl-controller.test.ts b/apps/desktop/src/main/__tests__/runtime-host-wsl-controller.test.ts new file mode 100644 index 0000000000..42fbc75a09 --- /dev/null +++ b/apps/desktop/src/main/__tests__/runtime-host-wsl-controller.test.ts @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import type { ChildProcessWithoutNullStreams } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { EventEmitter } from 'node:events'; +import { PassThrough } from 'node:stream'; +import test from 'node:test'; +import type { RuntimeHostWslProcessFactory } from '@maka/runtime-host/client'; +import { + encodeRuntimeHostSetupFrame, + RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV, +} from '@maka/runtime-host/operator'; +import { runDesktopRuntimeHostWslSetup } from '../runtime-host-wsl-controller.js'; + +test('WSL setup forwards the development archive and its exact evidence', async () => { + const launches: string[][] = []; + const processFactory: RuntimeHostWslProcessFactory = (_executable, args) => { + launches.push([...args]); + const child = new EventEmitter() as ChildProcessWithoutNullStreams; + const stdin = new PassThrough(); + const stdout = new PassThrough(); + const stderr = new PassThrough(); + Object.assign(child, { stdin, stdout, stderr, kill: () => true }); + process.nextTick(() => { + if (launches.length === 1) stdout.end('/mnt/c/maka-development.tgz\n'); + else { + stdout.end(encodeRuntimeHostSetupFrame({ + schemaVersion: 1, + sequence: 0, + kind: 'complete', + version: '0.2.0-development', + serviceId: 'b'.repeat(64), + deploymentId: '00000000-0000-4000-8000-000000000001', + operatorPath: '/tmp/maka/operator', + rootPath: '/tmp/maka/root', + rootId: 'a'.repeat(64), + endpoint: 'ws://127.0.0.1:7443/runtime-host', + credentialId: 'credential-1', + credential: 'secret-access-token', + })); + } + stderr.end(); + child.emit('close', 0, null); + }); + return child; + }; + const integrity = `sha512-${createHash('sha512').update('archive evidence').digest('base64')}`; + + await runDesktopRuntimeHostWslSetup({ + distribution: 'Ubuntu', + setupPackage: { + kind: 'development_archive', + path: 'C:\\maka-development.tgz', + integrity, + }, + principalId: 'desktop-owner:pairing', + }, () => undefined, undefined, { processFactory, wslExecutable: 'wsl.exe' }); + + assert.deepEqual(launches[0], [ + '--distribution', + 'Ubuntu', + '--exec', + 'wslpath', + '-a', + '-u', + 'C:\\maka-development.tgz', + ]); + const setupCommand = launches[1]?.at(-1) ?? ''; + assert.match(setupCommand, new RegExp(`${RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV}=`, 'u')); + assert.ok(setupCommand.includes(integrity)); + assert.match(setupCommand, /--package.*\/mnt\/c\/maka-development\.tgz/u); +}); diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index a90b5baf11..0913bbee60 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -229,6 +229,7 @@ const buildInfo = resolveBuildInfo(app.isPackaged, app.getAppPath()); const userDataDir = app.getPath("userData"); const runtimeHostPeerConfiguration = await configureDesktopRuntimeHostPeerClient({ isPackaged: app.isPackaged, + enableDevelopmentPeer: process.argv.includes('--runtime-host-peer'), appPath: app.getAppPath(), resourcesPath: process.resourcesPath, clientDataRoot: userDataDir, diff --git a/apps/desktop/src/main/runtime-host-local-operator.ts b/apps/desktop/src/main/runtime-host-local-operator.ts index d4e03524eb..5bed92f9c6 100644 --- a/apps/desktop/src/main/runtime-host-local-operator.ts +++ b/apps/desktop/src/main/runtime-host-local-operator.ts @@ -18,7 +18,7 @@ */ import { spawn, type ChildProcess } from 'node:child_process'; -import { mkdtemp, realpath, rm, rmdir, stat } from 'node:fs/promises'; +import { mkdtemp, rm, rmdir, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { redactSecrets } from '@maka/core/redaction'; @@ -35,16 +35,14 @@ import { RUNTIME_HOST_PEER_MANAGEMENT_FRAME_PREFIX, RUNTIME_HOST_SERVICE_MANAGEMENT_FRAME_PREFIX, RUNTIME_HOST_SETUP_FRAME_PREFIX, + RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV, type RuntimeHostAccessManagementFrame, type RuntimeHostPeerManagementFrame, type RuntimeHostServiceManagementFrame, type RuntimeHostSetupFrame, } from '@maka/runtime-host/operator'; import { createRuntimeHostFramedOutputFilter } from './runtime-host-framed-output.js'; -import { - isExactRuntimeHostSetupPackageSpecifier, - type DesktopRuntimeHostSetupPackage, -} from './runtime-host-ssh-terminal.js'; +import type { DesktopRuntimeHostSetupPackage } from './runtime-host-setup-package.js'; const SETUP_TIMEOUT_MS = 10 * 60_000; const SETUP_FRAME_PENDING_MAX = 20 * 1024; @@ -175,8 +173,11 @@ export function createDesktopRuntimeHostLocalOperator(input: { async runSetup(setup, onProgress) { if (closed) throw new Error('Local Runtime Host operator is closed'); setup.signal?.throwIfAborted(); - const packageSpecifier = await resolveLocalSetupPackage(setup.setupPackage); - const command = runtimeHostLocalSetupCommand({ ...setup, packageSpecifier }); + const setupPackage = await resolveLocalSetupPackage(setup.setupPackage); + const command = runtimeHostLocalSetupCommand({ + ...setup, + packageSpecifier: setupPackage.specifier, + }); const workingDirectory = await mkdtemp(join(tmpdir(), 'maka-runtime-host-local-setup-')); try { if (closed) throw new Error('Local Runtime Host operator is closed'); @@ -185,7 +186,15 @@ export function createDesktopRuntimeHostLocalOperator(input: { return await runSetupProcess({ command, cwd: workingDirectory, - environment: input.environment ?? process.env, + environment: { + ...(input.environment ?? process.env), + ...(setupPackage.integrity + ? { + [RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV]: + setupPackage.integrity, + } + : {}), + }, spawnProcess: input.spawnProcess ?? spawn, timeoutMs: input.setupTimeoutMs ?? SETUP_TIMEOUT_MS, terminate, @@ -363,20 +372,13 @@ function combinedSignal( return operation ? AbortSignal.any([operation, closing]) : closing; } -async function resolveLocalSetupPackage( +function resolveLocalSetupPackage( setupPackage: DesktopRuntimeHostSetupPackage, -): Promise { +): { readonly specifier: string; readonly integrity?: string } { if (setupPackage.kind === 'npm') { - if (!isExactRuntimeHostSetupPackageSpecifier(setupPackage.specifier)) { - throw new Error('Runtime Host setup package is invalid'); - } - return setupPackage.specifier; - } - const archive = await realpath(setupPackage.path); - if (!(await stat(archive)).isFile() || !archive.endsWith('.tgz')) { - throw new Error('Runtime Host development package must be a .tgz file'); + return { specifier: setupPackage.specifier }; } - return archive; + return { specifier: setupPackage.path, integrity: setupPackage.integrity }; } function managedTargetArgs(target: DesktopRuntimeHostLocalServiceTarget): string[] { diff --git a/apps/desktop/src/main/runtime-host-local-remote-access.ts b/apps/desktop/src/main/runtime-host-local-remote-access.ts index 0496147dae..d19b3803bd 100644 --- a/apps/desktop/src/main/runtime-host-local-remote-access.ts +++ b/apps/desktop/src/main/runtime-host-local-remote-access.ts @@ -37,7 +37,7 @@ import type { createDesktopRuntimeHostLocalOperator, DesktopRuntimeHostLocalServiceTarget, } from './runtime-host-local-operator.js'; -import type { DesktopRuntimeHostSetupPackage } from './runtime-host-ssh-terminal.js'; +import type { DesktopRuntimeHostSetupPackage } from './runtime-host-setup-package.js'; const LIFECYCLE_FILE = 'runtime-host-local-service.json'; const SERVICE_ID_PATTERN = /^[a-f0-9]{64}$/u; diff --git a/apps/desktop/src/main/runtime-host-management.ts b/apps/desktop/src/main/runtime-host-management.ts index 8b0099619e..4ae98e5fe4 100644 --- a/apps/desktop/src/main/runtime-host-management.ts +++ b/apps/desktop/src/main/runtime-host-management.ts @@ -49,12 +49,14 @@ import type { DesktopRuntimeHostSshUpdateInput, DesktopRuntimeHostSshUpdatePolicyInput, DesktopRuntimeHostSshUpdateReconciliationInput, - DesktopRuntimeHostSetupPackage, RuntimeHostServiceUpdatePolicyTerminalFrame, RuntimeHostServiceUpdateReconciliationTerminalFrame, RuntimeHostServiceUpdateTerminalFrame, } from './runtime-host-ssh-terminal.js'; -import type { DesktopRuntimeHostDevelopmentPeerTarget } from './runtime-host-setup-package.js'; +import type { + DesktopRuntimeHostDevelopmentPeerTarget, + DesktopRuntimeHostSetupPackage, +} from './runtime-host-setup-package.js'; const MANAGEMENT_ACTIONS = new Set([ 'status', diff --git a/apps/desktop/src/main/runtime-host-onboarding.ts b/apps/desktop/src/main/runtime-host-onboarding.ts index fd39a42760..0311ac536a 100644 --- a/apps/desktop/src/main/runtime-host-onboarding.ts +++ b/apps/desktop/src/main/runtime-host-onboarding.ts @@ -28,12 +28,12 @@ import type { DesktopRuntimeHostOnboardingSnapshot, } from '../preload/bridge-contract.js'; import type { DesktopRuntimeHostProfileService } from './runtime-host-profile-service.js'; +import type { DesktopRuntimeHostSshSetupInput } from './runtime-host-ssh-terminal.js'; +import type { DesktopRuntimeHostWslSetupInput } from './runtime-host-wsl-controller.js'; import type { + DesktopRuntimeHostDevelopmentPeerTarget, DesktopRuntimeHostSetupPackage, - DesktopRuntimeHostSshSetupInput, -} from './runtime-host-ssh-terminal.js'; -import type { DesktopRuntimeHostWslSetupInput } from './runtime-host-wsl-controller.js'; -import type { DesktopRuntimeHostDevelopmentPeerTarget } from './runtime-host-setup-package.js'; +} from './runtime-host-setup-package.js'; import { requireProjectDirectoryRoots } from '../shared/runtime-host-project-directory-policy.js'; type OnboardingState = DesktopRuntimeHostOnboardingSnapshot extends infer Snapshot diff --git a/apps/desktop/src/main/runtime-host-peer-client.ts b/apps/desktop/src/main/runtime-host-peer-client.ts index e674802116..1e3bf5ea45 100644 --- a/apps/desktop/src/main/runtime-host-peer-client.ts +++ b/apps/desktop/src/main/runtime-host-peer-client.ts @@ -24,6 +24,7 @@ const NATIVE_FILE = 'maka_runtime_host_peer.node'; export async function configureDesktopRuntimeHostPeerClient(input: { readonly isPackaged: boolean; + readonly enableDevelopmentPeer?: boolean; readonly appPath: string; readonly resourcesPath: string; readonly clientDataRoot: string; @@ -37,6 +38,7 @@ export async function configureDesktopRuntimeHostPeerClient(input: { ? { nativePath: explicitNativePath, keyPath: explicitKeyPath } : undefined; } + if (!input.isPackaged && !input.enableDevelopmentPeer) return undefined; const nativePath = input.isPackaged ? join(input.resourcesPath, 'runtime-host-peer', NATIVE_FILE) : join( diff --git a/apps/desktop/src/main/runtime-host-setup-package.ts b/apps/desktop/src/main/runtime-host-setup-package.ts index 5a182ace53..a0a03fee67 100644 --- a/apps/desktop/src/main/runtime-host-setup-package.ts +++ b/apps/desktop/src/main/runtime-host-setup-package.ts @@ -18,19 +18,29 @@ */ import { spawn, type ChildProcess } from 'node:child_process'; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { createHash } from 'node:crypto'; +import { createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { copyFile, mkdtemp, realpath, rm, stat } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; import { isAbsolute, join, relative, resolve } from 'node:path'; import { DEFAULT_PROCESS_TERMINATION_GRACE_MS, terminateChildProcessTree, } from '@maka/runtime/process-tree-terminator'; -import { - isExactRuntimeHostSetupPackageSpecifier, - type DesktopRuntimeHostSetupPackage, -} from './runtime-host-ssh-terminal.js'; - const DEVELOPMENT_ARCHIVE_ENV = 'MAKA_RUNTIME_HOST_SETUP_ARCHIVE'; +export type DesktopRuntimeHostSetupPackage = + | { readonly kind: 'npm'; readonly specifier: string } + | { + readonly kind: 'development_archive'; + readonly path: string; + readonly integrity: string; + }; + +function isExactRuntimeHostSetupPackageSpecifier(value: unknown): value is string { + return typeof value === 'string' && /^maka-agent@[0-9][0-9A-Za-z.+-]*$/u.test(value); +} + interface DevelopmentArchiveBuild { readonly result: Promise; close(): Promise; @@ -90,6 +100,15 @@ export function createRuntimeHostSetupPackageResolver(input: { DesktopRuntimeHostDevelopmentPeerTarget, DevelopmentBuildState >(); + let overrideSnapshot: ReturnType | undefined; + + const resolveOverrideSnapshot = (path: string) => { + overrideSnapshot ??= snapshotDevelopmentSetupPackage(path).catch((error: unknown) => { + overrideSnapshot = undefined; + throw error; + }); + return overrideSnapshot; + }; const startBuild = ( peerTarget: DesktopRuntimeHostDevelopmentPeerTarget, @@ -99,10 +118,7 @@ export function createRuntimeHostSetupPackageResolver(input: { startDevelopmentArchiveBuild(repoRoot, input.environment, peerTarget); const build: DevelopmentBuildState = { task, - result: task.result.then((path) => ({ - kind: 'development_archive' as const, - path, - })), + result: task.result.then(developmentSetupPackage), waiters: 0, settled: false, }; @@ -116,6 +132,7 @@ export function createRuntimeHostSetupPackageResolver(input: { if (developmentBuilds.get(peerTarget) === build) { developmentBuilds.delete(peerTarget); } + void stopBuild(peerTarget, build).catch(() => undefined); }, ); return build; @@ -152,7 +169,10 @@ export function createRuntimeHostSetupPackageResolver(input: { if (input.isPackaged) return packagedSetupPackage(input.appPath); const override = input.environment[DEVELOPMENT_ARCHIVE_ENV]; - if (override) return { kind: 'development_archive', path: override }; + if (override) { + const snapshot = await waitForPackage(resolveOverrideSnapshot(override), signal); + return snapshot.setupPackage; + } const build = await acquireBuild(peerTarget, signal); build.waiters += 1; @@ -171,6 +191,8 @@ export function createRuntimeHostSetupPackageResolver(input: { const builds = [...developmentBuilds.entries()]; developmentBuilds.clear(); await Promise.all(builds.map(([peerTarget, build]) => stopBuild(peerTarget, build))); + const snapshot = await overrideSnapshot?.catch(() => undefined); + if (snapshot) await rm(snapshot.root, { recursive: true, force: true }); }, }; } @@ -274,6 +296,47 @@ function startDevelopmentArchiveBuild( }; } +async function developmentSetupPackage(path: string): Promise { + const archive = await realpath(path); + if (!(await stat(archive)).isFile() || !archive.endsWith('.tgz')) { + throw new Error('Runtime Host development package must be a .tgz file'); + } + return { + kind: 'development_archive', + path: archive, + integrity: await sha512Integrity(archive), + }; +} + +async function snapshotDevelopmentSetupPackage(path: string): Promise<{ + readonly root: string; + readonly setupPackage: DesktopRuntimeHostSetupPackage; +}> { + const source = await realpath(path); + if (!(await stat(source)).isFile() || !source.endsWith('.tgz')) { + throw new Error('Runtime Host development package must be a .tgz file'); + } + const root = await mkdtemp(join(tmpdir(), 'maka-runtime-host-setup-override-')); + const snapshot = join(root, 'package.tgz'); + try { + await copyFile(source, snapshot); + return { root, setupPackage: await developmentSetupPackage(snapshot) }; + } catch (error) { + await rm(root, { recursive: true, force: true }); + throw error; + } +} + +function sha512Integrity(path: string): Promise { + return new Promise((resolveIntegrity, reject) => { + const hash = createHash('sha512'); + const stream = createReadStream(path); + stream.on('data', (chunk) => hash.update(chunk)); + stream.once('error', reject); + stream.once('end', () => resolveIntegrity(`sha512-${hash.digest('base64')}`)); + }); +} + function waitForPackage(promise: Promise, signal?: AbortSignal): Promise { if (!signal) return promise; signal.throwIfAborted(); diff --git a/apps/desktop/src/main/runtime-host-ssh-terminal.ts b/apps/desktop/src/main/runtime-host-ssh-terminal.ts index ed1c216868..2fa1307997 100644 --- a/apps/desktop/src/main/runtime-host-ssh-terminal.ts +++ b/apps/desktop/src/main/runtime-host-ssh-terminal.ts @@ -19,7 +19,6 @@ import { homedir } from 'node:os'; import { createHash, randomUUID } from 'node:crypto'; -import { realpath, stat } from 'node:fs/promises'; import { posix as pathPosix } from 'node:path'; import type { IpcMain } from 'electron'; import type { IPty } from 'node-pty'; @@ -54,6 +53,7 @@ import { RUNTIME_HOST_PEER_MESH_MANAGEMENT_FRAME_PREFIX, RUNTIME_HOST_SERVICE_MANAGEMENT_FRAME_PREFIX, RUNTIME_HOST_SETUP_FRAME_PREFIX, + RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV, type RuntimeHostAccessManagementFrame, type RuntimeHostActivationResult, type RuntimeHostManagedUpdatePolicy, @@ -72,7 +72,10 @@ import type { DesktopRuntimeHostSshTerminalSnapshot, } from '../preload/bridge-contract.js'; import { createRuntimeHostFramedOutputFilter } from './runtime-host-framed-output.js'; -import type { DesktopRuntimeHostDevelopmentPeerTarget } from './runtime-host-setup-package.js'; +import type { + DesktopRuntimeHostDevelopmentPeerTarget, + DesktopRuntimeHostSetupPackage, +} from './runtime-host-setup-package.js'; interface ActiveTerminal { readonly sessionId: string; @@ -220,10 +223,6 @@ export type DesktopRuntimeHostSshAccessInput = DesktopRuntimeHostSshAccessTarget } ); -export type DesktopRuntimeHostSetupPackage = - | { readonly kind: 'npm'; readonly specifier: string } - | { readonly kind: 'development_archive'; readonly path: string }; - export type RuntimeHostServiceUpdateTerminalFrame = | Extract | (Extract & { @@ -240,10 +239,6 @@ export type RuntimeHostServiceUpdateReconciliationTerminalFrame = Extract< { kind: 'result' | 'error'; action: 'reconcile_update' } >; -export function isExactRuntimeHostSetupPackageSpecifier(value: unknown): value is string { - return typeof value === 'string' && /^maka-agent@[0-9][0-9A-Za-z.+-]*$/u.test(value); -} - type RuntimeHostSetupCompleteFrame = Extract; export function createDesktopRuntimeHostSshTerminal(input: { @@ -1000,10 +995,14 @@ function cancellableUntilComplete(signal: AbortSignal | undefined): { }; } -interface PreparedSetupPackage { - readonly specifier: string; - readonly removeAfterSetup?: string; -} +type PreparedSetupPackage = + | { readonly kind: 'npm'; readonly specifier: string } + | { + readonly kind: 'development_archive'; + readonly specifier: string; + readonly integrity: string; + readonly removeAfterSetup: string; + }; async function prepareSetupPackage( setupPackage: DesktopRuntimeHostSetupPackage, @@ -1022,16 +1021,9 @@ async function prepareSetupPackage( terminateTree: typeof terminateProcessTree | undefined, ): Promise { if (setupPackage.kind === 'npm') { - if (!isExactRuntimeHostSetupPackageSpecifier(setupPackage.specifier)) { - throw new Error('Runtime Host setup package is invalid'); - } - return { specifier: setupPackage.specifier }; + return setupPackage; } signal?.throwIfAborted(); - const archive = await realpath(setupPackage.path); - if (!(await stat(archive)).isFile() || !archive.endsWith('.tgz')) { - throw new Error('Runtime Host development package must be a .tgz file'); - } const remoteArchive = remoteDevelopmentArchivePath(principalId); const { process, terminal } = startTerminalProcess('scp', [ '-o', @@ -1045,7 +1037,7 @@ async function prepareSetupPackage( '-o', 'ClearAllForwardings=yes', ...(sshPort === undefined ? [] : ['-P', String(sshPort)]), - archive, + setupPackage.path, `${destination}:${remoteArchive}`, ], undefined, true); const wait = await waitForTerminalProcess(process, { @@ -1062,7 +1054,12 @@ async function prepareSetupPackage( `Uploading the Runtime Host development package exited with code ${String(wait.exit.code)}`, ); } - return { specifier: remoteArchive, removeAfterSetup: remoteArchive }; + return { + kind: 'development_archive', + specifier: remoteArchive, + integrity: setupPackage.integrity, + removeAfterSetup: remoteArchive, + }; } function remoteDevelopmentArchivePath(principalId: string): string { @@ -1414,14 +1411,19 @@ function runtimeHostPackageRemoteCommand( environment: Readonly> = {}, ): string { const commandArgs = ['maka', ...args].map(quotePosix).join(' '); - const environmentPrefix = Object.entries(environment) + const environmentPrefix = Object.entries({ + ...environment, + ...(setupPackage.kind === 'development_archive' + ? { [RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV]: setupPackage.integrity } + : {}), + }) .map(([name, value]) => `${name}=${quotePosix(value)}`) .join(' '); const invocationPrefix = environmentPrefix ? `${environmentPrefix} ` : ''; - const commandInvocation = setupPackage.removeAfterSetup + const commandInvocation = setupPackage.kind === 'development_archive' ? `${invocationPrefix}npx --yes --package ${quotePosix(setupPackage.specifier)} ${commandArgs}` : `${invocationPrefix}npx --yes --prefix "$maka_command_prefix" --package ${quotePosix(setupPackage.specifier)} ${commandArgs}`; - const command = setupPackage.removeAfterSetup + const command = setupPackage.kind === 'development_archive' ? `cd "$HOME" || exit 1; maka_command_exit=0; ${commandInvocation} || maka_command_exit=$?; rm -f -- ${quotePosix(setupPackage.removeAfterSetup)}; exit "$maka_command_exit"` : `maka_command_prefix=$(mktemp -d) || exit 1; trap 'rm -rf -- "$maka_command_prefix"' EXIT; trap 'exit 129' HUP; trap 'exit 130' INT; trap 'exit 143' TERM; cd "$maka_command_prefix" || exit 1; ${commandInvocation}`; const loginCommand = `exec /bin/sh -c ${quotePosix(command)}`; diff --git a/apps/desktop/src/main/runtime-host-wsl-controller.ts b/apps/desktop/src/main/runtime-host-wsl-controller.ts index 3afebc18ac..80bc59a038 100644 --- a/apps/desktop/src/main/runtime-host-wsl-controller.ts +++ b/apps/desktop/src/main/runtime-host-wsl-controller.ts @@ -18,9 +18,6 @@ */ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'; -import { createHash } from 'node:crypto'; -import { createReadStream } from 'node:fs'; -import { realpath, stat } from 'node:fs/promises'; import type { Readable } from 'node:stream'; import { normalizeRuntimeHostWslDistribution, @@ -35,10 +32,7 @@ import { type RuntimeHostSetupPhase, } from '@maka/runtime-host/operator'; import { createRuntimeHostFramedOutputFilter } from './runtime-host-framed-output.js'; -import { - isExactRuntimeHostSetupPackageSpecifier, - type DesktopRuntimeHostSetupPackage, -} from './runtime-host-ssh-terminal.js'; +import type { DesktopRuntimeHostSetupPackage } from './runtime-host-setup-package.js'; const WSL_SETUP_TIMEOUT_MS = 10 * 60_000; const WSL_SETUP_OUTPUT_MAX_BYTES = 64 * 1024; @@ -142,16 +136,9 @@ async function resolveWslPackageSpecifier( processFactory: RuntimeHostWslProcessFactory, ): Promise<{ readonly specifier: string; readonly integrity?: string }> { if (setupPackage.kind === 'npm') { - if (!isExactRuntimeHostSetupPackageSpecifier(setupPackage.specifier)) { - throw new Error('Runtime Host setup package is invalid'); - } return { specifier: setupPackage.specifier }; } - const archive = await realpath(setupPackage.path); - if (!(await stat(archive)).isFile() || !archive.endsWith('.tgz')) { - throw new Error('Runtime Host development package must be a .tgz file'); - } - const child = processFactory(executable, ['--distribution', distribution, '--exec', 'wslpath', '-a', '-u', archive]); + const child = processFactory(executable, ['--distribution', distribution, '--exec', 'wslpath', '-a', '-u', setupPackage.path]); child.stdin.end(); const stdout = collectBounded(child.stdout, 4 * 1024); const stderr = collectBounded(child.stderr, WSL_SETUP_STDERR_MAX_BYTES); @@ -165,7 +152,7 @@ async function resolveWslPackageSpecifier( const diagnostic = formatBoundedDiagnostic(capturedStderr); throw new Error(`WSL could not resolve the setup package path${diagnostic ? `: ${diagnostic}` : ''}`); } - return { specifier: path, integrity: await sha512Integrity(archive) }; + return { specifier: path, integrity: setupPackage.integrity }; } function runtimeHostWslSetupCommand( @@ -208,16 +195,6 @@ function runtimeHostWslSetupCommand( return `exec "\${SHELL:-/bin/sh}" -lic ${quotePosix(loginCommand)}`; } -function sha512Integrity(path: string): Promise { - return new Promise((resolve, reject) => { - const hash = createHash('sha512'); - const stream = createReadStream(path); - stream.on('data', (chunk) => hash.update(chunk)); - stream.once('error', reject); - stream.once('end', () => resolve(`sha512-${hash.digest('base64')}`)); - }); -} - function quotePosix(value: string): string { return `'${value.replaceAll("'", `'"'"'`)}'`; } diff --git a/native/runtime-host-peer/build.mjs b/native/runtime-host-peer/build.mjs index 9c9d77ffe7..2a23841ba1 100644 --- a/native/runtime-host-peer/build.mjs +++ b/native/runtime-host-peer/build.mjs @@ -17,9 +17,9 @@ * under the License. */ -import { copyFile, readFile } from 'node:fs/promises'; +import { copyFile, mkdir, readFile } from 'node:fs/promises'; import { spawn } from 'node:child_process'; -import { dirname, join } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = dirname(fileURLToPath(import.meta.url)); @@ -67,7 +67,10 @@ const library = : targetPlatform === 'darwin' ? 'libmaka_runtime_host_peer.dylib' : 'libmaka_runtime_host_peer.so'; -const destination = join(root, 'target', 'release', 'maka_runtime_host_peer.node'); +const destination = process.env.MAKA_RUNTIME_HOST_PEER_OUTPUT?.trim() + ? resolve(process.env.MAKA_RUNTIME_HOST_PEER_OUTPUT.trim()) + : join(root, 'target', 'release', 'maka_runtime_host_peer.node'); +await mkdir(dirname(destination), { recursive: true }); await copyFile(join(releaseDirectory, library), destination); if (targetPlatform === 'darwin' && process.platform === 'darwin') { await run('install_name_tool', ['-id', '@rpath/maka_runtime_host_peer.node', destination], root, { diff --git a/package.json b/package.json index 7e3eac847d..00d98ce253 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,9 @@ "build:runtime-host-peer": "node native/runtime-host-peer/build.mjs", "lint:runtime-host-peer": "cargo clippy --locked --all-targets --manifest-path native/runtime-host-peer/Cargo.toml -- -D warnings", "dev": "npm --workspace @maka/desktop run dev:hmr --", + "dev:peer": "npm --workspace @maka/desktop run dev:peer --", "dev:full": "npm run build && npm --workspace @maka/desktop run start", + "dev:full:peer": "npm run build && npm --workspace @maka/desktop run start:peer", "cli:dev": "node packages/cli/dist/dev-cli.js", "build": "npm --workspace @maka/core run build && npm --workspace @maka/storage run build && npm --workspace @maka/mcp run build && npm --workspace @maka/runtime run build && npm --workspace @maka/runtime-host run build && npm --workspace @maka/computer-use run build && npm --workspace @maka/eval run build && npm --workspace maka-agent run build && npm --workspace @maka/ui run build && npm --workspace @maka/desktop run build", "build:test": "npm run clean && npm --workspace @maka/core run build && npm --workspace @maka/storage run build && npm --workspace @maka/mcp run build && npm --workspace @maka/runtime run build && npm --workspace @maka/runtime-host run build && npm --workspace @maka/computer-use run build && npm --workspace @maka/eval run build && npm --workspace maka-agent run build && npm --workspace @maka/ui run build && npm --workspace @maka/desktop run build:test", diff --git a/scripts/release-cli-package.mjs b/scripts/release-cli-package.mjs index da67bac7e9..1b0087dfa3 100644 --- a/scripts/release-cli-package.mjs +++ b/scripts/release-cli-package.mjs @@ -543,41 +543,31 @@ function copyReleaseDocuments() { } function copyRuntimeHostPeerPrebuilds(publishable) { - let sourceRoot = process.env.MAKA_RUNTIME_HOST_PEER_PREBUILDS?.trim(); - let generatedRoot; + const sourceRoot = process.env.MAKA_RUNTIME_HOST_PEER_PREBUILDS?.trim(); const targets = publishable ? peerPrebuildTargets : privatePeerTarget === 'none' ? [] : [privatePeerTarget]; if (targets.length === 0) return; + const destinationRoot = join(stageRoot, 'native/runtime-host-peer/prebuilds'); if (!sourceRoot && !publishable) { const [target] = targets; - buildDevelopmentPeerAddon(target); - sourceRoot = generatedRoot = mkdtempSync(join(tmpdir(), 'maka-runtime-host-peer-prebuilds-')); - const targetRoot = join(sourceRoot, target); - mkdirSync(targetRoot, { recursive: true, mode: 0o755 }); - copyFileSync( - join(repoRoot, 'native/runtime-host-peer/target/release/maka_runtime_host_peer.node'), - join(targetRoot, 'maka_runtime_host_peer.node'), - ); + const destination = join(destinationRoot, target, 'maka_runtime_host_peer.node'); + buildDevelopmentPeerAddon(target, destination); + return; } if (!sourceRoot) { throw new Error('MAKA_RUNTIME_HOST_PEER_PREBUILDS must contain all release platform addons'); } - const destinationRoot = join(stageRoot, 'native/runtime-host-peer/prebuilds'); - try { - for (const target of targets) { - const source = join(sourceRoot, target, 'maka_runtime_host_peer.node'); - if (!existsSync(source) || !statSync(source).isFile()) { - throw new Error(`Runtime Host peer prebuild is missing: ${target}`); - } - const destination = join(destinationRoot, target, 'maka_runtime_host_peer.node'); - mkdirSync(dirname(destination), { recursive: true, mode: 0o755 }); - copyFileSync(source, destination); + for (const target of targets) { + const source = join(sourceRoot, target, 'maka_runtime_host_peer.node'); + if (!existsSync(source) || !statSync(source).isFile()) { + throw new Error(`Runtime Host peer prebuild is missing: ${target}`); } - } finally { - if (generatedRoot) rmSync(generatedRoot, { recursive: true, force: true }); + const destination = join(destinationRoot, target, 'maka_runtime_host_peer.node'); + mkdirSync(dirname(destination), { recursive: true, mode: 0o755 }); + copyFileSync(source, destination); } } @@ -592,11 +582,15 @@ function resolveDevelopmentPeerTarget() { return target; } -function buildDevelopmentPeerAddon(target) { +function buildDevelopmentPeerAddon(target, output) { const hostTarget = `${process.platform}-${process.arch}`; const buildScript = join(repoRoot, 'native/runtime-host-peer/build.mjs'); if (target === hostTarget) { - execFileSync(process.execPath, [buildScript], { cwd: repoRoot, stdio: 'inherit' }); + execFileSync(process.execPath, [buildScript], { + cwd: repoRoot, + env: { ...process.env, MAKA_RUNTIME_HOST_PEER_OUTPUT: output }, + stdio: 'inherit', + }); return; } const rustTarget = { @@ -624,6 +618,7 @@ function buildDevelopmentPeerAddon(target) { ...process.env, MAKA_RUNTIME_HOST_PEER_CARGO_SUBCOMMAND: 'zigbuild', MAKA_RUNTIME_HOST_PEER_CARGO_TARGET: rustTarget, + MAKA_RUNTIME_HOST_PEER_OUTPUT: output, }, stdio: 'inherit', });