You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/08/28 13:48:32 UTC
(maka) branch main updated: feat(runtime-host): target development host packages (#4087)
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new cb60d416f feat(runtime-host): target development host packages (#4087)
cb60d416f is described below
commit cb60d416fa0ac72272f1c287219c7ec542ff106f
Author: Wang <m4...@gmail.com>
AuthorDate: Fri Aug 28 21:48:28 2026 +0800
feat(runtime-host): target development host packages (#4087)
Generated-by: Codex
---
.../main/__tests__/runtime-host-management.test.ts | 6 ++
.../main/__tests__/runtime-host-onboarding.test.ts | 9 ++
.../__tests__/runtime-host-setup-package.test.ts | 24 +++--
.../__tests__/runtime-host-ssh-terminal.test.ts | 32 +++++-
apps/desktop/src/main/runtime-host-boot.ts | 14 ++-
apps/desktop/src/main/runtime-host-management.ts | 19 +++-
apps/desktop/src/main/runtime-host-onboarding.ts | 32 +++++-
.../desktop/src/main/runtime-host-setup-package.ts | 107 +++++++++++++++------
apps/desktop/src/main/runtime-host-ssh-terminal.ts | 85 ++++++++++++++++
native/runtime-host-peer/build.mjs | 13 ++-
scripts/release-cli-package.mjs | 85 +++++++++++++---
11 files changed, 366 insertions(+), 60 deletions(-)
diff --git a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
index b82385efd..e7d234745 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
@@ -425,6 +425,9 @@ test('publishes update progress and waits for the managed profile to reconnect',
runUpdatePolicy: async () => assert.fail('update policy is not expected'),
runUpdateReconciliation: async () =>
assert.fail('update reconciliation is not expected'),
+ setupPackageMode: 'published',
+ resolveSshDevelopmentPeerTarget: async () =>
+ assert.fail('published update must not inspect the development target'),
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.3.0' }),
currentHostEpoch: () => 'host-before-update',
awaitUpdatedConnection: async (...args) => {
@@ -1175,6 +1178,9 @@ function unusedUpdateDependencies() {
runPeerManagement: async (): Promise<never> =>
assert.fail('direct peer management is not expected'),
directPeerClientAvailable: false,
+ setupPackageMode: 'published' as const,
+ resolveSshDevelopmentPeerTarget: async (): Promise<never> =>
+ assert.fail('published update must not inspect the development target'),
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.2.3' } as const),
currentHostEpoch: () => undefined,
awaitUpdatedConnection: async () => undefined,
diff --git a/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts b/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
index 2eae7b7d4..c40b096d2 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
@@ -94,6 +94,7 @@ test('persists a verified on-demand SSH profile without endpoint or credential p
test('onboards WSL as a credential-free environment profile', async () => {
let saved: DesktopRuntimeHostProfileAddInput | undefined;
+ const peerTargets: string[] = [];
const harness = createHarness({
profiles: {
addAndEnable: async (input) => {
@@ -108,6 +109,10 @@ test('onboards WSL as a credential-free environment profile', async () => {
operatorPath: '/home/operator/.local/share/maka/operator',
};
},
+ resolveSetupPackage: (peerTarget) => {
+ peerTargets.push(peerTarget);
+ return { kind: 'npm', specifier: 'maka-agent@0.2.0' };
+ },
});
const result = await harness.invoke('runtime-host-onboarding:start', {
@@ -125,6 +130,7 @@ test('onboards WSL as a credential-free environment profile', async () => {
rootId: 'a'.repeat(64),
operatorPath: '/home/operator/.local/share/maka/operator',
});
+ assert.deepEqual(peerTargets, ['none']);
await harness.onboarding.close();
});
@@ -294,6 +300,9 @@ function createHarness(overrides: HarnessOverrides = {}) {
addAndEnableVerified: async () => assert.fail('profile must not be saved'),
...profiles,
},
+ setupPackageMode: 'published',
+ resolveSshDevelopmentPeerTarget: async () =>
+ assert.fail('published setup must not inspect the development target'),
resolveSetupPackage: () => ({ kind: 'npm', specifier: 'maka-agent@0.2.0' }),
runSetup: async () => assert.fail('SSH must not start'),
runWslSetup: async () => assert.fail('WSL must not start'),
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 c5e9ecec4..6827082f2 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
@@ -23,17 +23,19 @@ import { join, resolve } from 'node:path';
import { test } from 'node:test';
import { createRuntimeHostSetupPackageResolver } from '../runtime-host-setup-package.js';
-test('development setup lazily builds one local CLI archive unless explicitly overridden', async () => {
+test('development setup lazily caches CLI archives by peer target unless overridden', async () => {
const repoRoot = resolve('/workspace');
const archive = join(repoRoot, 'packages', 'cli', 'release', 'maka-agent-dev.tgz');
let builds = 0;
let closes = 0;
+ const targets: string[] = [];
const resolvePackage = createRuntimeHostSetupPackageResolver({
isPackaged: false,
appPath: join(repoRoot, 'apps', 'desktop'),
environment: {},
- startDevelopmentArchiveBuild: (resolvedRoot) => {
+ startDevelopmentArchiveBuild: (resolvedRoot, target) => {
builds += 1;
+ targets.push(target);
assert.equal(resolvedRoot, repoRoot);
return {
result: Promise.resolve(archive),
@@ -44,7 +46,11 @@ test('development setup lazily builds one local CLI archive unless explicitly ov
},
});
- assert.deepEqual(await Promise.all([resolvePackage.resolve(), resolvePackage.resolve()]), [
+ assert.equal(resolvePackage.mode, 'development');
+ assert.deepEqual(await Promise.all([
+ resolvePackage.resolve('linux-x64'),
+ resolvePackage.resolve('linux-x64'),
+ ]), [
{
kind: 'development_archive',
path: archive,
@@ -54,7 +60,9 @@ test('development setup lazily builds one local CLI archive unless explicitly ov
path: archive,
},
]);
- assert.equal(builds, 1);
+ await resolvePackage.resolve('none');
+ assert.equal(builds, 2);
+ assert.deepEqual(targets, ['linux-x64', 'none']);
const override = join(tmpdir(), 'explicit.tgz');
const resolveOverride = createRuntimeHostSetupPackageResolver({
@@ -63,12 +71,12 @@ test('development setup lazily builds one local CLI archive unless explicitly ov
environment: { MAKA_RUNTIME_HOST_SETUP_ARCHIVE: override },
startDevelopmentArchiveBuild: () => assert.fail('override must bypass the local build'),
});
- assert.deepEqual(await resolveOverride.resolve(), {
+ assert.deepEqual(await resolveOverride.resolve('none'), {
kind: 'development_archive',
path: override,
});
await Promise.all([resolvePackage.close(), resolveOverride.close()]);
- assert.equal(closes, 1);
+ assert.equal(closes, 2);
});
test('cancelling the last waiter closes its build before a new setup starts', async () => {
@@ -107,10 +115,10 @@ test('cancelling the last waiter closes its build before a new setup starts', as
},
});
- const first = resolver.resolve(cancelled.signal);
+ const first = resolver.resolve('linux-x64', cancelled.signal);
cancelled.abort(new Error('setup cancelled'));
await closeStarted;
- const second = resolver.resolve();
+ const second = resolver.resolve('linux-x64');
await Promise.resolve();
assert.equal(builds, 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 3bbc6c564..c5a918c24 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
@@ -33,7 +33,37 @@ import {
RUNTIME_HOST_OPERATOR_PEER_MANAGEMENT_CAPABILITY,
RUNTIME_HOST_SETUP_FRAME_PREFIX,
} from '@maka/runtime-host/operator';
-import { createDesktopRuntimeHostSshTerminal } from '../runtime-host-ssh-terminal.js';
+import {
+ createDesktopRuntimeHostSshTerminal,
+ runtimeHostDevelopmentPeerTargetFromUname,
+} from '../runtime-host-ssh-terminal.js';
+
+test('maps supported SSH uname identities to development peer targets', () => {
+ assert.equal(runtimeHostDevelopmentPeerTargetFromUname('Linux', 'x86_64'), 'linux-x64');
+ assert.equal(runtimeHostDevelopmentPeerTargetFromUname('Linux', 'aarch64'), 'linux-arm64');
+ assert.equal(runtimeHostDevelopmentPeerTargetFromUname('Darwin', 'arm64'), 'darwin-arm64');
+ assert.throws(
+ () => runtimeHostDevelopmentPeerTargetFromUname('Linux', 'riscv64'),
+ /not available/u,
+ );
+});
+
+test('detects the development peer target through the bounded SSH preflight', async () => {
+ const harness = createHarness('pending');
+ const detection = harness.terminal.resolveDevelopmentPeerTarget({
+ destination: 'operator@example.com',
+ });
+ await waitFor(() => harness.pty.hasDataListener());
+ const command = harness.launchArgs[0]?.at(-1) ?? '';
+ const marker = command.match(/__MAKA_RUNTIME_HOST_TARGET_[0-9a-f]+__/u)?.[0];
+ assert.ok(marker);
+ harness.pty.emitData(`${marker}Linux:x86_64\r\n`);
+ harness.pty.exit(0);
+
+ assert.equal(await detection, 'linux-x64');
+ assert.doesNotMatch(JSON.stringify(harness.events), /MAKA_RUNTIME_HOST_TARGET/u);
+ await harness.terminal.close();
+});
test('keeps a connecting SSH prompt observable across renderer presentation changes', async () => {
const harness = createHarness('pending');
diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts
index 0bbce55db..5476d7e02 100644
--- a/apps/desktop/src/main/runtime-host-boot.ts
+++ b/apps/desktop/src/main/runtime-host-boot.ts
@@ -172,7 +172,10 @@ import {
createDesktopRuntimeHostSshTerminal,
} from "./runtime-host-ssh-terminal.js";
import { runDesktopRuntimeHostWslSetup } from './runtime-host-wsl-controller.js';
-import { createRuntimeHostSetupPackageResolver } from "./runtime-host-setup-package.js";
+import {
+ createRuntimeHostSetupPackageResolver,
+ desktopRuntimeHostDevelopmentPeerTarget,
+} from "./runtime-host-setup-package.js";
import { configureDesktopRuntimeHostPeerClient } from './runtime-host-peer-client.js';
import { createDesktopRuntimeHostLocalOperator } from './runtime-host-local-operator.js';
import { createDesktopLocalRuntimeHostRemoteAccess } from './runtime-host-local-remote-access.js';
@@ -399,7 +402,10 @@ const localRuntimeHostRemoteAccess = createDesktopLocalRuntimeHostRemoteAccess({
rootId: startupLocalStorageRoot.rootId,
directPeerAvailable: runtimeHostDirectPeerAvailable,
manager: () => runtimeHostManager,
- resolveSetupPackage: runtimeHostSetupPackage.resolve,
+ resolveSetupPackage: (signal) => runtimeHostSetupPackage.resolve(
+ desktopRuntimeHostDevelopmentPeerTarget(),
+ signal,
+ ),
operator: localRuntimeHostOperator,
});
const native = assembleDesktopNativeCapabilities({
@@ -474,6 +480,8 @@ const runtimeHostOnboarding = createDesktopRuntimeHostOnboarding({
runSetup: runtimeHostSshTerminal.runSetup,
runWslSetup: runDesktopRuntimeHostWslSetup,
listWslDistributions: listRuntimeHostWslDistributions,
+ setupPackageMode: runtimeHostSetupPackage.mode,
+ resolveSshDevelopmentPeerTarget: runtimeHostSshTerminal.resolveDevelopmentPeerTarget,
resolveSetupPackage: runtimeHostSetupPackage.resolve,
send: (snapshot) =>
mainWindowController.send("runtime-host-onboarding:changed", snapshot),
@@ -487,6 +495,8 @@ const runtimeHostManagement = createDesktopRuntimeHostManagement({
runUpdate: runtimeHostSshTerminal.runUpdate,
runUpdatePolicy: runtimeHostSshTerminal.runUpdatePolicy,
runUpdateReconciliation: runtimeHostSshTerminal.runUpdateReconciliation,
+ setupPackageMode: runtimeHostSetupPackage.mode,
+ resolveSshDevelopmentPeerTarget: runtimeHostSshTerminal.resolveDevelopmentPeerTarget,
resolveUpdatePackage: runtimeHostSetupPackage.resolve,
currentHostEpoch: (profileId) =>
runtimeHostManager?.current(profileId)?.candidate?.client.hostEpoch,
diff --git a/apps/desktop/src/main/runtime-host-management.ts b/apps/desktop/src/main/runtime-host-management.ts
index 3b82eb0b9..c1e82c737 100644
--- a/apps/desktop/src/main/runtime-host-management.ts
+++ b/apps/desktop/src/main/runtime-host-management.ts
@@ -54,6 +54,7 @@ import type {
RuntimeHostServiceUpdateReconciliationTerminalFrame,
RuntimeHostServiceUpdateTerminalFrame,
} from './runtime-host-ssh-terminal.js';
+import type { DesktopRuntimeHostDevelopmentPeerTarget } from './runtime-host-setup-package.js';
const MANAGEMENT_ACTIONS = new Set<DesktopRuntimeHostManagementAction>([
'status',
@@ -104,7 +105,15 @@ export function createDesktopRuntimeHostManagement(input: {
input: DesktopRuntimeHostSshUpdateReconciliationInput,
onProgress: (phase: DesktopRuntimeHostManagementProgress['phase']) => void,
) => Promise<RuntimeHostServiceUpdateReconciliationTerminalFrame>;
- readonly resolveUpdatePackage: () =>
+ readonly setupPackageMode: 'published' | 'development';
+ readonly resolveSshDevelopmentPeerTarget: (input: {
+ readonly destination: string;
+ readonly sshPort?: number;
+ readonly signal?: AbortSignal;
+ }) => Promise<Exclude<DesktopRuntimeHostDevelopmentPeerTarget, 'none'>>;
+ readonly resolveUpdatePackage: (
+ peerTarget: DesktopRuntimeHostDevelopmentPeerTarget,
+ ) =>
| DesktopRuntimeHostSetupPackage
| Promise<DesktopRuntimeHostSetupPackage>;
readonly currentHostEpoch: (profileId: string) => string | undefined;
@@ -466,7 +475,13 @@ export function createDesktopRuntimeHostManagement(input: {
await managedMutationTarget(profileIdValue);
const previousHostEpoch = input.currentHostEpoch(profileId);
input.sendProgress({ profileId, phase: 'preparing_cli' });
- const setupPackage = await input.resolveUpdatePackage();
+ const peerTarget = input.setupPackageMode === 'development'
+ ? await input.resolveSshDevelopmentPeerTarget({
+ destination: transport.destination,
+ ...(transport.sshPort === undefined ? {} : { sshPort: transport.sshPort }),
+ })
+ : 'none';
+ const setupPackage = await input.resolveUpdatePackage(peerTarget);
const response = await input.runUpdate(
{
destination: transport.destination,
diff --git a/apps/desktop/src/main/runtime-host-onboarding.ts b/apps/desktop/src/main/runtime-host-onboarding.ts
index 1c9674b8e..fd39a4276 100644
--- a/apps/desktop/src/main/runtime-host-onboarding.ts
+++ b/apps/desktop/src/main/runtime-host-onboarding.ts
@@ -33,6 +33,7 @@ import type {
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';
import { requireProjectDirectoryRoots } from '../shared/runtime-host-project-directory-policy.js';
type OnboardingState = DesktopRuntimeHostOnboardingSnapshot extends infer Snapshot
@@ -65,7 +66,14 @@ export function createDesktopRuntimeHostOnboarding(input: {
) => Promise<{ readonly rootId: string; readonly operatorPath: string }>;
readonly listWslDistributions: () => Promise<readonly string[]>;
readonly send: (snapshot: DesktopRuntimeHostOnboardingSnapshot) => void;
+ readonly setupPackageMode: 'published' | 'development';
+ readonly resolveSshDevelopmentPeerTarget: (input: {
+ readonly destination: string;
+ readonly sshPort?: number;
+ readonly signal?: AbortSignal;
+ }) => Promise<Exclude<DesktopRuntimeHostDevelopmentPeerTarget, 'none'>>;
readonly resolveSetupPackage: (
+ peerTarget: DesktopRuntimeHostDevelopmentPeerTarget,
signal?: AbortSignal,
) => DesktopRuntimeHostSetupPackage | Promise<DesktopRuntimeHostSetupPackage>;
}): { close(): Promise<void> } {
@@ -113,8 +121,14 @@ export function createDesktopRuntimeHostOnboarding(input: {
signal: AbortSignal,
): Promise<DesktopRuntimeHostOnboardingSnapshot> => {
try {
- const setupPackage = await input.resolveSetupPackage(signal);
- if (request.kind === 'wsl') return await runWsl(request, setupPackage, signal);
+ if (request.kind === 'wsl') {
+ const setupPackage = await input.resolveSetupPackage('none', signal);
+ return await runWsl(request, setupPackage, signal);
+ }
+ const peerTarget = input.setupPackageMode === 'development'
+ ? await resolveSshDevelopmentPeerTarget(request, signal)
+ : 'none';
+ const setupPackage = await input.resolveSetupPackage(peerTarget, signal);
const lifecycle = setupPackage.kind === 'npm' ? 'on_demand' : 'supervised';
signal.throwIfAborted();
publish({ kind: 'running', phase: 'connecting_ssh' });
@@ -203,6 +217,20 @@ export function createDesktopRuntimeHostOnboarding(input: {
}
};
+ const resolveSshDevelopmentPeerTarget = async (
+ request: Extract<DesktopRuntimeHostOnboardingInput, { readonly kind: 'ssh' }>,
+ signal: AbortSignal,
+ ): Promise<Exclude<DesktopRuntimeHostDevelopmentPeerTarget, 'none'>> => {
+ publish({ kind: 'running', phase: 'connecting_ssh' });
+ const target = await input.resolveSshDevelopmentPeerTarget({
+ destination: request.destination,
+ ...(request.sshPort === undefined ? {} : { sshPort: request.sshPort }),
+ signal,
+ });
+ publish({ kind: 'running', phase: 'preparing_cli' });
+ return target;
+ };
+
const runWsl = async (
request: Extract<DesktopRuntimeHostOnboardingInput, { readonly kind: 'wsl' }>,
setupPackage: DesktopRuntimeHostSetupPackage,
diff --git a/apps/desktop/src/main/runtime-host-setup-package.ts b/apps/desktop/src/main/runtime-host-setup-package.ts
index 7a9ca7f10..5a182ace5 100644
--- a/apps/desktop/src/main/runtime-host-setup-package.ts
+++ b/apps/desktop/src/main/runtime-host-setup-package.ts
@@ -36,33 +36,68 @@ interface DevelopmentArchiveBuild {
close(): Promise<void>;
}
+interface DevelopmentBuildState {
+ readonly task: DevelopmentArchiveBuild;
+ readonly result: Promise<DesktopRuntimeHostSetupPackage>;
+ waiters: number;
+ settled: boolean;
+ closing?: Promise<void>;
+}
+
+export type DesktopRuntimeHostDevelopmentPeerTarget =
+ | 'none'
+ | 'darwin-arm64'
+ | 'linux-arm64'
+ | 'linux-x64'
+ | 'win32-x64';
+
export interface RuntimeHostSetupPackageResolver {
- resolve(signal?: AbortSignal): Promise<DesktopRuntimeHostSetupPackage>;
+ readonly mode: 'published' | 'development';
+ resolve(
+ peerTarget: DesktopRuntimeHostDevelopmentPeerTarget,
+ signal?: AbortSignal,
+ ): Promise<DesktopRuntimeHostSetupPackage>;
close(): Promise<void>;
}
+export function desktopRuntimeHostDevelopmentPeerTarget(
+ platform: NodeJS.Platform = process.platform,
+ arch: string = process.arch,
+): Exclude<DesktopRuntimeHostDevelopmentPeerTarget, 'none'> {
+ const target = `${platform}-${arch}`;
+ if (
+ target !== 'darwin-arm64' &&
+ target !== 'linux-arm64' &&
+ target !== 'linux-x64' &&
+ target !== 'win32-x64'
+ ) {
+ throw new Error(`Direct peer is not available on ${target}`);
+ }
+ return target;
+}
+
export function createRuntimeHostSetupPackageResolver(input: {
readonly isPackaged: boolean;
readonly appPath: string;
readonly environment: NodeJS.ProcessEnv;
- readonly startDevelopmentArchiveBuild?: (repoRoot: string) => DevelopmentArchiveBuild;
+ readonly startDevelopmentArchiveBuild?: (
+ repoRoot: string,
+ peerTarget: DesktopRuntimeHostDevelopmentPeerTarget,
+ ) => DevelopmentArchiveBuild;
}): RuntimeHostSetupPackageResolver {
let closed = false;
- let developmentBuild:
- | {
- readonly task: DevelopmentArchiveBuild;
- readonly result: Promise<DesktopRuntimeHostSetupPackage>;
- waiters: number;
- settled: boolean;
- closing?: Promise<void>;
- }
- | undefined;
+ const developmentBuilds = new Map<
+ DesktopRuntimeHostDevelopmentPeerTarget,
+ DevelopmentBuildState
+ >();
- const startBuild = () => {
+ const startBuild = (
+ peerTarget: DesktopRuntimeHostDevelopmentPeerTarget,
+ ): DevelopmentBuildState => {
const repoRoot = resolve(input.appPath, '..', '..');
- const task = input.startDevelopmentArchiveBuild?.(repoRoot) ??
- startDevelopmentArchiveBuild(repoRoot, input.environment);
- const build = {
+ const task = input.startDevelopmentArchiveBuild?.(repoRoot, peerTarget) ??
+ startDevelopmentArchiveBuild(repoRoot, input.environment, peerTarget);
+ const build: DevelopmentBuildState = {
task,
result: task.result.then((path) => ({
kind: 'development_archive' as const,
@@ -71,62 +106,71 @@ export function createRuntimeHostSetupPackageResolver(input: {
waiters: 0,
settled: false,
};
- developmentBuild = build;
+ developmentBuilds.set(peerTarget, build);
void build.result.then(
() => {
build.settled = true;
},
() => {
build.settled = true;
- if (developmentBuild === build) developmentBuild = undefined;
+ if (developmentBuilds.get(peerTarget) === build) {
+ developmentBuilds.delete(peerTarget);
+ }
},
);
return build;
};
- const stopBuild = async (build: NonNullable<typeof developmentBuild>) => {
+ const stopBuild = async (
+ peerTarget: DesktopRuntimeHostDevelopmentPeerTarget,
+ build: DevelopmentBuildState,
+ ) => {
build.closing ??= build.task.close().finally(() => {
- if (developmentBuild === build) developmentBuild = undefined;
+ if (developmentBuilds.get(peerTarget) === build) developmentBuilds.delete(peerTarget);
});
await build.closing;
await build.result.catch(() => undefined);
};
- const acquireBuild = async (signal?: AbortSignal) => {
+ const acquireBuild = async (
+ peerTarget: DesktopRuntimeHostDevelopmentPeerTarget,
+ signal?: AbortSignal,
+ ) => {
while (true) {
if (closed) throw new Error('Runtime Host setup package resolver is closed');
- const build = developmentBuild;
- if (!build) return startBuild();
+ const build = developmentBuilds.get(peerTarget);
+ if (!build) return startBuild(peerTarget);
if (!build.closing) return build;
await waitForPackage(build.closing, signal);
}
};
return {
- async resolve(signal) {
+ mode: input.isPackaged ? 'published' : 'development',
+ async resolve(peerTarget, signal) {
if (closed) throw new Error('Runtime Host setup package resolver is closed');
if (input.isPackaged) return packagedSetupPackage(input.appPath);
const override = input.environment[DEVELOPMENT_ARCHIVE_ENV];
if (override) return { kind: 'development_archive', path: override };
- const build = await acquireBuild(signal);
+ const build = await acquireBuild(peerTarget, signal);
build.waiters += 1;
try {
return await waitForPackage(build.result, signal);
} finally {
build.waiters -= 1;
if (signal?.aborted && build.waiters === 0 && !build.settled) {
- await stopBuild(build);
+ await stopBuild(peerTarget, build);
}
}
},
async close() {
if (closed) return;
closed = true;
- const build = developmentBuild;
- developmentBuild = undefined;
- if (build) await stopBuild(build);
+ const builds = [...developmentBuilds.entries()];
+ developmentBuilds.clear();
+ await Promise.all(builds.map(([peerTarget, build]) => stopBuild(peerTarget, build)));
},
};
}
@@ -144,6 +188,7 @@ function packagedSetupPackage(appPath: string): DesktopRuntimeHostSetupPackage {
function startDevelopmentArchiveBuild(
repoRoot: string,
environment: NodeJS.ProcessEnv,
+ peerTarget: DesktopRuntimeHostDevelopmentPeerTarget,
): DevelopmentArchiveBuild {
const script = join(repoRoot, 'scripts', 'release-cli-package.mjs');
const nodeExecutable = environment.npm_node_execpath?.trim() || 'node';
@@ -153,7 +198,11 @@ function startDevelopmentArchiveBuild(
const child = spawn(nodeExecutable, [script, '--development'], {
cwd: repoRoot,
detached: process.platform !== 'win32',
- env: { ...environment, MAKA_CLI_DEVELOPMENT_OUTPUT_ROOT: outputRoot },
+ env: {
+ ...environment,
+ MAKA_CLI_DEVELOPMENT_OUTPUT_ROOT: outputRoot,
+ MAKA_CLI_DEVELOPMENT_PEER_TARGET: peerTarget,
+ },
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
});
diff --git a/apps/desktop/src/main/runtime-host-ssh-terminal.ts b/apps/desktop/src/main/runtime-host-ssh-terminal.ts
index 1b2d82e21..97b133c39 100644
--- a/apps/desktop/src/main/runtime-host-ssh-terminal.ts
+++ b/apps/desktop/src/main/runtime-host-ssh-terminal.ts
@@ -67,6 +67,7 @@ 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';
interface ActiveTerminal {
readonly sessionId: string;
@@ -105,6 +106,12 @@ export interface DesktopRuntimeHostSshSetupInput {
readonly signal?: AbortSignal;
}
+export interface DesktopRuntimeHostSshTargetInput {
+ readonly destination: string;
+ readonly sshPort?: number;
+ readonly signal?: AbortSignal;
+}
+
export interface DesktopRuntimeHostSshManagementInput {
readonly destination: string;
readonly sshPort?: number;
@@ -236,6 +243,9 @@ export function createDesktopRuntimeHostSshTerminal(input: {
input: RuntimeHostSshOperatorActivationInput,
): Promise<RuntimeHostActivationResult>;
openSshTunnel(input: RuntimeHostSshTunnelInput): Promise<RuntimeHostSshTunnel>;
+ resolveDevelopmentPeerTarget(
+ input: DesktopRuntimeHostSshTargetInput,
+ ): Promise<Exclude<DesktopRuntimeHostDevelopmentPeerTarget, 'none'>>;
runSetup(
input: DesktopRuntimeHostSshSetupInput,
onProgress: (frame: Extract<RuntimeHostSetupFrame, { kind: 'progress' }>) => void,
@@ -603,6 +613,66 @@ export function createDesktopRuntimeHostSshTerminal(input: {
}
return tunnel;
},
+ resolveDevelopmentPeerTarget: async (targetInput) => {
+ if (closed) throw new Error('Runtime Host SSH terminal is closed');
+ targetInput.signal?.throwIfAborted();
+ const destination = normalizeRuntimeHostSshDestination(targetInput.destination);
+ const sshPort = targetInput.sshPort === undefined
+ ? undefined
+ : requireSetupPort(targetInput.sshPort);
+ const marker = `__MAKA_RUNTIME_HOST_TARGET_${randomUUID().replaceAll('-', '')}__`;
+ const remoteCommand = `printf '${marker}%s:%s\\n' "$(uname -s)" "$(uname -m)"`;
+ let target: Exclude<DesktopRuntimeHostDevelopmentPeerTarget, 'none'> | undefined;
+ let failure: Error | undefined;
+ const filter = createRuntimeHostFramedOutputFilter({
+ prefix: marker,
+ pendingMaxBytes: 256,
+ decode: (line) => line.slice(marker.length).replaceAll('\r', '').trimEnd(),
+ label: 'Remote Runtime Host target detection',
+ onFrame: (identity) => {
+ if (target) {
+ failure = new Error('Remote Runtime Host target detection returned multiple results');
+ return;
+ }
+ const [system, machine, ...extra] = identity.split(':');
+ if (!system || !machine || extra.length > 0) {
+ failure = new Error('Remote Runtime Host target detection returned an invalid result');
+ return;
+ }
+ try {
+ target = runtimeHostDevelopmentPeerTargetFromUname(system, machine);
+ } catch (error) {
+ failure = error instanceof Error ? error : new Error(String(error));
+ }
+ },
+ onError: (error) => {
+ failure = error;
+ },
+ });
+ const { process, terminal } = startTerminalProcess(
+ 'ssh',
+ sshRemoteCommandArgs(destination, sshPort, remoteCommand),
+ filter.push,
+ true,
+ );
+ const wait = await waitForTerminalProcess(process, {
+ signal: targetInput.signal,
+ timeoutMs: input.managementTimeoutMs ?? MANAGEMENT_TIMEOUT_MS,
+ stopGraceMs: input.processStopGraceMs,
+ onAbort: () => dismissPresentation(terminal),
+ }, input.terminateProcessTree);
+ if (wait.timedOut) throw new Error('Remote Runtime Host target detection timed out');
+ if (wait.exit.code !== 0) {
+ throw new Error(
+ `Remote Runtime Host target detection exited with code ${String(wait.exit.code)}`,
+ );
+ }
+ filter.finish();
+ if (failure) throw failure;
+ completePresentation(terminal);
+ if (!target) throw new Error('Remote Runtime Host target detection returned no result');
+ return target;
+ },
runSetup: async (setupInput, onProgress, onComplete) => {
if (closed) throw new Error('Runtime Host SSH terminal is closed');
setupInput.signal?.throwIfAborted();
@@ -842,6 +912,21 @@ export function createDesktopRuntimeHostSshTerminal(input: {
};
}
+export function runtimeHostDevelopmentPeerTargetFromUname(
+ system: string,
+ machine: string,
+): Exclude<DesktopRuntimeHostDevelopmentPeerTarget, 'none'> {
+ const normalizedMachine = machine.toLowerCase();
+ if (system === 'Darwin' && normalizedMachine === 'arm64') return 'darwin-arm64';
+ if (system === 'Linux') {
+ if (normalizedMachine === 'x86_64') return 'linux-x64';
+ if (normalizedMachine === 'aarch64' || normalizedMachine === 'arm64') {
+ return 'linux-arm64';
+ }
+ }
+ throw new Error(`Direct peer is not available on ${system}/${machine}`);
+}
+
function cancellableUntilComplete(signal: AbortSignal | undefined): {
readonly signal: AbortSignal;
commit(): boolean;
diff --git a/native/runtime-host-peer/build.mjs b/native/runtime-host-peer/build.mjs
index b103cfe15..9c9d77ffe 100644
--- a/native/runtime-host-peer/build.mjs
+++ b/native/runtime-host-peer/build.mjs
@@ -53,6 +53,13 @@ const targetPlatform = cargoTarget
? 'darwin'
: 'linux'
: process.platform;
+const targetArch = cargoTarget
+ ? cargoTarget.startsWith('aarch64-')
+ ? 'arm64'
+ : cargoTarget.startsWith('x86_64-')
+ ? 'x64'
+ : undefined
+ : process.arch;
const library =
targetPlatform === 'win32'
@@ -67,7 +74,11 @@ if (targetPlatform === 'darwin' && process.platform === 'darwin') {
...process.env,
});
await run('strip', ['-x', destination], root, { ...process.env });
-} else if (targetPlatform === 'linux' && process.platform === 'linux') {
+} else if (
+ targetPlatform === 'linux' &&
+ process.platform === 'linux' &&
+ targetArch === process.arch
+) {
await run('strip', ['--strip-unneeded', destination], root, { ...process.env });
}
if ((await readFile(destination)).includes(Buffer.from(root))) {
diff --git a/scripts/release-cli-package.mjs b/scripts/release-cli-package.mjs
index 2a6a12a22..da67bac7e 100644
--- a/scripts/release-cli-package.mjs
+++ b/scripts/release-cli-package.mjs
@@ -58,6 +58,9 @@ const releaseRoot = join(cliSource, 'release');
const artifactRoot = developmentBuild ? createDevelopmentArtifactRoot() : releaseRoot;
const stageRoot = join(artifactRoot, 'package');
const peerPrebuildTargets = ['darwin-arm64', 'linux-arm64', 'linux-x64', 'win32-x64'];
+const privatePeerTarget = developmentBuild
+ ? resolveDevelopmentPeerTarget()
+ : `${process.platform}-${process.arch}`;
const unsupportedArguments = process.argv
.slice(2)
.filter((argument) => !['--allow-dirty', '--development'].includes(argument));
@@ -157,7 +160,7 @@ function packageCli(publishable) {
entryCount: pack.entryCount,
});
const tarballPath = join(artifactRoot, pack.filename);
- validatePackedFiles(pack.files, expectedDependencyManifests);
+ validatePackedFiles(pack.files, expectedDependencyManifests, publishable);
const sha256 = digestFile(tarballPath);
writeFileSync(`${tarballPath}.sha256`, `${sha256} ${pack.filename}\n`, 'utf8');
writeFileSync(
@@ -542,16 +545,15 @@ function copyReleaseDocuments() {
function copyRuntimeHostPeerPrebuilds(publishable) {
let sourceRoot = process.env.MAKA_RUNTIME_HOST_PEER_PREBUILDS?.trim();
let generatedRoot;
- let targets = peerPrebuildTargets;
+ const targets = publishable
+ ? peerPrebuildTargets
+ : privatePeerTarget === 'none'
+ ? []
+ : [privatePeerTarget];
+ if (targets.length === 0) return;
if (!sourceRoot && !publishable) {
- execFileSync(process.execPath, [join(repoRoot, 'native/runtime-host-peer/build.mjs')], {
- cwd: repoRoot,
- stdio: 'inherit',
- });
- const target = `${process.platform}-${process.arch}`;
- if (!peerPrebuildTargets.includes(target)) {
- throw new Error(`Direct peer is not supported on ${target}`);
- }
+ 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 });
@@ -559,7 +561,6 @@ function copyRuntimeHostPeerPrebuilds(publishable) {
join(repoRoot, 'native/runtime-host-peer/target/release/maka_runtime_host_peer.node'),
join(targetRoot, 'maka_runtime_host_peer.node'),
);
- targets = [target];
}
if (!sourceRoot) {
throw new Error('MAKA_RUNTIME_HOST_PEER_PREBUILDS must contain all release platform addons');
@@ -580,6 +581,60 @@ function copyRuntimeHostPeerPrebuilds(publishable) {
}
}
+function resolveDevelopmentPeerTarget() {
+ const configured = process.env.MAKA_CLI_DEVELOPMENT_PEER_TARGET?.trim();
+ const target = configured || `${process.platform}-${process.arch}`;
+ if (target !== 'none' && !peerPrebuildTargets.includes(target)) {
+ throw new Error(
+ `MAKA_CLI_DEVELOPMENT_PEER_TARGET must be none or a supported target; found ${target}`,
+ );
+ }
+ return target;
+}
+
+function buildDevelopmentPeerAddon(target) {
+ 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' });
+ return;
+ }
+ const rustTarget = {
+ 'linux-arm64': 'aarch64-unknown-linux-gnu.2.28',
+ 'linux-x64': 'x86_64-unknown-linux-gnu.2.28',
+ }[target];
+ if (!rustTarget) {
+ throw new Error(
+ `Cannot build the ${target} direct-peer addon from ${hostTarget}; run Desktop on that target or provide MAKA_RUNTIME_HOST_PEER_PREBUILDS`,
+ );
+ }
+ requireDevelopmentCommand(
+ 'zig',
+ ['version'],
+ `Cross-compiling the ${target} direct-peer addon requires Zig on PATH (CI uses 0.16.x)`,
+ );
+ requireDevelopmentCommand(
+ 'cargo-zigbuild',
+ ['--version'],
+ `Cross-compiling the ${target} direct-peer addon requires cargo-zigbuild (cargo install cargo-zigbuild --version 0.23.2 --locked)`,
+ );
+ execFileSync(process.execPath, [buildScript], {
+ cwd: repoRoot,
+ env: {
+ ...process.env,
+ MAKA_RUNTIME_HOST_PEER_CARGO_SUBCOMMAND: 'zigbuild',
+ MAKA_RUNTIME_HOST_PEER_CARGO_TARGET: rustTarget,
+ },
+ stdio: 'inherit',
+ });
+}
+
+function requireDevelopmentCommand(command, args, message) {
+ const result = spawnSync(command, args, { cwd: repoRoot, encoding: 'utf8' });
+ if (result.status === 0) return;
+ throw new Error(`${message}; install it before setting up this development Runtime Host`);
+}
+
function writeReleaseManifest(cli, publishable) {
const source = readJson(join(cliSource, 'package.json'));
const root = readJson(join(repoRoot, 'package.json'));
@@ -690,9 +745,9 @@ function validateStaging(publishable) {
(target) => `native/runtime-host-peer/prebuilds/${target}/maka_runtime_host_peer.node`,
),
);
- } else {
+ } else if (privatePeerTarget !== 'none') {
required.push(
- `native/runtime-host-peer/prebuilds/${process.platform}-${process.arch}/maka_runtime_host_peer.node`,
+ `native/runtime-host-peer/prebuilds/${privatePeerTarget}/maka_runtime_host_peer.node`,
);
}
for (const path of required) {
@@ -745,7 +800,7 @@ function validateStaging(publishable) {
}
}
-function validatePackedFiles(files, expectedDependencyManifests) {
+function validatePackedFiles(files, expectedDependencyManifests, publishable) {
const paths = files.map((file) => file.path);
for (const file of files) {
const { path } = file;
@@ -780,7 +835,7 @@ function validatePackedFiles(files, expectedDependencyManifests) {
'node_modules/@maka/runtime/dist/workers/filesystem-worker.js',
'node_modules/@maka/runtime-host/dist/execution-candidate-main.js',
'packages/eval/harbor/relay_agent.py',
- 'native/runtime-host-peer/prebuilds/',
+ ...(publishable || privatePeerTarget !== 'none' ? ['native/runtime-host-peer/prebuilds/'] : []),
];
for (const suffix of requiredPacked) {
if (
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org