Skip to content

Commit

Permalink
feat(cli): build based on configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
tuler committed Oct 22, 2024
1 parent d0c4bbd commit 5391a5f
Show file tree
Hide file tree
Showing 41 changed files with 1,753 additions and 330 deletions.
5 changes: 5 additions & 0 deletions .changeset/eight-schools-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cartesi/cli": major
---

build based on cartesi.toml
1 change: 1 addition & 0 deletions apps/cli/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ node_modules
oclif.manifest.json
src/contracts.ts
src/graphql/
test/builder/output
1 change: 1 addition & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"ora": "^8.1.0",
"progress-stream": "^2.0.0",
"semver": "^7.6.3",
"smol-toml": "^1.3.0",
"tmp": "^0.2.3",
"viem": "^2.21.27"
},
Expand Down
7 changes: 7 additions & 0 deletions apps/cli/src/baseCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import fs from "fs";
import path from "path";
import { Address, Hash, getAddress, isHash } from "viem";

import { Config, parse } from "./config.js";
import {
applicationFactoryAddress,
authorityFactoryAddress,
Expand Down Expand Up @@ -53,6 +54,12 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
return path.join(".cartesi", ...paths);
}

protected getApplicationConfig(configPath: string): Config {
return fs.existsSync(configPath)
? parse(fs.readFileSync(configPath).toString())
: parse("");
}

