From 2173cc9a7de5e4aba7ebe54ec82da088c04a7e0d Mon Sep 17 00:00:00 2001 From: Danilo Tuler Date: Wed, 9 Oct 2024 17:22:56 -0400 Subject: [PATCH] fixup! feat(cli): build based on configuration shell --- apps/cli/src/builder/directory.ts | 5 +- apps/cli/src/builder/docker.ts | 5 +- apps/cli/src/builder/tar.ts | 5 +- apps/cli/src/commands/build.ts | 95 +-------------------------- apps/cli/src/commands/shell.ts | 86 +++++++++++++------------ apps/cli/src/config.ts | 21 ++++++ apps/cli/src/exec.ts | 2 + apps/cli/src/machine.ts | 103 ++++++++++++++++++++++++++++++ apps/cli/test/configs/full.toml | 1 + 9 files changed, 184 insertions(+), 139 deletions(-) create mode 100644 apps/cli/src/machine.ts diff --git a/apps/cli/src/builder/directory.ts b/apps/cli/src/builder/directory.ts index e9618e46..f7bcfaa1 100644 --- a/apps/cli/src/builder/directory.ts +++ b/apps/cli/src/builder/directory.ts @@ -31,6 +31,7 @@ export const build = async ( name, "--readjustment", extraSize, + filename, ]; await execaDockerFallback(command, args, { cwd: destination, @@ -42,6 +43,8 @@ export const build = async ( const compression = "lzo"; // make customizable? default is gzip const command = "mksquashfs"; const args = [ + name, + filename, "-all-time", "0", "-all-root", // XXX: should we use this? @@ -49,8 +52,6 @@ export const build = async ( "-comp", compression, "-no-progress", - name, - filename, ]; await execaDockerFallback(command, args, { cwd: destination, diff --git a/apps/cli/src/builder/docker.ts b/apps/cli/src/builder/docker.ts index e182724b..f145518f 100644 --- a/apps/cli/src/builder/docker.ts +++ b/apps/cli/src/builder/docker.ts @@ -123,6 +123,8 @@ export const build = async ( const compression = "lzo"; // make customizable? default is gzip const command = "mksquashfs"; const args = [ + "-", + filename, "-tar", "-all-time", "0", @@ -131,12 +133,11 @@ export const build = async ( "-comp", compression, "-no-progress", - filename, ]; await execaDockerFallback(command, args, { cwd: destination, image: sdkImage, - inputFile: tar, + inputFile: path.join(destination, tar), }); break; } diff --git a/apps/cli/src/builder/tar.ts b/apps/cli/src/builder/tar.ts index 4fad8e26..5d3d6eee 100644 --- a/apps/cli/src/builder/tar.ts +++ b/apps/cli/src/builder/tar.ts @@ -28,6 +28,8 @@ export const build = async ( const compression = "lzo"; // make customizable? default is gzip const command = "mksquashfs"; const args = [ + "-", + filename, "-tar", "-all-time", "0", @@ -36,12 +38,11 @@ export const build = async ( "-comp", compression, "-no-progress", - filename, ]; await execaDockerFallback(command, args, { cwd: destination, image: sdkImage, - inputFile: tar, + inputFile: path.join(destination, tar), }); break; } diff --git a/apps/cli/src/commands/build.ts b/apps/cli/src/commands/build.ts index 5301f774..62d5b7be 100644 --- a/apps/cli/src/commands/build.ts +++ b/apps/cli/src/commands/build.ts @@ -10,17 +10,8 @@ import { buildNone, buildTar, } from "../builder/index.js"; -import { Config, DriveConfig } from "../config.js"; -import { execaDockerFallback } from "../exec.js"; - -type ImageInfo = { - cmd: string[]; - entrypoint: string[]; - env: string[]; - workdir: string; -}; - -type DriveResult = ImageInfo | undefined | void; +import { DriveConfig, DriveResult } from "../config.js"; +import { bootMachine } from "../machine.js"; const buildDrive = async ( name: string, @@ -47,86 +38,6 @@ const buildDrive = async ( } }; -const bootMachine = async ( - config: Config, - info: ImageInfo | undefined, - sdkImage: string, - destination: string, -) => { - const { machine } = config; - const { assertRollingTemplate, maxMCycle, noRollup, ramLength, ramImage } = - machine; - - // list of environment variables of docker image - const env = info?.env ?? []; - const envs = env.map( - (variable) => `--append-entrypoint=export "${variable}"`, - ); - - // bootargs from config string array - const bootargs = machine.bootargs.map( - (arg) => `--append-bootargs="${arg}"`, - ); - - // entrypoint from config or image info (Docker ENTRYPOINT + CMD) - const entrypoint = - machine.entrypoint ?? // takes priority - (info ? [...info.entrypoint, ...info.cmd].join(" ") : undefined); // ENTRYPOINT and CMD as a space separated string - - if (!entrypoint) { - throw new Error("Undefined machine entrypoint"); - } - - const flashDrives = Object.entries(config.drives).map(([label, drive]) => { - const { format, mount, shared, user } = drive; - // TODO: filename should be absolute dir inside docker container - const filename = `${label}.${format}`; - const vars = [`label:${label}`, `filename:${filename}`]; - if (mount) { - vars.push(`mount:${mount}`); - } - if (user) { - vars.push(`user:${user}`); - } - if (shared) { - vars.push("shared"); - } - // don't specify start and length - return `--flash-drive=${vars.join(",")}`; - }); - - // command to change working directory if WORKDIR is defined - const command = "cartesi-machine"; - const args = [ - ...bootargs, - ...envs, - ...flashDrives, - `--ram-image=${ramImage}`, - `--ram-length=${ramLength}`, - "--final-hash", - "--store=image", - `--append-entrypoint=${entrypoint}`, - ]; - if (info?.workdir) { - args.push(`--append-init=WORKDIR="${info.workdir}"`); - } - if (noRollup) { - args.push("--no-rollup"); - } - if (maxMCycle) { - args.push(`--max-mcycle=${maxMCycle.toString()}`); - } - if (assertRollingTemplate) { - args.push("--assert-rolling-template"); - } - - return execaDockerFallback(command, args, { - cwd: destination, - image: sdkImage, - stdio: "inherit", - }); -}; - export default class Build extends BaseCommand { static summary = "Build application."; @@ -186,7 +97,7 @@ export default class Build extends BaseCommand { const snapshotPath = this.getContextPath("image"); // create machine snapshot - await bootMachine(config, imageInfo, config.sdk, destination); + await bootMachine(config, imageInfo, destination); await fs.chmod(snapshotPath, 0o755); } diff --git a/apps/cli/src/commands/shell.ts b/apps/cli/src/commands/shell.ts index b0423300..65e1abfa 100644 --- a/apps/cli/src/commands/shell.ts +++ b/apps/cli/src/commands/shell.ts @@ -1,9 +1,9 @@ import { Args, Flags } from "@oclif/core"; -import { execa } from "execa"; import fs from "fs-extra"; -import { lookpath } from "lookpath"; import path from "path"; import { BaseCommand } from "../baseCommand.js"; +import { ImageInfo } from "../config.js"; +import { bootMachine } from "../machine.js"; export default class Shell extends BaseCommand { static description = "Start a shell in cartesi machine of application"; @@ -18,62 +18,66 @@ export default class Shell extends BaseCommand { }; static flags = { + config: Flags.file({ + char: "c", + default: "cartesi.toml", + summary: "path to the configuration file", + }), "run-as-root": Flags.boolean({ description: "run as root user", default: false, }), }; - private async startShell( - ext2Path: string, - runAsRoot: boolean, - ): Promise { - const containerDir = "/mnt"; - const bind = `${path.resolve(path.dirname(ext2Path))}:${containerDir}`; - const ext2 = path.join(containerDir, path.basename(ext2Path)); - const ramSize = "128Mi"; - const driveLabel = "root"; - const sdkImage = "cartesi/sdk:0.10.0"; // XXX: how to resolve sdk version? - const args = [ - "run", - "--interactive", - "--tty", - "--volume", - bind, - sdkImage, - "cartesi-machine", - `--ram-length=${ramSize}`, - "--append-bootargs=no4lvl", - `--flash-drive=label:${driveLabel},filename:${ext2}`, - ]; + public async run(): Promise { + const { flags } = await this.parse(Shell); + + // get application configuration from 'cartesi.toml' + const config = this.getApplicationConfig(flags.config); + + // destination directory for image and intermediate files + const destination = path.resolve(this.getContextPath()); - if (runAsRoot) { - args.push("--append-init=USER=root"); + // check if all drives are built + for (const [name, drive] of Object.entries(config.drives)) { + const filename = `${name}.${drive.format}`; + const pathname = this.getContextPath(filename); + if (!fs.existsSync(pathname)) { + throw new Error( + `drive '${name}' not built, run '${this.config.bin} build'`, + ); + } } + // create shell entrypoint + const info: ImageInfo = { + cmd: [], + entrypoint: ["/bin/bash"], + env: [], + workdir: "/", + }; + + // start with interactive mode on + config.machine.interactive = true; + + /* why this? if (!(await lookpath("stty"))) { args.push("-i"); } else { args.push("-it"); } + */ - await execa("docker", [...args, "--", "/bin/bash"], { - stdio: "inherit", - }); - } + // interactive mode can't have final hash + config.machine.finalHash = false; - public async run(): Promise { - const { flags } = await this.parse(Shell); + // do not store machine in interactive mode + config.machine.store = undefined; - // use pre-existing image or build dapp image - const ext2Path = this.getContextPath("root.ext2"); - if (!fs.existsSync(ext2Path)) { - throw new Error( - `machine not built, run '${this.config.bin} build'`, - ); - } + // run as root if flag is set + config.machine.user = flags["run-as-root"] ? "root" : undefined; - // execute the machine and save snapshot - await this.startShell(ext2Path, flags["run-as-root"]); + // boot machine + await bootMachine(config, info, destination); } } diff --git a/apps/cli/src/config.ts b/apps/cli/src/config.ts index 7e5e4393..de972761 100644 --- a/apps/cli/src/config.ts +++ b/apps/cli/src/config.ts @@ -17,6 +17,15 @@ const DEFAULT_SDK = "cartesi/sdk:0.10.0"; type Builder = "directory" | "docker" | "empty" | "none" | "tar"; type DriveFormat = "ext2" | "sqfs"; +export type ImageInfo = { + cmd: string[]; + entrypoint: string[]; + env: string[]; + workdir: string; +}; + +export type DriveResult = ImageInfo | undefined | void; + export type DirectoryDriveConfig = { builder: "directory"; extraSize: number; // default is 0 (no extra size) @@ -69,10 +78,14 @@ 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 ramLength: string; ramImage: string; + store?: string; + user?: string; // default given by cartesi-machine }; export type Config = { @@ -104,10 +117,14 @@ export const defaultMachineConfig = (): MachineConfig => ({ assertRollingTemplate: undefined, bootargs: [], entrypoint: undefined, + finalHash: true, + interactive: undefined, maxMCycle: undefined, noRollup: undefined, ramLength: DEFAULT_RAM, ramImage: defaultRamImage(), + store: "image", + user: undefined, }); export const defaultConfig = (): Config => ({ @@ -275,10 +292,14 @@ const parseMachine = (value: TomlPrimitive): MachineConfig => { toml["assert-rolling-template"], ), bootargs: parseStringArray(toml.bootargs), + finalHash: parseBoolean(toml["final-hash"], true), + 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"], defaultRamImage()), + store: "image", + user: parseOptionalString(toml.user), }; }; diff --git a/apps/cli/src/exec.ts b/apps/cli/src/exec.ts index 8847d43d..ed17be98 100644 --- a/apps/cli/src/exec.ts +++ b/apps/cli/src/exec.ts @@ -29,6 +29,8 @@ export const execaDockerFallback = async ( `${options.cwd}:/work`, "--workdir", "/work", + "--interactive", + "--tty", "--user", `${userInfo.uid}:${userInfo.gid}`, ]; diff --git a/apps/cli/src/machine.ts b/apps/cli/src/machine.ts new file mode 100644 index 00000000..3977d2c3 --- /dev/null +++ b/apps/cli/src/machine.ts @@ -0,0 +1,103 @@ +import { Config, DriveConfig, ImageInfo } from "./config.js"; +import { execaDockerFallback } from "./exec.js"; + +const flashDrive = (label: string, drive: DriveConfig): string => { + const { format, mount, shared, user } = drive; + const filename = `${label}.${format}`; + const vars = [`label:${label}`, `filename:${filename}`]; + if (mount !== undefined) { + vars.push(`mount:${mount}`); + } + if (user) { + vars.push(`user:${user}`); + } + if (shared) { + vars.push("shared"); + } + // don't specify start and length + return `--flash-drive=${vars.join(",")}`; +}; + +export const bootMachine = async ( + config: Config, + info: ImageInfo | undefined, + destination: string, +) => { + const { machine } = config; + const { + assertRollingTemplate, + finalHash, + interactive, + maxMCycle, + noRollup, + ramLength, + ramImage, + store, + user, + } = machine; + + // list of environment variables of docker image + const env = info?.env ?? []; + const envs = env.map( + (variable) => `--append-entrypoint=export "${variable}"`, + ); + + // bootargs from config string array + const bootargs = machine.bootargs.map( + (arg) => `--append-bootargs="${arg}"`, + ); + + // entrypoint from config or image info (Docker ENTRYPOINT + CMD) + const entrypoint = + machine.entrypoint ?? // takes priority + (info ? [...info.entrypoint, ...info.cmd].join(" ") : undefined); // ENTRYPOINT and CMD as a space separated string + + if (!entrypoint) { + throw new Error("Undefined machine entrypoint"); + } + + const flashDrives = Object.entries(config.drives).map(([label, drive]) => + flashDrive(label, drive), + ); + + // command to change working directory if WORKDIR is defined + const command = "cartesi-machine"; + const args = [ + ...bootargs, + ...envs, + ...flashDrives, + `--ram-image=${ramImage}`, + `--ram-length=${ramLength}`, + `--append-entrypoint=${entrypoint}`, + ]; + if (assertRollingTemplate) { + args.push("--assert-rolling-template"); + } + if (finalHash) { + args.push("--final-hash"); + } + if (info?.workdir) { + args.push(`--append-init=WORKDIR="${info.workdir}"`); + } + if (interactive) { + args.push("-it"); + } + if (noRollup) { + args.push("--no-rollup"); + } + if (maxMCycle) { + args.push(`--max-mcycle=${maxMCycle.toString()}`); + } + if (store) { + args.push(`--store=${store}`); + } + if (user) { + args.push(`--append-init=USER=${user}`); + } + + return execaDockerFallback(command, args, { + cwd: destination, + image: config.sdk, + stdio: "inherit", + }); +}; diff --git a/apps/cli/test/configs/full.toml b/apps/cli/test/configs/full.toml index 9c60c3df..18e6a903 100644 --- a/apps/cli/test/configs/full.toml +++ b/apps/cli/test/configs/full.toml @@ -6,6 +6,7 @@ # assert_rolling_update = true # bootargs = ["no4lvl", "quiet", "earlycon=sbi", "console=hvc0", "rootfstype=ext2", "root=/dev/pmem0", "rw", "init=/usr/sbin/cartesi-init"] # entrypoint = "/usr/local/bin/app" +# final-hash = true # max-mcycle = 0 # no-rollup = false # ram-image = "/usr/share/cartesi-machine/images/linux.bin" # directory inside SDK image