From 2f9f96eaf79318a414f6a0373c25bee8b9954dc4 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 27 Sep 2025 13:48:43 +0200 Subject: [PATCH 1/3] feat: support for running the emulator with apple container --- package-lock.json | 14 ++-- package.json | 2 +- src/services/emulator/_runner.services.ts | 66 ++++++++++++++++--- src/utils/runner.utils.ts | 79 ++++++++++++++++++++++- 4 files changed, 140 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8630f1ef..6ec2dfda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@junobuild/admin": "^2.3.0", "@junobuild/cdn": "^1.3.2", "@junobuild/cli-tools": "^0.8.0", - "@junobuild/config": "^2.3.0-next-2025-09-27.1", + "@junobuild/config": "^2.3.0-next-2025-09-27.2", "@junobuild/config-loader": "^0.4.5", "@junobuild/core": "^2.2.0", "@junobuild/did-tools": "^0.3.3", @@ -1490,9 +1490,9 @@ } }, "node_modules/@junobuild/config": { - "version": "2.3.0-next-2025-09-27.1", - "resolved": "https://registry.npmjs.org/@junobuild/config/-/config-2.3.0-next-2025-09-27.1.tgz", - "integrity": "sha512-2BHK8B0iDDNylkYfR4gjSREqUXhfBb9EJMZjQqDU60m803XBkxeHw8q6lQpcwsV94Qiahat9an9kMOCFcIcPmw==", + "version": "2.3.0-next-2025-09-27.2", + "resolved": "https://registry.npmjs.org/@junobuild/config/-/config-2.3.0-next-2025-09-27.2.tgz", + "integrity": "sha512-MFAChIKF3NUMFLRL5zGD2ZT8nYbWncz0r/wFl5+y1J9rhsrXQ6vSd8/XwLngLkFZKyYulDENqA/8uuyaoiXPpw==", "license": "MIT", "peerDependencies": { "@dfinity/zod-schemas": "*", @@ -7304,9 +7304,9 @@ } }, "@junobuild/config": { - "version": "2.3.0-next-2025-09-27.1", - "resolved": "https://registry.npmjs.org/@junobuild/config/-/config-2.3.0-next-2025-09-27.1.tgz", - "integrity": "sha512-2BHK8B0iDDNylkYfR4gjSREqUXhfBb9EJMZjQqDU60m803XBkxeHw8q6lQpcwsV94Qiahat9an9kMOCFcIcPmw==", + "version": "2.3.0-next-2025-09-27.2", + "resolved": "https://registry.npmjs.org/@junobuild/config/-/config-2.3.0-next-2025-09-27.2.tgz", + "integrity": "sha512-MFAChIKF3NUMFLRL5zGD2ZT8nYbWncz0r/wFl5+y1J9rhsrXQ6vSd8/XwLngLkFZKyYulDENqA/8uuyaoiXPpw==", "requires": {} }, "@junobuild/config-loader": { diff --git a/package.json b/package.json index 6c2a203f..90d6de7d 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@junobuild/admin": "^2.3.0", "@junobuild/cdn": "^1.3.2", "@junobuild/cli-tools": "^0.8.0", - "@junobuild/config": "^2.3.0-next-2025-09-27.1", + "@junobuild/config": "^2.3.0-next-2025-09-27.2", "@junobuild/config-loader": "^0.4.5", "@junobuild/core": "^2.2.0", "@junobuild/did-tools": "^0.3.3", diff --git a/src/services/emulator/_runner.services.ts b/src/services/emulator/_runner.services.ts index 637063aa..1cfb602a 100644 --- a/src/services/emulator/_runner.services.ts +++ b/src/services/emulator/_runner.services.ts @@ -1,6 +1,6 @@ import {nonNullish} from '@dfinity/utils'; import {assertAnswerCtrlC, execute, spawn} from '@junobuild/cli-tools'; -import {type EmulatorPorts} from '@junobuild/config'; +import {type EmulatorPorts, type EmulatorRunner} from '@junobuild/config'; import {red, yellow} from 'kleur'; import {basename, join} from 'node:path'; import prompts from 'prompts'; @@ -23,6 +23,7 @@ import { assertContainerRunnerRunning, checkDockerVersion, hasExistingContainer, + hasExistingVolume, isContainerRunning } from '../../utils/runner.utils'; import {initConfigNoneInteractive} from '../config/init.services'; @@ -100,9 +101,10 @@ const promptRunnerType = async (): Promise<{runnerType: EmulatorRunnerType}> => choices: [ { title: 'Docker', - value: `docker` + value: 'docker' }, - {title: `Podman`, value: `podman`} + {title: 'Podman', value: 'podman'}, + {title: 'Apple container', value: 'container'} ] }); @@ -209,12 +211,28 @@ const startEmulator = async ({config: extendedConfig}: {config: CliEmulatorConfi // Podman does not auto create the path folders. await createDeployTargetDir({targetDeploy}); + // Apple container does not auto create the volume. + const {result: createResult} = await createVolume({volume, runner}); + + if (createResult === 'error') { + console.log(red(`Unable to create a volume ${volume} for ${runner}.`)); + return; + } + const image = config.runner?.image ?? `junobuild/${emulatorType}:latest`; const platform = config.runner?.platform; const network = config?.network; + const volumes = [ + `${volume}:/juno/.juno`, + ...(nonNullish(configFile) && nonNullish(configFilePath) + ? [`${configFilePath}:/juno/${configFile}`] + : []), + `${targetDeploy}:/juno/target/deploy` + ]; + await execute({ command: runner, args: [ @@ -233,13 +251,7 @@ const startEmulator = async ({config: extendedConfig}: {config: CliEmulatorConfi ] : []), ...(nonNullish(network) ? ['-e', `NETWORK=${JSON.stringify(network)}`] : []), - '-v', - `${volume}:/juno/.juno`, - ...(nonNullish(configFile) && nonNullish(configFilePath) - ? ['-v', `${configFilePath}:/juno/${configFile}`] - : []), - '-v', - `${targetDeploy}:/juno/target/deploy`, + ...volumes.flatMap((v) => ['-v', v]), ...(nonNullish(platform) ? [`--platform=${platform}`] : []), image ] @@ -278,3 +290,37 @@ const assertContainerRunning = async ({ return result; }; + +const createVolume = async ({ + volume, + runner +}: Pick & Required>): Promise<{ + result: 'success' | 'error' | 'skip'; +}> => { + // Docker and Podman auto-create the volume on run + if (runner !== 'container') { + return {result: 'skip'}; + } + + const check = await hasExistingVolume({ + volume, + runner + }); + + if ('err' in check) { + return {result: 'error'}; + } + + const {exist} = check; + + if (exist) { + return {result: 'skip'}; + } + + await execute({ + command: runner, + args: ['volume', 'create', volume] + }); + + return {result: 'success'}; +}; diff --git a/src/utils/runner.utils.ts b/src/utils/runner.utils.ts index 1847cc81..5806f31f 100644 --- a/src/utils/runner.utils.ts +++ b/src/utils/runner.utils.ts @@ -1,4 +1,6 @@ +import {notEmptyString} from '@dfinity/utils'; import {spawn} from '@junobuild/cli-tools'; +import {type EmulatorRunner} from '@junobuild/config'; import {green, red, yellow} from 'kleur'; import {lt} from 'semver'; import {DOCKER_MIN_VERSION} from '../constants/dev.constants'; @@ -35,9 +37,13 @@ export const assertContainerRunnerRunning = async ({ runner }: Pick) => { try { + // container does not support ps + // Reference: https://github.com/apple/container/pull/299 + const args = runner === 'container' ? ['ls', '--quiet'] : ['ps', '--quiet']; + await spawn({ command: runner, - args: ['ps', '--quiet'], + args, silentOut: true }); } catch (_e: unknown) { @@ -54,13 +60,64 @@ export const hasExistingContainer = async ({ > => { try { let output = ''; + + const args = + runner === 'container' ? ['ls', '-aq'] : ['ps', '-aq', '-f', `name=^/${containerName}$`]; + await spawn({ command: runner, - args: ['ps', '-aq', '-f', `name=^/${containerName}$`], + args, stdout: (o) => (output += o), silentOut: true }); + if (runner === 'container') { + const exist = output + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(notEmptyString) + .some((name) => name === containerName); + + return {exist}; + } + + return {exist: output.trim().length > 0}; + } catch (err: unknown) { + return {err}; + } +}; + +export const hasExistingVolume = async ({ + volume, + runner +}: Pick & Required>): Promise< + {exist: boolean} | {err: unknown} +> => { + try { + let output = ''; + + const args = + runner === 'container' + ? ['volume', 'ls', '-q'] + : ['volume', 'ls', '-q', '-f', `name=^${volume}$`]; + + await spawn({ + command: runner, + args, + stdout: (o) => (output += o), + silentOut: true + }); + + if (runner === 'container') { + const exist = output + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(notEmptyString) + .some((name) => name === volume); + + return {exist}; + } + return {exist: output.trim().length > 0}; } catch (err: unknown) { return {err}; @@ -75,13 +132,29 @@ export const isContainerRunning = async ({ > => { try { let output = ''; + + const args = + runner === 'container' + ? ['ls', '--quiet'] + : ['ps', '--quiet', '-f', `name=^/${containerName}$`]; + await spawn({ command: runner, - args: ['ps', '--quiet', '-f', `name=^/${containerName}$`], + args, stdout: (o) => (output += o), silentOut: true }); + if (runner === 'container') { + const running = output + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(notEmptyString) + .some((name) => name === containerName); + + return {running}; + } + return {running: output.trim().length > 0}; } catch (err: unknown) { return {err}; From 88fef3026ab0e3c878692f95a8cfb195971bb0f2 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 15 Nov 2025 08:51:26 +0100 Subject: [PATCH 2/3] build: next up-to-date --- package-lock.json | 18 +++++++++--------- package.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 66fb8ad1..687cbb7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@junobuild/admin": "^3.0.1", "@junobuild/cdn": "^2.0.1", "@junobuild/cli-tools": "^0.9.0", - "@junobuild/config": "^2.6.0", + "@junobuild/config": "^2.6.0-next-2025-11-15.1", "@junobuild/config-loader": "^0.4.6", "@junobuild/core": "^3.1.0", "@junobuild/did-tools": "^0.3.4", @@ -1735,14 +1735,14 @@ } }, "node_modules/@junobuild/config": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@junobuild/config/-/config-2.6.0.tgz", - "integrity": "sha512-yZhRmitD6ykXXSEK4l/6N6CB83gqW9PlI5+seikWJUsOdTQA9nQaVnCPZRGACvqPzfkp5tbsjl9r8buJdFrSWA==", + "version": "2.6.0-next-2025-11-15.1", + "resolved": "https://registry.npmjs.org/@junobuild/config/-/config-2.6.0-next-2025-11-15.1.tgz", + "integrity": "sha512-8SVbS+IaVYhtslTonEmicTifyAesxcMQL2u2y+71b5G63TsKqLj1EmEPxS4Krf1poofM9Has7MTNasdGv3/RLQ==", "license": "MIT", "peer": true, "peerDependencies": { - "@dfinity/zod-schemas": "^3", - "zod": "^4" + "@dfinity/zod-schemas": "*", + "zod": "*" } }, "node_modules/@junobuild/config-loader": { @@ -7842,9 +7842,9 @@ } }, "@junobuild/config": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@junobuild/config/-/config-2.6.0.tgz", - "integrity": "sha512-yZhRmitD6ykXXSEK4l/6N6CB83gqW9PlI5+seikWJUsOdTQA9nQaVnCPZRGACvqPzfkp5tbsjl9r8buJdFrSWA==", + "version": "2.6.0-next-2025-11-15.1", + "resolved": "https://registry.npmjs.org/@junobuild/config/-/config-2.6.0-next-2025-11-15.1.tgz", + "integrity": "sha512-8SVbS+IaVYhtslTonEmicTifyAesxcMQL2u2y+71b5G63TsKqLj1EmEPxS4Krf1poofM9Has7MTNasdGv3/RLQ==", "peer": true, "requires": {} }, diff --git a/package.json b/package.json index 32871990..02c8046a 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@junobuild/admin": "^3.0.1", "@junobuild/cdn": "^2.0.1", "@junobuild/cli-tools": "^0.9.0", - "@junobuild/config": "^2.6.0", + "@junobuild/config": "^2.6.0-next-2025-11-15.1", "@junobuild/config-loader": "^0.4.6", "@junobuild/core": "^3.1.0", "@junobuild/did-tools": "^0.3.4", From 63d02d37318fd6bdb9a00dfed3c8abdce9c429d2 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 17 Nov 2025 10:15:14 +0100 Subject: [PATCH 3/3] feat: container now creates volume --- src/services/emulator/_runner.services.ts | 57 +---------------------- src/utils/runner.utils.ts | 38 --------------- 2 files changed, 2 insertions(+), 93 deletions(-) diff --git a/src/services/emulator/_runner.services.ts b/src/services/emulator/_runner.services.ts index 377b0609..826bf027 100644 --- a/src/services/emulator/_runner.services.ts +++ b/src/services/emulator/_runner.services.ts @@ -1,11 +1,10 @@ import {nonNullish} from '@dfinity/utils'; import {assertAnswerCtrlC, execute, spawn} from '@junobuild/cli-tools'; -import {type EmulatorPorts, type EmulatorRunner} from '@junobuild/config'; +import {type EmulatorPorts} from '@junobuild/config'; import {red, yellow} from 'kleur'; -import {basename, join} from 'node:path'; import prompts from 'prompts'; import {readEmulatorConfig} from '../../configs/emulator.config'; -import {junoConfigExist, junoConfigFile} from '../../configs/juno.config'; +import {junoConfigExist} from '../../configs/juno.config'; import { EMULATOR_PORT_ADMIN, EMULATOR_PORT_CONSOLE, @@ -23,7 +22,6 @@ import { assertContainerRunnerRunning, checkDockerVersion, hasExistingContainer, - hasExistingVolume, isContainerRunning } from '../../utils/runner.utils'; import {initConfigNoneInteractive} from '../config/init.services'; @@ -204,23 +202,9 @@ const startEmulator = async ({config: extendedConfig}: {config: CliEmulatorConfi const volume = config.runner?.volume ?? containerName.replaceAll('-', '_'); - const detectedConfig = junoConfigFile(); - const configFile = nonNullish(detectedConfig.configPath) - ? basename(detectedConfig.configPath) - : undefined; - const configFilePath = nonNullish(configFile) ? join(process.cwd(), configFile) : undefined; - // Podman does not auto create the path folders. await createDeployTargetDir({targetDeploy}); - // Apple container does not auto create the volume. - const {result: createResult} = await createVolume({volume, runner}); - - if (createResult === 'error') { - console.log(red(`Unable to create a volume ${volume} for ${runner}.`)); - return; - } - const image = config.runner?.image ?? `junobuild/${emulatorType}:latest`; const platform = config.runner?.platform; @@ -247,9 +231,6 @@ const startEmulator = async ({config: extendedConfig}: {config: CliEmulatorConfi : []), '-v', `${volume}:/juno/.juno`, - ...(nonNullish(configFile) && nonNullish(configFilePath) - ? ['-v', `${configFilePath}:/juno/${configFile}`] - : []), '-v', `${targetDeploy}:/juno/target/deploy`, ...(nonNullish(platform) ? [`--platform=${platform}`] : []), @@ -290,37 +271,3 @@ const assertContainerRunning = async ({ return result; }; - -const createVolume = async ({ - volume, - runner -}: Pick & Required>): Promise<{ - result: 'success' | 'error' | 'skip'; -}> => { - // Docker and Podman auto-create the volume on run - if (runner !== 'container') { - return {result: 'skip'}; - } - - const check = await hasExistingVolume({ - volume, - runner - }); - - if ('err' in check) { - return {result: 'error'}; - } - - const {exist} = check; - - if (exist) { - return {result: 'skip'}; - } - - await execute({ - command: runner, - args: ['volume', 'create', volume] - }); - - return {result: 'success'}; -}; diff --git a/src/utils/runner.utils.ts b/src/utils/runner.utils.ts index 5806f31f..ce4a425a 100644 --- a/src/utils/runner.utils.ts +++ b/src/utils/runner.utils.ts @@ -1,6 +1,5 @@ import {notEmptyString} from '@dfinity/utils'; import {spawn} from '@junobuild/cli-tools'; -import {type EmulatorRunner} from '@junobuild/config'; import {green, red, yellow} from 'kleur'; import {lt} from 'semver'; import {DOCKER_MIN_VERSION} from '../constants/dev.constants'; @@ -87,43 +86,6 @@ export const hasExistingContainer = async ({ } }; -export const hasExistingVolume = async ({ - volume, - runner -}: Pick & Required>): Promise< - {exist: boolean} | {err: unknown} -> => { - try { - let output = ''; - - const args = - runner === 'container' - ? ['volume', 'ls', '-q'] - : ['volume', 'ls', '-q', '-f', `name=^${volume}$`]; - - await spawn({ - command: runner, - args, - stdout: (o) => (output += o), - silentOut: true - }); - - if (runner === 'container') { - const exist = output - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(notEmptyString) - .some((name) => name === volume); - - return {exist}; - } - - return {exist: output.trim().length > 0}; - } catch (err: unknown) { - return {err}; - } -}; - export const isContainerRunning = async ({ containerName, runner