protected getMachineHash(): Hash | undefined {
// read hash of the cartesi machine snapshot, if one exists
const hashPath = this.getContextPath("image", "hash");
Expand Down
45 changes: 45 additions & 0 deletions apps/cli/src/builder/directory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import fs from "fs-extra";
import path from "path";
import { DirectoryDriveConfig } from "../config.js";
import { genext2fs, mksquashfs } from "../exec/index.js";

export const build = async (
name: string,
drive: DirectoryDriveConfig,
sdkImage: string,
destination: string,
): Promise<void> => {
const filename = `${name}.${drive.format}`;

// copy directory to destination
const dest = path.join(destination, name);
await fs.mkdirp(dest);
await fs.copy(drive.directory, dest);

try {
switch (drive.format) {
case "ext2": {
await genext2fs.fromDirectory({
extraSize: drive.extraSize,
input: name,
output: filename,
cwd: destination,
image: sdkImage,
});
break;
}
case "sqfs": {
await mksquashfs.fromDirectory({
input: name,
output: filename,
cwd: destination,
image: sdkImage,
});
break;
}
}
} finally {
// delete copied
await fs.remove(dest);
}
};
138 changes: 138 additions & 0 deletions apps/cli/src/builder/docker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { execa } from "execa";
import fs from "fs-extra";
import path from "path";
import tmp from "tmp";
import { DockerDriveConfig } from "../config.js";
import { crane, genext2fs, mksquashfs } from "../exec/index.js";

type ImageBuildOptions = Pick<
DockerDriveConfig,
"context" | "dockerfile" | "tags" | "target"
>;

type ImageInfo = {
cmd: string[];
entrypoint: string[];
env: string[];
workdir: string;
};

/**
* Build Docker image (linux/riscv64). Returns image id.
*/
const buildImage = async (options: ImageBuildOptions): Promise<string> => {
const { context, dockerfile, tags, target } = options;
const buildResult = tmp.tmpNameSync();
const args = [
"buildx",
"build",
"--file",
dockerfile,
"--load",
"--iidfile",
buildResult,
context,
];

// set tags for the image built
args.push(...tags.map((tag) => ["--tag", tag]).flat());

if (target) {
args.push("--target", target);
}

await execa("docker", args, { stdio: "inherit" });
return fs.readFileSync(buildResult, "utf8");
};

/**
* Query the image using docker image inspect
* @param image image id or name
* @returns Information about the image
*/
const getImageInfo = async (image: string): Promise<ImageInfo> => {
const { stdout: jsonStr } = await execa("docker", [
"image",
"inspect",
image,
]);
// parse image info from docker inspect output
const [imageInfo] = JSON.parse(jsonStr);

// validate image architecture (must be riscv64)
if (imageInfo["Architecture"] !== "riscv64") {
throw new Error(
`Invalid image Architecture: ${imageInfo["Architecture"]}. Expected riscv64`,
);
}

const info: ImageInfo = {
cmd: imageInfo["Config"]["Cmd"] ?? [],
entrypoint: imageInfo["Config"]["Entrypoint"] ?? [],
env: imageInfo["Config"]["Env"] || [],
workdir: imageInfo["Config"]["WorkingDir"],
};

return info;
};

export const build = async (
name: string,
drive: DockerDriveConfig,
sdkImage: string,
destination: string,
): Promise<ImageInfo | undefined> => {
const { format } = drive;

const ocitar = `${name}.oci.tar`;
const tar = `${name}.tar`;
const filename = `${name}.${format}`;

// use pre-existing image or build docker image
const image = drive.image || (await buildImage(drive));

// get image info
const imageInfo = await getImageInfo(image);

try {
// create OCI Docker tarball from Docker image
await execa("docker", ["image", "save", image, "-o", ocitar], {
cwd: destination,
});

// create rootfs tar from OCI tar
await crane.exportImage({
stdin: fs.openSync(path.join(destination, ocitar), "r"),
stdout: fs.openSync(path.join(destination, tar), "w"),
image: sdkImage,
});

switch (format) {
case "ext2": {
await genext2fs.fromTar({
extraSize: drive.extraSize,
input: tar,
output: filename,
cwd: destination,
image: sdkImage,
});
break;
}
case "sqfs": {
await mksquashfs.fromTar({
input: path.join(destination, tar),
output: filename,
cwd: destination,
image: sdkImage,
});
break;
}
}
} finally {
// delete intermediate files
await fs.remove(path.join(destination, ocitar));
await fs.remove(path.join(destination, tar));
}

return imageInfo;
};
31 changes: 31 additions & 0 deletions apps/cli/src/builder/empty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import fs from "fs-extra";
import path from "path";
import { EmptyDriveConfig } from "../config.js";
import { genext2fs } from "../exec/index.js";

export const build = async (
name: string,
drive: EmptyDriveConfig,
sdkImage: string,
destination: string,
): Promise<void> => {
const filename = `${name}.${drive.format}`;
switch (drive.format) {
case "ext2": {
await genext2fs.empty({
output: filename,
size: drive.size,
cwd: destination,
image: sdkImage,
});
break;
}
case "raw": {
await fs.writeFile(
path.join(destination, filename),
Buffer.alloc(drive.size),
);
break;
}
}
};
5 changes: 5 additions & 0 deletions apps/cli/src/builder/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,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 buildTar } from "./tar.js";
17 changes: 17 additions & 0 deletions apps/cli/src/builder/none.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import fs from "fs-extra";
import path from "path";
import { ExistingDriveConfig, getDriveFormat } from "../config.js";

export const build = async (
name: string,
drive: ExistingDriveConfig,
destination: string,
): Promise<void> => {
// no need to build, drive already exists
const src = drive.filename;
const format = getDriveFormat(src);
const filename = path.join(destination, `${name}.${format}`);

// just copy it
await fs.copyFile(src, filename);
};
39 changes: 39 additions & 0 deletions apps/cli/src/builder/tar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import fs from "fs-extra";
import path from "path";
import { TarDriveConfig } from "../config.js";
import { genext2fs, mksquashfs } from "../exec/index.js";

export const build = async (
name: string,
drive: TarDriveConfig,
sdkImage: string,
destination: string,
): Promise<void> => {
const tar = `${name}.tar`;
const filename = `${name}.${drive.format}`;

// copy input tar to destination directory (with drive name)
await fs.copy(drive.filename, path.join(destination, tar));

switch (drive.format) {
case "ext2": {
await genext2fs.fromTar({
extraSize: drive.extraSize,
input: tar,
output: filename,
cwd: destination,
image: sdkImage,
});
break;
}
case "sqfs": {
await mksquashfs.fromTar({
input: path.join(destination, tar),
output: filename,
cwd: destination,
image: sdkImage,
});
break;
}
}
};
Loading

0 comments on commit 5391a5f

Please sign in to comment.