Skip to content
Draft
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/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
---

Require cartesi-machine 0.21.0. The `nvrams` configuration depends on its `--nvram` option, so the default SDK image has to be bumped to one shipping 0.21.0, and a host install of previous 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.
12 changes: 12 additions & 0 deletions .github/workflows/cli.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
paths:
- ".github/workflows/cli.yaml"
- "apps/cli/**"
- "packages/sdk/**"
- "packages/tsconfig/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
Expand All @@ -18,6 +19,9 @@ jobs:
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
# TEMPORARY: run the integration tests against the SDK image sdk.yaml
# builds for this PR, instead of the released cartesi/sdk version.
CARTESI_TEST_SDK: ghcr.io/cartesi/sdk:pr-${{ github.event.number }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -41,6 +45,14 @@ jobs:
- name: Build
run: bun run build --filter @cartesi/cli

# TEMPORARY: needed to pull the PR-tagged SDK image referenced by CARTESI_TEST_SDK.
- name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Test
run: bun test apps/cli/

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/sdk.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Fetch Temporary rollups-node debs
run: ./temp/fetch.sh
working-directory: packages/sdk

- name: Build and push
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
if: ${{ !startsWith(github.ref, 'refs/tags/sdk@') }}
Expand Down
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