From eeb142f3424410e2951ec238b89cf6581cc6d017 Mon Sep 17 00:00:00 2001 From: Danilo Tuler Date: Mon, 21 Jul 2025 09:36:48 -0400 Subject: [PATCH 1/6] feat(cli): remove unnecessary finalHash config --- .changeset/plain-moons-kneel.md | 5 +++++ apps/cli/src/commands/shell.ts | 3 --- apps/cli/src/config.ts | 3 --- apps/cli/src/machine.ts | 4 ---- apps/cli/tests/unit/config.test.ts | 6 ------ 5 files changed, 5 insertions(+), 16 deletions(-) create mode 100644 .changeset/plain-moons-kneel.md diff --git a/.changeset/plain-moons-kneel.md b/.changeset/plain-moons-kneel.md new file mode 100644 index 00000000..2f8e3561 --- /dev/null +++ b/.changeset/plain-moons-kneel.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +remove unnecessary finalHash config diff --git a/apps/cli/src/commands/shell.ts b/apps/cli/src/commands/shell.ts index 61e7ead4..faec3055 100755 --- a/apps/cli/src/commands/shell.ts +++ b/apps/cli/src/commands/shell.ts @@ -44,9 +44,6 @@ export const createShellCommand = () => { // start with interactive mode on config.machine.interactive = true; - // interactive mode can't have final hash - config.machine.finalHash = false; - // do not store machine in interactive mode config.machine.store = undefined; diff --git a/apps/cli/src/config.ts b/apps/cli/src/config.ts index 8236388f..7699225a 100644 --- a/apps/cli/src/config.ts +++ b/apps/cli/src/config.ts @@ -144,7 +144,6 @@ export type MachineConfig = { assertRollingTemplate?: boolean; // default given by cartesi-machine bootargs: string[]; entrypoint?: string; - finalHash: boolean; interactive?: boolean; // default given by cartesi-machine maxMCycle?: bigint; // default given by cartesi-machine noRollup?: boolean; // default given by cartesi-machine @@ -178,7 +177,6 @@ export const defaultMachineConfig = (): MachineConfig => ({ assertRollingTemplate: undefined, bootargs: [], entrypoint: undefined, - finalHash: true, interactive: undefined, maxMCycle: undefined, noRollup: undefined, @@ -375,7 +373,6 @@ const parseMachine = (value: TomlPrimitive): MachineConfig => { ), bootargs: parseStringArray(toml.boot_args), entrypoint: parseOptionalString(toml.entrypoint), - finalHash: parseBoolean(toml.final_hash, true), interactive: undefined, maxMCycle: parseOptionalNumber(toml.max_mcycle), noRollup: parseBoolean(toml.no_rollup, false), diff --git a/apps/cli/src/machine.ts b/apps/cli/src/machine.ts index c9fdee1d..1ce94c71 100644 --- a/apps/cli/src/machine.ts +++ b/apps/cli/src/machine.ts @@ -28,7 +28,6 @@ export const bootMachine = ( const { machine } = config; const { assertRollingTemplate, - finalHash, interactive, maxMCycle, noRollup, @@ -91,9 +90,6 @@ export const bootMachine = ( if (assertRollingTemplate) { args.push("--assert-rolling-template"); } - if (finalHash) { - args.push("--final-hash"); - } if (useDockerWorkdir && info?.workdir) { args.push(`--workdir="${info.workdir}"`); } diff --git a/apps/cli/tests/unit/config.test.ts b/apps/cli/tests/unit/config.test.ts index 3491d8da..e7e57dc6 100644 --- a/apps/cli/tests/unit/config.test.ts +++ b/apps/cli/tests/unit/config.test.ts @@ -273,12 +273,6 @@ shared = true`); ).not.toThrow(); }); - it("should fail for invalid boolean value", () => { - expect(() => parse("[machine]\nfinal_hash = 42")).toThrowError( - new InvalidBooleanValueError(42), - ); - }); - it("should fail for invalid optional boolean value", () => { expect(() => parse("[machine]\nassert_rolling_template = 42"), From d4253478a21a9bce48c6198e973dc3fea9209dc4 Mon Sep 17 00:00:00 2001 From: Danilo Tuler Date: Mon, 21 Jul 2025 09:39:49 -0400 Subject: [PATCH 2/6] chore(cli): using ExecaOptionsDockerFallback instead of additional param --- apps/cli/src/commands/build.ts | 3 ++- apps/cli/src/commands/shell.ts | 3 ++- apps/cli/src/machine.ts | 2 -- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/cli/src/commands/build.ts b/apps/cli/src/commands/build.ts index 263b98aa..e408f1c1 100755 --- a/apps/cli/src/commands/build.ts +++ b/apps/cli/src/commands/build.ts @@ -141,7 +141,8 @@ export const createBuildCommand = () => { ); // create machine snapshot - await bootMachine(config, imageInfo, destination, { + await bootMachine(config, imageInfo, { + cwd: destination, stdout: new WritableStream({ write(chunk) { task.output = chunk; diff --git a/apps/cli/src/commands/shell.ts b/apps/cli/src/commands/shell.ts index faec3055..fbd8333f 100755 --- a/apps/cli/src/commands/shell.ts +++ b/apps/cli/src/commands/shell.ts @@ -52,7 +52,8 @@ export const createShellCommand = () => { // boot machine try { - await bootMachine(config, info, destination, { + await bootMachine(config, info, { + cwd: destination, stdio: "inherit", }); } catch (error: unknown) { diff --git a/apps/cli/src/machine.ts b/apps/cli/src/machine.ts index 1ce94c71..fadf60ec 100644 --- a/apps/cli/src/machine.ts +++ b/apps/cli/src/machine.ts @@ -22,7 +22,6 @@ const flashDrive = (label: string, drive: DriveConfig): string => { export const bootMachine = ( config: Config, info: ImageInfo | undefined, - destination: string, options?: ExecaOptionsDockerFallback, ) => { const { machine } = config; @@ -112,7 +111,6 @@ export const bootMachine = ( args.push(entrypoint); return cartesiMachine.boot(args, { - cwd: destination, image: config.sdk, ...options, }); From 391f33a9dc733aac3e2c99ba23bc9349f3d584b0 Mon Sep 17 00:00:00 2001 From: Danilo Tuler Date: Mon, 21 Jul 2025 10:00:43 -0400 Subject: [PATCH 3/6] feat(cli): remove unnecessary store and interactive config --- .changeset/curly-dolls-cross.md | 5 +++++ .changeset/nice-olives-spend.md | 5 +++++ apps/cli/src/commands/build.ts | 35 +++++++++++++++++++-------------- apps/cli/src/commands/shell.ts | 19 +++++++++--------- apps/cli/src/config.ts | 6 ------ apps/cli/src/machine.ts | 18 ++++++++++++----- 6 files changed, 52 insertions(+), 36 deletions(-) create mode 100644 .changeset/curly-dolls-cross.md create mode 100644 .changeset/nice-olives-spend.md diff --git a/.changeset/curly-dolls-cross.md b/.changeset/curly-dolls-cross.md new file mode 100644 index 00000000..f6a3053c --- /dev/null +++ b/.changeset/curly-dolls-cross.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +remove unnecessary interactive config diff --git a/.changeset/nice-olives-spend.md b/.changeset/nice-olives-spend.md new file mode 100644 index 00000000..b011963b --- /dev/null +++ b/.changeset/nice-olives-spend.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +remove unnecessary store config diff --git a/apps/cli/src/commands/build.ts b/apps/cli/src/commands/build.ts index e408f1c1..2dcc7a79 100755 --- a/apps/cli/src/commands/build.ts +++ b/apps/cli/src/commands/build.ts @@ -134,24 +134,29 @@ export const createBuildCommand = () => { task: async (ctx, task) => { const { destination, imageInfo } = ctx; - // path of machine snapshot - const snapshotPath = path.join( - destination, - "image", - ); - // create machine snapshot - await bootMachine(config, imageInfo, { - cwd: destination, - stdout: new WritableStream({ - write(chunk) { - task.output = chunk; - }, - }), - }); + await bootMachine( + config, + imageInfo, + { + finalHash: true, + store: "image", + }, + { + cwd: destination, + stdout: new WritableStream({ + write(chunk) { + task.output = chunk; + }, + }), + }, + ); // make snapshot readable by all users, because cartesi-machine sets to 600 - await fs.chmod(snapshotPath, 0o755); + await fs.chmod( + path.join(destination, "image"), + 0o755, + ); // get and display machine hash const hash = getMachineHash(); diff --git a/apps/cli/src/commands/shell.ts b/apps/cli/src/commands/shell.ts index fbd8333f..e624e756 100755 --- a/apps/cli/src/commands/shell.ts +++ b/apps/cli/src/commands/shell.ts @@ -41,21 +41,20 @@ export const createShellCommand = () => { workdir: "/", }; - // start with interactive mode on - config.machine.interactive = true; - - // do not store machine in interactive mode - config.machine.store = undefined; - // run as root if flag is set config.machine.user = runAsRoot ? "root" : undefined; // boot machine try { - await bootMachine(config, info, { - cwd: destination, - stdio: "inherit", - }); + await bootMachine( + config, + info, + { interactive: true }, // start with interactive mode on + { + cwd: destination, + stdio: "inherit", + }, + ); } catch (error: unknown) { if (error instanceof ExecaError) { // just continue gracefully diff --git a/apps/cli/src/config.ts b/apps/cli/src/config.ts index 7699225a..6ebe6c93 100644 --- a/apps/cli/src/config.ts +++ b/apps/cli/src/config.ts @@ -144,12 +144,10 @@ export type MachineConfig = { assertRollingTemplate?: boolean; // default given by cartesi-machine bootargs: string[]; entrypoint?: string; - interactive?: boolean; // default given by cartesi-machine maxMCycle?: bigint; // default given by cartesi-machine noRollup?: boolean; // default given by cartesi-machine ramLength: string; ramImage: string; - store?: string; useDockerEnv: boolean; // inject docker image ENV into cartesi-machine ENV useDockerWorkdir: boolean; // inject docker image WORKDIR into cartesi-machine WORKDIR user?: string; // default given by cartesi-machine @@ -177,12 +175,10 @@ export const defaultMachineConfig = (): MachineConfig => ({ assertRollingTemplate: undefined, bootargs: [], entrypoint: undefined, - interactive: undefined, maxMCycle: undefined, noRollup: undefined, ramLength: DEFAULT_RAM, ramImage: DEFAULT_RAM_IMAGE, - store: "image", useDockerEnv: true, useDockerWorkdir: true, user: undefined, @@ -373,12 +369,10 @@ const parseMachine = (value: TomlPrimitive): MachineConfig => { ), bootargs: parseStringArray(toml.boot_args), entrypoint: parseOptionalString(toml.entrypoint), - interactive: undefined, maxMCycle: parseOptionalNumber(toml.max_mcycle), noRollup: parseBoolean(toml.no_rollup, false), ramLength: parseString(toml.ram_length, DEFAULT_RAM), ramImage: parseString(toml.ram_image, DEFAULT_RAM_IMAGE), - store: "image", useDockerEnv: parseBoolean(toml.use_docker_env, true), useDockerWorkdir: parseBoolean(toml.use_docker_workdir, true), user: parseOptionalString(toml.user), diff --git a/apps/cli/src/machine.ts b/apps/cli/src/machine.ts index fadf60ec..f9b9b3c9 100644 --- a/apps/cli/src/machine.ts +++ b/apps/cli/src/machine.ts @@ -19,20 +19,25 @@ const flashDrive = (label: string, drive: DriveConfig): string => { return `--flash-drive=${vars.join(",")}`; }; +export type BootMachineOptions = { + finalHash?: boolean; + interactive?: boolean; + store?: string; +}; + export const bootMachine = ( config: Config, info: ImageInfo | undefined, + bootOptions: BootMachineOptions, options?: ExecaOptionsDockerFallback, ) => { const { machine } = config; const { assertRollingTemplate, - interactive, maxMCycle, noRollup, ramLength, ramImage, - store, useDockerEnv, useDockerWorkdir, user, @@ -89,10 +94,13 @@ export const bootMachine = ( if (assertRollingTemplate) { args.push("--assert-rolling-template"); } + if (bootOptions.finalHash) { + args.push("--final-hash"); + } if (useDockerWorkdir && info?.workdir) { args.push(`--workdir="${info.workdir}"`); } - if (interactive) { + if (bootOptions.interactive) { args.push("-it"); } if (noRollup) { @@ -101,8 +109,8 @@ export const bootMachine = ( if (maxMCycle) { args.push(`--max-mcycle=${maxMCycle.toString()}`); } - if (store) { - args.push(`--store=${store}`); + if (bootOptions.store) { + args.push(`--store=${bootOptions.store}`); } if (user) { args.push(`--user=${user}`); From f784de0f4393260e6145fa65de9b8b531a160b56 Mon Sep 17 00:00:00 2001 From: Danilo Tuler Date: Mon, 21 Jul 2025 17:30:58 -0400 Subject: [PATCH 4/6] feat(cli): possibility to use an override config file --- .changeset/chubby-eagles-mate.md | 5 +++ apps/cli/src/base.ts | 12 ++++-- apps/cli/src/commands/build.ts | 3 +- apps/cli/src/commands/shell.ts | 3 +- apps/cli/src/config.ts | 65 +++++++++++++++++++++++++++- apps/cli/tests/unit/config.test.ts | 68 +++++++++++++++--------------- 6 files changed, 115 insertions(+), 41 deletions(-) create mode 100644 .changeset/chubby-eagles-mate.md diff --git a/.changeset/chubby-eagles-mate.md b/.changeset/chubby-eagles-mate.md new file mode 100644 index 00000000..cd42e902 --- /dev/null +++ b/.changeset/chubby-eagles-mate.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +possibility to use an override config file diff --git a/apps/cli/src/base.ts b/apps/cli/src/base.ts index 862756cb..d1c34470 100644 --- a/apps/cli/src/base.ts +++ b/apps/cli/src/base.ts @@ -44,10 +44,14 @@ export const getMachineHash = (): Hash | undefined => { return undefined; }; -export const getApplicationConfig = (configPath: string): Config => { - return fs.existsSync(configPath) - ? parse(fs.readFileSync(configPath).toString()) - : parse(""); +export const getApplicationConfig = (configPaths: string[]): Config => { + const tomls = configPaths.map((configPath) => { + if (fs.existsSync(configPath)) { + return fs.readFileSync(configPath).toString(); + } + throw new Error(`Config file ${configPath} does not exist`); + }); + return parse(tomls); }; export const getProjectName = (options: { projectName?: string }) => { diff --git a/apps/cli/src/commands/build.ts b/apps/cli/src/commands/build.ts index 2dcc7a79..38458974 100755 --- a/apps/cli/src/commands/build.ts +++ b/apps/cli/src/commands/build.ts @@ -79,7 +79,8 @@ export const createBuildCommand = () => { .option( "-c, --config ", "path to the configuration file", - "cartesi.toml", + (value, prev) => prev.concat([value]), + ["cartesi.toml"], ) .addOption( new Option( diff --git a/apps/cli/src/commands/shell.ts b/apps/cli/src/commands/shell.ts index e624e756..f603d72a 100755 --- a/apps/cli/src/commands/shell.ts +++ b/apps/cli/src/commands/shell.ts @@ -12,7 +12,8 @@ export const createShellCommand = () => { .option( "-c, --config ", "path to the configuration file", - "cartesi.toml", + (value, prev) => prev.concat([value]), + ["cartesi.toml"], ) .option("--run-as-root", "run as root user", false) .action(async (options) => { diff --git a/apps/cli/src/config.ts b/apps/cli/src/config.ts index 6ebe6c93..6764c0c8 100644 --- a/apps/cli/src/config.ts +++ b/apps/cli/src/config.ts @@ -497,8 +497,11 @@ const parseDrives = (config: TomlPrimitive): Record => { return drives; }; -export const parse = (str: string): Config => { - const toml = parseToml(str); +export const parse = (str: string[]): Config => { + let toml: TomlTable = {}; + for (const s of str) { + toml = mergeTomlTables(toml, parseToml(s)); + } const config: Config = { drives: parseDrives(toml.drives), @@ -511,3 +514,61 @@ export const parse = (str: string): Config => { return config; }; + +/** + * Checks if a value is a plain object (TOML table) + */ +function isTomlTable(value: TomlPrimitive): value is TomlTable { + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + !("toISOString" in value) + ); // Check for TomlDate (has toISOString method) +} + +/** + * Recursively merges two TOML table objects + * Values from 'other' take precedence over 'base' + * + * @param base - The base TOML table + * @param other - The TOML table to merge into base (takes precedence) + * @returns A new merged TOML table + */ +export function mergeTomlTables(base: TomlTable, other: TomlTable): TomlTable { + const result: TomlTable = { ...base }; + + for (const [key, otherValue] of Object.entries(other)) { + const baseValue = result[key]; + + // If both values are tables, merge them recursively + if (isTomlTable(baseValue) && isTomlTable(otherValue)) { + result[key] = mergeTomlTables(baseValue, otherValue); + } else { + // For all other cases, other value takes precedence + result[key] = otherValue; + } + } + + return result; +} + +/** + * Merges two TOML values of any type + * + * @param base - The base TOML value + * @param other - The TOML value to merge into base (takes precedence) + * @returns The merged TOML value + */ +export function mergeTomlValues( + base: TomlPrimitive, + other: TomlPrimitive, +): TomlPrimitive { + // If both are tables, merge recursively + if (isTomlTable(base) && isTomlTable(other)) { + return mergeTomlTables(base, other); + } + + // For arrays, replaces entirely + return other; +} diff --git a/apps/cli/tests/unit/config.test.ts b/apps/cli/tests/unit/config.test.ts index e7e57dc6..bf90071b 100644 --- a/apps/cli/tests/unit/config.test.ts +++ b/apps/cli/tests/unit/config.test.ts @@ -23,7 +23,7 @@ const loadDriveConfig = (driveName: string) => { "drives", `${driveName}.toml`, ); - return fs.readFileSync(filePath, "utf-8"); + return [fs.readFileSync(filePath, "utf-8")]; }; describe("when parsing only drive config files", () => { @@ -60,15 +60,17 @@ describe("when parsing only drive config files", () => { describe("when parsing a cartesi.toml config", () => { it("should load the default config when file is empty", () => { - const config = parse(""); + const config = parse([""]); expect(config).toEqual(defaultConfig()); }); it("non-standard root drive", () => { - const config = parse(`[drives.root] + const config = parse([ + `[drives.root] builder = "docker" dockerfile = "backend/Dockerfile" -shared = true`); +shared = true`, + ]); expect(config).toEqual({ ...defaultConfig(), @@ -100,7 +102,7 @@ shared = true`); no_rollup = true `; it("machine-config", () => { - expect(parse(config)).toEqual({ + expect(parse([config])).toEqual({ ...defaultConfig(), machine: { ...defaultMachineConfig(), @@ -113,7 +115,7 @@ shared = true`); ${config} boot_args = ["no4lvl", "quiet", false] `; - expect(() => parse(invalidConfig)).toThrowError( + expect(() => parse([invalidConfig])).toThrowError( new InvalidStringValueError(false), ); }); @@ -122,7 +124,7 @@ shared = true`); ${config} entrypoint = "echo 'Hello, World!'" `; - expect(parse(entrypointConfig)).toEqual({ + expect(parse([entrypointConfig])).toEqual({ ...defaultConfig(), machine: { ...defaultMachineConfig(), @@ -138,37 +140,37 @@ shared = true`); */ describe("when parsing [drives]", () => { it("should fail for invalid configuration", () => { - expect(parse("drives = 42")).toEqual(defaultConfig()); - expect(parse("drives.root = true")).toEqual(defaultConfig()); - expect(parse("drives.root = 42")).toEqual(defaultConfig()); + expect(parse(["drives = 42"])).toEqual(defaultConfig()); + expect(parse(["drives.root = true"])).toEqual(defaultConfig()); + expect(parse(["drives.root = 42"])).toEqual(defaultConfig()); }); it("should fail for invalid builder", () => { expect(() => - parse('[drives.root]\nbuilder = "invalid"'), + parse(['[drives.root]\nbuilder = "invalid"']), ).toThrowError(new InvalidBuilderError("invalid")); - expect(() => parse("[drives.root]\nbuilder = true")).toThrowError( + expect(() => parse(["[drives.root]\nbuilder = true"])).toThrowError( new InvalidBuilderError(true), ); - expect(() => parse("[drives.root]\nbuilder = 10")).toThrowError( + expect(() => parse(["[drives.root]\nbuilder = 10"])).toThrowError( new InvalidBuilderError(10), ); - expect(() => parse("[drives.root]\nbuilder = {}")).toThrowError( + expect(() => parse(["[drives.root]\nbuilder = {}"])).toThrowError( new InvalidBuilderError({}), ); }); it("should fail for invalid format", () => { expect(() => - parse('[drives.root]\nformat = "invalid"'), + parse(['[drives.root]\nformat = "invalid"']), ).toThrowError(new InvalidDriveFormatError("invalid")); - expect(() => parse("[drives.root]\nformat = true")).toThrowError( + expect(() => parse(["[drives.root]\nformat = true"])).toThrowError( new InvalidDriveFormatError(true), ); - expect(() => parse("[drives.root]\nformat = 10")).toThrowError( + expect(() => parse(["[drives.root]\nformat = 10"])).toThrowError( new InvalidDriveFormatError(10), ); - expect(() => parse("[drives.root]\nformat = {}")).toThrowError( + expect(() => parse(["[drives.root]\nformat = {}"])).toThrowError( new InvalidDriveFormatError({}), ); }); @@ -180,20 +182,20 @@ shared = true`); filename = "./games/doom.xyzfs" mount = "/usr/local/games/doom" `; - expect(() => parse(builderNone)).toThrowError( + expect(() => parse([builderNone])).toThrowError( new InvalidDriveFormatError(".xyzfs"), ); }); it("should fail for invalid mount", () => { - expect(() => parse("[drives.data]\nmount = 42")).toThrowError( + expect(() => parse(["[drives.data]\nmount = 42"])).toThrowError( new InvalidStringValueError(42), ); }); it("should fail for invalid empty drive format", () => { expect(() => - parse("[drives.data]\nbuilder = 'empty'\nformat = 42"), + parse(["[drives.data]\nbuilder = 'empty'\nformat = 42"]), ).toThrowError(new InvalidEmptyDriveFormatError(42)); }); }); @@ -203,13 +205,13 @@ shared = true`); */ describe("when parsing fields types", () => { it("should fail for invalid boolean value", () => { - expect(() => parse("[machine]\nno_rollup = 42")).toThrowError( + expect(() => parse(["[machine]\nno_rollup = 42"])).toThrowError( new InvalidBooleanValueError(42), ); }); it("should fail for invalid number value", () => { - expect(() => parse("[machine]\nmax_mcycle = 'abc'")).toThrowError( + expect(() => parse(["[machine]\nmax_mcycle = 'abc'"])).toThrowError( new InvalidNumberValueError("abc"), ); }); @@ -221,7 +223,7 @@ shared = true`); filename = 42 # invalid format = "ext2" `; - expect(() => parse(invalidTarDrive)).toThrowError( + expect(() => parse([invalidTarDrive])).toThrowError( new InvalidStringValueError(42), ); }); @@ -234,7 +236,7 @@ shared = true`); filename = "data.tar" format = "ext2" `; - expect(() => parse(invalidTarDrive)).toThrowError( + expect(() => parse([invalidTarDrive])).toThrowError( new InvalidBytesValueError("abc"), ); }); @@ -242,40 +244,40 @@ shared = true`); it("should pass for valid bytes value", () => { // nukmber expect(() => - parse( + parse([ `[drives.data] builder = "directory" directory = "/data" extra_size = 128 `, - ), + ]), ).not.toThrow(); // string expect(() => - parse( + parse([ `[drives.data] builder = "directory" directory = "/data" extra_size = "128MB" `, - ), + ]), ).not.toThrow(); // bigint const bigInt = BigInt(128); expect(() => - parse( + parse([ `[drives.data] builder = "directory" directory = "/data" extra_size = ${bigInt} `, - ), + ]), ).not.toThrow(); }); it("should fail for invalid optional boolean value", () => { expect(() => - parse("[machine]\nassert_rolling_template = 42"), + parse(["[machine]\nassert_rolling_template = 42"]), ).toThrowError(new InvalidBooleanValueError(42)); }); @@ -285,7 +287,7 @@ shared = true`); builder = "directory" # directory = '' # required `; - expect(() => parse(invalidDirectoryDrive)).toThrowError( + expect(() => parse([invalidDirectoryDrive])).toThrowError( new RequiredFieldError("directory"), //XXX: how to know which field was required ); }); From bb32f393cb62714d6038e011b0e11595cd5da03a Mon Sep 17 00:00:00 2001 From: Danilo Tuler Date: Tue, 22 Jul 2025 14:52:23 -0400 Subject: [PATCH 5/6] feat(cli): fix cartesi-machine boot --- apps/cli/src/commands/build.ts | 70 ++++++++++++---------------------- 1 file changed, 24 insertions(+), 46 deletions(-) diff --git a/apps/cli/src/commands/build.ts b/apps/cli/src/commands/build.ts index 38458974..958fa08d 100755 --- a/apps/cli/src/commands/build.ts +++ b/apps/cli/src/commands/build.ts @@ -4,11 +4,7 @@ import fs from "fs-extra"; import { Listr, type ListrTask } from "listr2"; import path from "node:path"; import tmp from "tmp"; -import { - getApplicationConfig, - getContextPath, - getMachineHash, -} from "../base.js"; +import { getApplicationConfig, getContextPath } from "../base.js"; import { buildDirectory, buildDocker, @@ -129,49 +125,31 @@ export const createBuildCommand = () => { }); }, }, - { - title: "Build Cartesi machine", - enabled: !drivesOnly, // if only build drives, don't do this task - task: async (ctx, task) => { - const { destination, imageInfo } = ctx; - - // create machine snapshot - await bootMachine( - config, - imageInfo, - { - finalHash: true, - store: "image", - }, - { - cwd: destination, - stdout: new WritableStream({ - write(chunk) { - task.output = chunk; - }, - }), - }, - ); - - // make snapshot readable by all users, because cartesi-machine sets to 600 - await fs.chmod( - path.join(destination, "image"), - 0o755, - ); - - // get and display machine hash - const hash = getMachineHash(); - if (hash) { - task.title = `Build Cartesi machine ${chalk.cyan(hash)}`; - } - }, - rendererOptions: { - outputBar: 5, - }, - }, ], { ctx, renderer: verbose ? "verbose" : "default" }, ); - await builds.run(); + const result = await builds.run(); + + // if only build drives, quit here + if (drivesOnly) { + return; + } + + // create machine snapshot + await bootMachine( + config, + result.imageInfo, + { + finalHash: true, + store: "image", + }, + { + cwd: destination, + stdio: "inherit", + }, + ); + + // make snapshot readable by all users, because cartesi-machine sets to 600 + await fs.chmod(path.join(destination, "image"), 0o755); }); }; From 9a602ce4094a78345f11e3cafc3134a0e26e69c8 Mon Sep 17 00:00:00 2001 From: Danilo Tuler Date: Tue, 22 Jul 2025 15:08:59 -0400 Subject: [PATCH 6/6] feat(cli): fix shell entrypoint --- apps/cli/src/commands/shell.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/cli/src/commands/shell.ts b/apps/cli/src/commands/shell.ts index f603d72a..4f843664 100755 --- a/apps/cli/src/commands/shell.ts +++ b/apps/cli/src/commands/shell.ts @@ -3,7 +3,6 @@ import { ExecaError } from "execa"; import fs from "fs-extra"; import path from "node:path"; import { getApplicationConfig, getContextPath } from "../base.js"; -import type { ImageInfo } from "../config.js"; import { bootMachine } from "../machine.js"; export const createShellCommand = () => { @@ -35,12 +34,7 @@ export const createShellCommand = () => { } // create shell entrypoint - const info: ImageInfo = { - cmd: [], - entrypoint: [command], - env: [], - workdir: "/", - }; + config.machine.entrypoint = command; // run as root if flag is set config.machine.user = runAsRoot ? "root" : undefined; @@ -49,7 +43,7 @@ export const createShellCommand = () => { try { await bootMachine( config, - info, + undefined, { interactive: true }, // start with interactive mode on { cwd: destination,