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 14, 2024
1 parent c7ee447 commit 342d0b5
Show file tree
Hide file tree
Showing 29 changed files with 1,351 additions and 340 deletions.
6 changes: 5 additions & 1 deletion apps/cli/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ module.exports = {
],
parser: "@typescript-eslint/parser",
parserOptions: {
project: ["./tsconfig.eslint.json", "./tsconfig.json"],
project: [
"./tsconfig.eslint.json",
"./tsconfig.json",
"./test/tsconfig.json",
],
tsconfigRootDir: __dirname,
},
};
3 changes: 2 additions & 1 deletion 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.9"
},
Expand Down Expand Up @@ -84,7 +85,7 @@
"clean": "rimraf dist",
"codegen": "run-p codegen:wagmi",
"codegen:wagmi": "wagmi generate",
"compile": "tsc -b",
"compile": "tsc -p tsconfig.build.json",
"copy-files": "copyfiles -u 1 \"src/**/*.yaml\" \"src/**/*.env\" \"src/**/*.txt\" dist",
"lint": "eslint \"src/**/*.ts*\"",
"postpack": "rimraf oclif.manifest.json",
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 {
authorityHistoryPairFactoryAddress,
cartesiDAppFactoryAddress,
Expand Down Expand Up @@ -54,6 +55,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
66 changes: 66 additions & 0 deletions apps/cli/src/builder/directory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import fs from "fs-extra";
import path from "path";
import { DirectoryDriveConfig } from "../config.js";
import { execaDockerFallback } from "../exec.js";

export const build = async (
name: string,
drive: DirectoryDriveConfig,
sdkImage: string,
destination: string,
): Promise<void> => {
const filename = `${name}.${drive.format}`;
const blockSize = 4096; // fixed at 4k
const extraBlocks = Math.ceil(drive.extraSize / blockSize);
const extraSize = `+${extraBlocks}`;

// 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": {
const command = "xgenext2fs";
const args = [
"--block-size",
blockSize.toString(),
"--faketime",
"--root",
name,
"--readjustment",
extraSize,
filename,
];
await execaDockerFallback(command, args, {
cwd: destination,
image: sdkImage,
});
break;
}
case "sqfs": {
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?
"-noappend",
"-comp",
compression,
"-no-progress",
];
await execaDockerFallback(command, args, {
cwd: destination,
image: sdkImage,
});
}
}
} finally {
// delete copied
await fs.remove(dest);
}
};
153 changes: 153 additions & 0 deletions apps/cli/src/builder/docker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { execa } from "execa";
import fs from "fs-extra";
import path from "path";
import tmp from "tmp";
import { DockerDriveConfig } from "../config.js";
import { execaDockerFallback, spawnSyncDockerFallback } from "../exec.js";
import { tarToExt } from "./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 spawnSyncDockerFallback("crane", ["export", "-", "-"], {
stdio: [
fs.openSync(path.join(destination, ocitar), "r"),
fs.openSync(path.join(destination, tar), "w"),
"inherit",
],
image: sdkImage,
});

switch (format) {
case "ext2": {
// create ext2
await tarToExt(tar, filename, format, drive.extraSize, {
cwd: destination,
image: sdkImage,
});
break;
}
case "sqfs": {
const compression = "lzo"; // make customizable? default is gzip
const command = "mksquashfs";
const args = [
"-",
filename,
"-tar",
"-all-time",
"0",
"-all-root", // XXX: should we use this?
"-noappend",
"-comp",
compression,
"-no-progress",
];
await execaDockerFallback(command, args, {
cwd: destination,
image: sdkImage,
inputFile: path.join(destination, tar),
});
break;
}
}
} finally {
// delete intermediate files
// await fs.remove(path.join(destination, ocitar));
// await fs.remove(path.join(destination, tar));
}

return imageInfo;
};
40 changes: 40 additions & 0 deletions apps/cli/src/builder/empty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import fs from "fs-extra";
import path from "path";
import { EmptyDriveConfig } from "../config.js";
import { execaDockerFallback } from "../exec.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": {
const blockSize = 4096; // fixed at 4k
const size = Math.ceil(drive.size / blockSize); // size in blocks
const command = "xgenext2fs";
const args = [
"--block-size",
blockSize.toString(),
"--faketime",
"--size-in-blocks",
size.toString(),
filename,
];
await execaDockerFallback(command, args, {
cwd: destination,
image: sdkImage,
});
break;
}
case "raw": {
await fs.writeFile(
path.join(destination, filename),
Buffer.alloc(drive.size),
);
break;
}
}
};
32 changes: 32 additions & 0 deletions apps/cli/src/builder/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { execaDockerFallback, ExecaOptionsDockerFallback } from "../exec.js";

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";

export const tarToExt = async (
input: string,
output: string,
format: "ext2",
extraSize: number,
options: ExecaOptionsDockerFallback,
) => {
const blockSize = 4096; // fixed at 4k
const extraBlocks = Math.ceil(extraSize / blockSize);
const adjustment = `+${extraBlocks}`;

const command = "xgenext2fs";
const args = [
"--block-size",
blockSize.toString(),
"--faketime",
"--readjustment",
adjustment.toString(),
"--tarball",
input,
output,
];
return execaDockerFallback(command, args, options);
};
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);
};
Loading

0 comments on commit 342d0b5

Please sign in to comment.