Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clear-falcons-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cartesi/cli": patch
---

Fix the cartesi-machine version check ignoring its `forceDocker` option, which made it report the version of the host binary instead of the one inside the SDK image.
5 changes: 5 additions & 0 deletions .changeset/four-plums-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cartesi/cli": patch
---

Bump CLI default sdk to 0.12.0.alpha.44
5 changes: 5 additions & 0 deletions .changeset/free-facts-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cartesi/cli": patch
---

`build` and `shell` now check the cartesi-machine version before booting and stop with an explicit message when it is unsupported, instead of surfacing the emulator's `unrecognized option` traceback.
5 changes: 5 additions & 0 deletions .changeset/tired-lights-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cartesi/cli": patch
---

Add support for `nvrams` in `cartesi.toml`. An nvram is a raw range of bytes the guest reaches through a `/dev/uio*` device, with no filesystem and no mount point, so writes are visible to the emulator without a page cache in between. Declare one with `[nvrams.<label>]` and either `size`, for a range filled with zeros, or `filename`, pointing at an existing raw image whose size defines the range. Add `shared` so guest writes are persisted to the image, and `user` so the unprivileged entrypoint user can write to it. Up to 8 nvrams are supported, their labels cannot collide with drive labels, and sizes must be a multiple of 4Ki.
5 changes: 5 additions & 0 deletions .changeset/violet-flies-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cartesi/cli": patch
---

Set the minimum cartesi-machine version to 0.21.0. The `nvrams` configuration depends on its `--nvram` option, so a host install of an earlier version will no longer work.
5 changes: 5 additions & 0 deletions .changeset/wild-camels-sink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cartesi/cli": patch
---

`doctor` now reports the cartesi-machine version and fails when it does not satisfy the version required by the CLI.
1 change: 1 addition & 0 deletions apps/cli/src/builder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export { build as buildDirectory } from "./directory.js";
export { build as buildDocker } from "./docker.js";
export { build as buildEmpty } from "./empty.js";
export { build as buildNone } from "./none.js";
export { build as buildNvram } from "./nvram.js";
export { build as buildTar } from "./tar.js";
47 changes: 47 additions & 0 deletions apps/cli/src/builder/nvram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import fs from "fs-extra";
import path from "node:path";
import {
MissingNvramSourceError,
NVRAM_ALIGNMENT,
type NvramConfig,
nvramHasImage,
nvramImageFilename,
} from "../config.js";

export const build = async (
label: string,
nvram: NvramConfig,
destination: string,
): Promise<void> => {
// a pristine nvram needs no image, cartesi-machine fills its range with zeros
if (!nvramHasImage(nvram)) {
return;
}

const target = path.join(destination, nvramImageFilename(label));
const source = nvram.filename;

if (source === undefined) {
// shared with no image of its own, start it filled with zeros
if (nvram.size === undefined) {
throw new MissingNvramSourceError(label);
}
await fs.writeFile(target, Buffer.alloc(nvram.size));
return;
}

const { size } = await fs.stat(source);
if (nvram.size !== undefined && nvram.size !== size) {
throw new Error(
`Size ${nvram.size} of nvram '${label}' does not match the ${size} bytes of ${source}`,
);
}
if (size % NVRAM_ALIGNMENT !== 0) {
throw new Error(
`Image ${source} of nvram '${label}' has ${size} bytes, which is not a multiple of ${NVRAM_ALIGNMENT}`,
);
}

// copy it into the destination, so it is reachable when running inside the sdk image
await fs.copyFile(source, target);
};
68 changes: 51 additions & 17 deletions apps/cli/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
buildDocker,
buildEmpty,
buildNone,
buildNvram,
buildTar,
} from "../builder/index.js";
import type { Config, DriveConfig, ImageInfo } from "../config.js";
import type { Config, DriveConfig, ImageInfo, NvramConfig } from "../config.js";
import { bootMachine } from "../machine.js";

// context for Listr build tasks
Expand Down Expand Up @@ -79,6 +80,17 @@ const buildDriveTask = (
},
});

const buildNvramTask = (
label: string,
nvram: NvramConfig,
): ListrTask<BuildContext> => ({
title: `Building nvram ${chalk.cyan(label)}`,
task: async (ctx, task) => {
await buildNvram(label, nvram, ctx.destination);
task.title = `Build nvram ${chalk.cyan(label)}`;
},
});

