From d1924278ca041feaf5caca38c2e940956fe9aa20 Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Wed, 1 Jul 2026 12:31:17 +0100 Subject: [PATCH 1/2] feat(cli): print logs when passing --verbose to the build command. * Improve the --verbose flag to stream logs from the build process. * Mainly affects the docker usage that was silent by default. --- .changeset/vast-kids-read.md | 5 +++ apps/cli/src/builder/directory.ts | 4 ++ apps/cli/src/builder/docker.ts | 43 ++++++++++++++++--- apps/cli/src/builder/tar.ts | 4 ++ apps/cli/src/commands/build.ts | 24 +++++++++-- apps/cli/src/exec/genext2fs.ts | 12 +++--- apps/cli/src/exec/mksquashfs.ts | 6 ++- apps/cli/src/exec/util.ts | 25 +++++++++-- apps/cli/src/types/docker.ts | 24 +++++++++++ .../tests/integration/builder/docker.test.ts | 2 + 10 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 .changeset/vast-kids-read.md diff --git a/.changeset/vast-kids-read.md b/.changeset/vast-kids-read.md new file mode 100644 index 00000000..e11903cd --- /dev/null +++ b/.changeset/vast-kids-read.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Print logs of the build process when passing --verbose to the build command. It also uses the buildx-metadata generated to recover the image-id not relying on the stdout regardless of the value passed to the --progress flag. diff --git a/apps/cli/src/builder/directory.ts b/apps/cli/src/builder/directory.ts index 17b64f3b..55eb5398 100644 --- a/apps/cli/src/builder/directory.ts +++ b/apps/cli/src/builder/directory.ts @@ -2,6 +2,7 @@ import fs from "fs-extra"; import path from "node:path"; import type { DirectoryDriveConfig } from "../config.js"; import { genext2fs, mksquashfs } from "../exec/index.js"; +import type { Reporter } from "../exec/util.js"; export const build = async ( name: string, @@ -9,6 +10,7 @@ export const build = async ( sdkImage: string, destination: string, debug: boolean, + reporter?: Reporter, ): Promise => { const filename = `${name}.${drive.format}`; @@ -26,6 +28,7 @@ export const build = async ( output: filename, cwd: destination, image: sdkImage, + reporter, }); break; } @@ -35,6 +38,7 @@ export const build = async ( output: filename, cwd: destination, image: sdkImage, + reporter, }); break; } diff --git a/apps/cli/src/builder/docker.ts b/apps/cli/src/builder/docker.ts index 3e2192e8..908b3acd 100644 --- a/apps/cli/src/builder/docker.ts +++ b/apps/cli/src/builder/docker.ts @@ -1,13 +1,16 @@ import { execa } from "execa"; import fs from "fs-extra"; import path from "node:path"; +import tmp from "tmp"; import type { DockerDriveConfig } from "../config.js"; import { genext2fs, mksquashfs } from "../exec/index.js"; +import type { Reporter } from "../exec/util.js"; +import type { BuildxMetadata } from "../types/docker.js"; type ImageBuildOptions = Pick< DockerDriveConfig, "buildArgs" | "context" | "dockerfile" | "tags" | "target" -> & { destination: string; dockerfileContent?: string }; +> & { destination: string; dockerfileContent?: string; reporter?: Reporter }; type ImageInfo = { cmd: string[]; @@ -26,6 +29,7 @@ const buildImage = async (options: ImageBuildOptions): Promise => { destination, dockerfile, dockerfileContent, + reporter, tags, target, } = options; @@ -43,7 +47,7 @@ const buildImage = async (options: ImageBuildOptions): Promise => { "--output", `type=tar,dest=${destination}`, "--progress", - "quiet", + reporter ? "plain" : "quiet", ]; // set tags for the image built @@ -52,16 +56,38 @@ const buildImage = async (options: ImageBuildOptions): Promise => { // set build args args.push(...buildArgs.flatMap((arg) => ["--build-arg", arg])); + // use --metadata-file to capture the image ID from json format file, so stdout can be safely + // ignored regardless of what --progress mode outputs there + const tmpFile = tmp.tmpNameSync(); + args.push("--metadata-file", tmpFile); + if (target) { args.push("--target", target); } args.push(context); - const { stdout: imageId } = await execa("docker", args, { - input: dockerfileContent, - }); - return imageId; + if (reporter) + reporter(`Building docker image with args: ${args.join(" ")}`); + + const proc = execa("docker", args, { input: dockerfileContent }); + if (reporter) { + proc.stderr?.on("data", (chunk: Buffer) => { + for (const line of chunk.toString().split("\n")) { + if (line.trim()) reporter(line.trimEnd()); + } + }); + } + await proc; + const metadata = JSON.parse( + fs.readFileSync(tmpFile, "utf-8"), + ) as BuildxMetadata; + + return ( + metadata["containerimage.config.digest"] ?? + metadata["containerimage.digest"] ?? + "" + ); }; /** @@ -101,6 +127,7 @@ export const build = async ( sdkImage: string, destination: string, debug: boolean, + reporter?: Reporter, ): Promise => { const { format } = drive; @@ -116,11 +143,13 @@ export const build = async ( ...drive, destination: path.join(destination, tar), dockerfileContent: `FROM ${drive.image}`, + reporter, }); } else { image = await buildImage({ ...drive, destination: path.join(destination, tar), + reporter, }); } const imageInfo = await getImageInfo(image); @@ -134,6 +163,7 @@ export const build = async ( output: filename, cwd: destination, image: sdkImage, + reporter, }); break; } @@ -143,6 +173,7 @@ export const build = async ( output: filename, cwd: destination, image: sdkImage, + reporter, }); break; } diff --git a/apps/cli/src/builder/tar.ts b/apps/cli/src/builder/tar.ts index 307c7aa2..34633ecc 100644 --- a/apps/cli/src/builder/tar.ts +++ b/apps/cli/src/builder/tar.ts @@ -2,12 +2,14 @@ import fs from "fs-extra"; import path from "node:path"; import type { TarDriveConfig } from "../config.js"; import { genext2fs, mksquashfs } from "../exec/index.js"; +import type { Reporter } from "../exec/util.js"; export const build = async ( name: string, drive: TarDriveConfig, sdkImage: string, destination: string, + reporter?: Reporter, ): Promise => { const tar = `${name}.tar`; const filename = `${name}.${drive.format}`; @@ -23,6 +25,7 @@ export const build = async ( output: filename, cwd: destination, image: sdkImage, + reporter, }); break; } @@ -32,6 +35,7 @@ export const build = async ( output: filename, cwd: destination, image: sdkImage, + reporter, }); break; } diff --git a/apps/cli/src/commands/build.ts b/apps/cli/src/commands/build.ts index 958fa08d..fa6cdde9 100755 --- a/apps/cli/src/commands/build.ts +++ b/apps/cli/src/commands/build.ts @@ -31,9 +31,20 @@ const buildDriveTask = ( task: async (ctx, task) => { const { config, debug, destination } = ctx; const sdk = config.sdk; + const reporter = (line: string) => { + task.output = line; + }; + switch (drive.builder) { case "directory": { - await buildDirectory(name, drive, sdk, destination, debug); + await buildDirectory( + name, + drive, + sdk, + destination, + debug, + reporter, + ); break; } case "docker": { @@ -43,6 +54,7 @@ const buildDriveTask = ( sdk, destination, debug, + reporter, ); if (imageInfo && name === "root") { // only set image info for root drive @@ -55,7 +67,7 @@ const buildDriveTask = ( break; } case "tar": { - await buildTar(name, drive, sdk, destination); + await buildTar(name, drive, sdk, destination, reporter); break; } case "none": { @@ -104,7 +116,13 @@ export const createBuildCommand = () => { await fs.emptyDir(destination); // XXX: make it less error prone // build context - const ctx = { config, debug, destination, imageInfo: undefined }; + const ctx = { + config, + debug, + destination, + verbose, + imageInfo: undefined, + }; // tasks to build drives const driveTasks = Object.entries(config.drives).map( diff --git a/apps/cli/src/exec/genext2fs.ts b/apps/cli/src/exec/genext2fs.ts index 7895e949..99ba584e 100644 --- a/apps/cli/src/exec/genext2fs.ts +++ b/apps/cli/src/exec/genext2fs.ts @@ -20,7 +20,7 @@ export const empty = ( output: string; } & DockerFallbackOptions, ) => { - const { size, output } = options; + const { size, output, reporter } = options; const blocks = Math.ceil(size / BLOCK_SIZE); // size in blocks return execaDockerFallback( "xgenext2fs", @@ -32,7 +32,7 @@ export const empty = ( blocks.toString(), output, ], - options, + { ...options, reporter }, ); }; @@ -44,12 +44,12 @@ export const fromDirectory = ( output: string; } & DockerFallbackOptions, ) => { - const { cwd, extraSize, image, input, output } = options; + const { cwd, extraSize, image, input, output, reporter } = options; const extraBlocks = Math.ceil(extraSize / BLOCK_SIZE); return execaDockerFallback( "xgenext2fs", [...baseArgs({ extraBlocks }), "--root", input, output], - { cwd, image }, + { cwd, image, reporter }, ); }; @@ -61,12 +61,12 @@ export const fromTar = ( output: string; } & DockerFallbackOptions, ) => { - const { cwd, extraSize, image, input, output } = options; + const { cwd, extraSize, image, input, output, reporter } = options; const extraBlocks = Math.ceil(extraSize / BLOCK_SIZE); return execaDockerFallback( "xgenext2fs", [...baseArgs({ extraBlocks }), "--tarball", input, output], - { cwd, image }, + { cwd, image, reporter }, ); }; diff --git a/apps/cli/src/exec/mksquashfs.ts b/apps/cli/src/exec/mksquashfs.ts index fd9bca36..7a0a6f9f 100644 --- a/apps/cli/src/exec/mksquashfs.ts +++ b/apps/cli/src/exec/mksquashfs.ts @@ -24,10 +24,11 @@ export const fromDirectory = ( output: string; } & DockerFallbackOptions, ) => { - const { cwd, image, input, output } = options; + const { cwd, image, input, output, reporter } = options; return execaDockerFallback("mksquashfs", [input, output, ...baseArgs()], { cwd, image, + reporter, }); }; @@ -38,13 +39,14 @@ export const fromTar = ( output: string; } & DockerFallbackOptions, ) => { - const { cwd, image, input, output } = options; + const { cwd, image, input, output, reporter } = options; return execaDockerFallback( "mksquashfs", ["-", output, "-tar", ...baseArgs()], { cwd, image, + reporter, inputFile: input, // use stdin in case of tar file }, ); diff --git a/apps/cli/src/exec/util.ts b/apps/cli/src/exec/util.ts index f2ee451d..0869348f 100644 --- a/apps/cli/src/exec/util.ts +++ b/apps/cli/src/exec/util.ts @@ -1,9 +1,16 @@ import { ExecaError, execa, type Options } from "execa"; import os from "node:os"; +export type Reporter = (line: string) => void; + export type DockerFallbackOptions = - | { image: string; forceDocker: true; tty?: boolean } - | { image?: string; forceDocker?: false; tty?: boolean }; + | { image: string; forceDocker: true; tty?: boolean; reporter?: Reporter } + | { + image?: string; + forceDocker?: false; + tty?: boolean; + reporter?: Reporter; + }; /** * Calls execa and falls back to docker run if command (on the host) fails @@ -13,11 +20,21 @@ export type DockerFallbackOptions = * @returns return of execa */ export type ExecaOptionsDockerFallback = Options & DockerFallbackOptions; + +const pipeReporter = (proc: ReturnType, reporter: Reporter) => { + proc.stderr?.on("data", (chunk: Buffer) => { + for (const line of chunk.toString().split("\n")) { + if (line.trim()) reporter(line.trimEnd()); + } + }); +}; + export const execaDockerFallback = async ( command: string, args: readonly string[], options: ExecaOptionsDockerFallback, ) => { + const { reporter } = options; try { if (options.forceDocker) { const error = new ExecaError(); @@ -41,11 +58,13 @@ export const execaDockerFallback = async ( "--user", `${userInfo.uid}:${userInfo.gid}`, ]; - return await execa( + const proc = execa( "docker", ["run", ...dockerOpts, options.image, command, ...args], options, ); + if (reporter) pipeReporter(proc, reporter); + return await proc; } } throw error; diff --git a/apps/cli/src/types/docker.ts b/apps/cli/src/types/docker.ts index ab275142..0bf2476a 100644 --- a/apps/cli/src/types/docker.ts +++ b/apps/cli/src/types/docker.ts @@ -21,3 +21,27 @@ export type ServicePublisher = { }; export type PsResponse = ServiceStatus; + +/** + * A partial representation of the metadata produced by `docker buildx build --metadata-file`. + * + */ +export type BuildxMetadata = { + /** + * The manifest digest. SHA256 of the manifest JSON, which describes the image's + * layers and points to the config blob. + */ + "containerimage.digest"?: string | null; + + /** + * the image id of the built image. SHA256 of the image config JSON, + * which contains the layer diff IDs, env, entrypoint, cmd, etc. + */ + "containerimage.config.digest"?: string | null; + + /** + * The name/tag of the image if specified (e.g., via `-t` or `name=...`). + * This is explicitly null if no image name/tag was designated for the build. + */ + "image.name"?: string | null; +}; diff --git a/apps/cli/tests/integration/builder/docker.test.ts b/apps/cli/tests/integration/builder/docker.test.ts index 0e4c3cbc..488b59fd 100644 --- a/apps/cli/tests/integration/builder/docker.test.ts +++ b/apps/cli/tests/integration/builder/docker.test.ts @@ -8,6 +8,7 @@ import { } from "bun:test"; import fs from "fs-extra"; import path from "node:path"; +import tmp from "tmp"; import { build } from "../../../src/builder/docker.js"; import type { DockerDriveConfig } from "../../../src/config.js"; import { setupIntegrationTests, TEST_SDK } from "../config.js"; @@ -15,6 +16,7 @@ import { cleanupTempDir, createTempDir } from "./tmpdirTest.js"; beforeAll( async () => { + tmp.setGracefulCleanup(); await setupIntegrationTests(); }, { timeout: 60000 }, From 6c4c34290a6fba5d48626e0f25f1581d25464516 Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Wed, 1 Jul 2026 12:32:09 +0100 Subject: [PATCH 2/2] refactor(cli): Pass verbose flag to build command for integration tests. --- apps/cli/tests/integration/config.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/cli/tests/integration/config.ts b/apps/cli/tests/integration/config.ts index b8fb643e..57ebadce 100644 --- a/apps/cli/tests/integration/config.ts +++ b/apps/cli/tests/integration/config.ts @@ -54,6 +54,8 @@ export async function createTemporaryCartesiApplication(): Promise<{ ); } + const isCI = process.env.CI === "true" || process.env.CI === "1"; + console.log(`✓ Temporary sandbox directory created at: ${tempDir.name}`); console.log(`✓ CLI binary located at: ${cliPath}`); console.log(`✓ Using Docker image: ${TEST_SDK}`); @@ -84,8 +86,10 @@ export async function createTemporaryCartesiApplication(): Promise<{ console.log(`! Building the temporary Cartesi application...`); // Programmatically BUILD the application - await execa("node", [cliPath, "build"], { - stdio: ["ignore", "pipe", "pipe"], + const flags = isCI ? ["--verbose"] : []; + await execa("node", [cliPath, "build", ...flags], { + stdio: "inherit", + reject: true, }); console.log(`✓ Temporary Cartesi application built successfully.`);