From 27e3d332ad2127d4b535dcc8ce135a9577d221c9 Mon Sep 17 00:00:00 2001 From: Wang Date: Sat, 29 Aug 2026 14:23:41 +0800 Subject: [PATCH] fix(desktop): prepare peer support in development Prepare the native peer addon before every Desktop development launch. Keep live addon and package staging under separate owners, and let the setup resolver own immutable development archives with exact SHA-512 evidence. Generated-by: OpenAI Codex --- CONTRIBUTING.md | 2 +- CONTRIBUTING.zh-CN.md | 2 +- README.md | 9 ++ README.zh-CN.md | 9 ++ apps/desktop/package.json | 3 + .../runtime-host-local-operator.test.ts | 65 +++++++++++++- .../runtime-host-peer-client.test.ts | 67 ++++++++++++++ .../runtime-host-setup-package.test.ts | 63 ++++++++++--- .../runtime-host-ssh-terminal.test.ts | 10 ++- .../runtime-host-wsl-controller.test.ts | 90 +++++++++++++++++++ apps/desktop/src/main/runtime-host-boot.ts | 1 + .../src/main/runtime-host-local-operator.ts | 40 +++++---- .../main/runtime-host-local-remote-access.ts | 2 +- .../src/main/runtime-host-management.ts | 6 +- .../src/main/runtime-host-onboarding.ts | 8 +- .../src/main/runtime-host-peer-client.ts | 2 + .../src/main/runtime-host-setup-package.ts | 85 +++++++++++++++--- .../src/main/runtime-host-ssh-terminal.ts | 56 ++++++------ .../src/main/runtime-host-wsl-controller.ts | 29 +----- native/runtime-host-peer/build.mjs | 9 +- package.json | 2 + scripts/release-cli-package.mjs | 43 ++++----- 22 files changed, 470 insertions(+), 133 deletions(-) create mode 100644 apps/desktop/src/main/__tests__/runtime-host-peer-client.test.ts create mode 100644 apps/desktop/src/main/__tests__/runtime-host-wsl-controller.test.ts 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', });