export const createBuildCommand = () => {
return new Command("build")
.description(
Expand Down Expand Up @@ -129,23 +141,45 @@ export const createBuildCommand = () => {
([name, drive]) => buildDriveTask(name, drive),
);

const builds = new Listr(
[
{
title: "Build drives",
task: async (_ctx, task) => {
return task.newListr(driveTasks, {
concurrent: true,
rendererOptions: {
collapseSubtasks: false,
},
ctx,
});
},
},
],
{ ctx, renderer: verbose ? "verbose" : "default" },
// tasks to build the images backing nvrams, pristine ones need none
const nvramTasks = Object.entries(config.nvrams).map(
([label, nvram]) => buildNvramTask(label, nvram),
);

const groups: ListrTask<BuildContext>[] = [
{
title: "Build drives",
task: async (_ctx, task) => {
return task.newListr(driveTasks, {
concurrent: true,
rendererOptions: {
collapseSubtasks: false,
},
ctx,
});
},
},
];

if (nvramTasks.length > 0) {
groups.push({
title: "Build nvrams",
task: async (_ctx, task) => {
return task.newListr(nvramTasks, {
concurrent: true,
rendererOptions: {
collapseSubtasks: false,
},
ctx,
});
},
});
}

const builds = new Listr(groups, {
ctx,
renderer: verbose ? "verbose" : "default",
});
const result = await builds.run();

// if only build drives, quit here
Expand Down
27 changes: 27 additions & 0 deletions apps/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import chalk from "chalk";
import { execa } from "execa";
import ora, { type Ora } from "ora";
import semver from "semver";
import { DEFAULT_SDK_IMAGE, DEFAULT_SDK_VERSION } from "../config.js";
import { cartesiMachine } from "../exec/index.js";

const MINIMUM_DOCKER_VERSION = "25.0.0"; // Replace with our minimum required Docker version
const MINIMUM_DOCKER_COMPOSE_VERSION = "2.24.0"; // Replace with our minimum required Docker Compose version
Expand Down Expand Up @@ -119,13 +121,38 @@ const checkBuildx = async (progress: Ora): Promise<true | never> => {
return true;
};

const checkCartesiMachine = async (progress: Ora): Promise<true | never> => {
progress.start("Checking Cartesi Machine version...");

// doctor does not read cartesi.toml, so check against the default sdk image. the host binary
// still takes precedence, which is the install most likely to be out of date
const v = await cartesiMachine.version({
image: `${DEFAULT_SDK_IMAGE}:${DEFAULT_SDK_VERSION}`,
});

if (v === null) {
throw new Error(
"Could not determine the Cartesi Machine version. Check that Docker is running.",
);
}
if (!semver.satisfies(v.format(), cartesiMachine.requiredVersion)) {
throw new Error(
`Unsupported Cartesi Machine version. Required version is ${cartesiMachine.requiredVersion.raw}. Installed version is ${v.format()}.`,
);
}
progress.succeed(`Cartesi Machine ${chalk.cyan(v.format())}`);

return true;
};

export const createDoctorCommand = () => {
return new Command("doctor").action(async () => {
const progress = ora();
try {
await checkDocker(progress);
await checkCompose(progress);
await checkBuildx(progress);
await checkCartesiMachine(progress);
progress.succeed("Your system is ready.");
} catch (e: unknown) {
progress.fail((e as Error).message);
Expand Down
12 changes: 12 additions & 0 deletions apps/cli/src/commands/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ExecaError } from "execa";
import fs from "fs-extra";
import path from "node:path";
import { getApplicationConfig, getContextPath } from "../base.js";
import { nvramHasImage, nvramImageFilename } from "../config.js";
import { bootMachine } from "../machine.js";

export const createShellCommand = () => {
Expand Down Expand Up @@ -33,6 +34,17 @@ export const createShellCommand = () => {
}
}

// check if all nvrams backed by an image are built, pristine ones have none
for (const [label, nvram] of Object.entries(config.nvrams)) {
if (!nvramHasImage(nvram)) {
continue;
}
const pathname = getContextPath(nvramImageFilename(label));
if (!fs.existsSync(pathname)) {
throw new Error(`nvram '${label}' not built, run 'build'`);
}
}

// create shell entrypoint
config.machine.entrypoint = command;

Expand Down
Loading
Loading