Skip to content

Commit

Permalink
feat(cli): tools exec wrappers
Browse files Browse the repository at this point in the history
  • Loading branch information
tuler committed Oct 22, 2024
1 parent 4686b12 commit 6a34195
Show file tree
Hide file tree
Showing 15 changed files with 403 additions and 10 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.build.json",
"./tsconfig.eslint.json",
"./tsconfig.json",
],
tsconfigRootDir: __dirname,
},
};
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,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
35 changes: 35 additions & 0 deletions apps/cli/src/exec/cartesi-machine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { parse, Range, SemVer } from "semver";
import {
DockerFallbackOptions,
execaDockerFallback,
ExecaOptionsDockerFallback,
} from "./util.js";

export const requiredVersion = new Range("^0.18.1");

export const boot = async (
args: readonly string[],
options: ExecaOptionsDockerFallback,
) => {
return execaDockerFallback("cartesi-machine", args, options);
};

export const version = async (
options?: DockerFallbackOptions,
): Promise<SemVer | null> => {
const { image } = options || {};
try {
const { stdout } = await execaDockerFallback(
"cartesi-machine",
["--version-json"],
{ image },
);
if (typeof stdout === "string") {
const output = JSON.parse(stdout);
return parse(output.version);
}
return null;
} catch (e: unknown) {
return null;
}
};
28 changes: 28 additions & 0 deletions apps/cli/src/exec/crane.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { parse, Range, SemVer } from "semver";
import { Stream } from "stream";
import { DockerFallbackOptions, spawnSyncDockerFallback } from "./util.js";

export const requiredVersion = new Range("^0.19.1");

export const exportImage = async (
options: {
stdin: Stream;
stdout: Stream;
} & DockerFallbackOptions,
) => {
const { image, stdin, stdout } = options;
return spawnSyncDockerFallback("crane", ["export", "-", "-"], {
image,
stdio: [stdin, stdout, "inherit"],
});
};

export const version = async (
options?: DockerFallbackOptions,
): Promise<SemVer | null> => {
const result = spawnSyncDockerFallback("crane", ["version"], options || {});
if (result.error) {
return null;
}
return parse(result.stdout.toString());
};
67 changes: 67 additions & 0 deletions apps/cli/src/exec/genext2fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { parse, Range, SemVer } from "semver";
import { DockerFallbackOptions, execaDockerFallback } from "./util.js";

const BLOCK_SIZE = 4096; // fixed at 4k

export const requiredVersion: Range = new Range("^1.5.6");

const baseArgs = (options: { extraBlocks: number }) => [
"--block-size",
BLOCK_SIZE.toString(),
"--faketime",
"--readjustment",
`+${options.extraBlocks}`,
];

export const fromDirectory = async (
options: {
cwd?: string;
extraSize: number;
input: string;
output: string;
} & DockerFallbackOptions,
) => {
const { cwd, extraSize, image, input, output } = options;
const extraBlocks = Math.ceil(extraSize / BLOCK_SIZE);
return execaDockerFallback(
"xgenext2fs",
[...baseArgs({ extraBlocks }), "--root", input, output],
{ cwd, image },
);
};

export const fromTar = async (
options: {
cwd?: string;
extraSize: number;
input: string;
output: string;
} & DockerFallbackOptions,
) => {
const { cwd, extraSize, image, input, output } = options;
const extraBlocks = Math.ceil(extraSize / BLOCK_SIZE);
return execaDockerFallback(
"xgenext2fs",
[...baseArgs({ extraBlocks }), "--tarball", input, output],
{ cwd, image },
);
};

export const version = async (
options?: DockerFallbackOptions,
): Promise<SemVer | null> => {
const { stdout } = await execaDockerFallback(
"xgenext2fs",
["--version"],
options || {},
);
if (typeof stdout === "string") {
const regex =
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?/;
const m = stdout.match(regex);
if (m && m[0]) {
return parse(m[0]);
}
}
return null;
};
4 changes: 4 additions & 0 deletions apps/cli/src/exec/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * as cartesiMachine from "./cartesi-machine.js";
export * as crane from "./crane.js";
export * as genext2fs from "./genext2fs.js";
export * as mksquashfs from "./mksquashfs.js";
73 changes: 73 additions & 0 deletions apps/cli/src/exec/mksquashfs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { parse, Range, SemVer } from "semver";
import { DockerFallbackOptions, execaDockerFallback } from "./util.js";

const COMPRESSION = "lzo"; // make customizable? default is gzip

export const requiredVersion: Range = new Range("^4.5.1");

const baseArgs = () => [
"--all-time",
"0",
"-all-root", // XXX: should we use this?
"-noappend",
"-comp",
COMPRESSION,
"-no-progress",
];

export const fromDirectory = (
options: {
cwd?: string;
input: string;
output: string;
} & DockerFallbackOptions,
) => {
const { cwd, image, input, output } = options;
return execaDockerFallback("mksquashfs", [input, output, ...baseArgs()], {
cwd,
image,
});
};

export const fromTar = (
options: {
cwd?: string;
input: string;
output: string;
} & DockerFallbackOptions,
) => {
const { cwd, image, input, output } = options;
return execaDockerFallback(
"mksquashfs",
["-", output, "-tar", ...baseArgs()],
{
cwd,
image,
inputFile: input, // use stdin in case of tar file
},
);
};

export const version = async (
options?: DockerFallbackOptions,
): Promise<SemVer | null> => {
try {
const { stdout } = await execaDockerFallback(
"mksquashfs",
["-version"],
options || {},
);
if (typeof stdout === "string") {
const regex =
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?/gm;
const m = stdout.match(regex);
if (m && m[0]) {
return parse(m[0]);
}
}
} catch (e: unknown) {
console.error(e);
return null;
}
return null;
};
117 changes: 117 additions & 0 deletions apps/cli/src/exec/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { spawnSync, SpawnSyncOptions } from "child_process";
import { execa, ExecaError, Options } from "execa";
import os from "os";

export type DockerFallbackOptions =
| { image: string; forceDocker: true }
| { image?: string; forceDocker?: false };

/**
* Calls execa and falls back to docker run if command (on the host) fails
* @param command command to be executed
* @param args arguments to be passed to the command
* @param options execution options
* @returns return of execa
*/
export type ExecaOptionsDockerFallback = Options & DockerFallbackOptions;
export const execaDockerFallback = async (
command: string,
args: readonly string[],
options: ExecaOptionsDockerFallback,
) => {
try {
if (options.forceDocker) {
const error = new ExecaError();
error.code = "ENOENT";
throw error;
}
return await execa(command, args, options);
} catch (error) {
if (error instanceof ExecaError) {
if (error.code === "ENOENT" && options.image) {
if (!options.forceDocker) {
console.warn(
`error executing '${command}', falling back to docker execution using image '${options.image}'`,
);
}
const userInfo = os.userInfo();
const dockerOpts = [
"--volume",
`${options.cwd}:/work`,
"--workdir",
"/work",
"--interactive",
"--user",
`${userInfo.uid}:${userInfo.gid}`,
];
return await execa(
"docker",
["run", ...dockerOpts, options.image, command, ...args],
options,
);
} else {
console.error(`error executing '${command}'`, error);
}
}
throw error;
}
};

/**
* Calls spawnSync and falls back to docker run if command (on the host) fails
* @param command command to be executed
* @param args arguments to be passed to the command
* @param options execution options
* @returns return of execa
*/
export type SpawnOptionsDockerFallback = SpawnSyncOptions &
DockerFallbackOptions;
export const spawnSyncDockerFallback = (
command: string,
args: readonly string[],
options: SpawnOptionsDockerFallback,
) => {
const result = options.forceDocker
? { error: { code: "ENOENT" }, stdout: "" }
: spawnSync(command, args, options);
if (result.error) {
const code = (result.error as any).code;
if (code === "ENOENT" && options.image) {
if (!options.forceDocker) {
console.warn(
`error executing '${command}', falling back to docker execution using image '${options.image}'`,
);
}
const userInfo = os.userInfo();
const dockerOpts = [
"--volume",
`${options.cwd}:/work`,
"--workdir",
"/work",
"--interactive",
"--user",
`${userInfo.uid}:${userInfo.gid}`,
];
const dockerArgs = [
"run",
...dockerOpts,
options.image,
command,
...args,
];
const dockerResult = spawnSync("docker", dockerArgs, options);
if (dockerResult.error) {
console.error(
`error executing '${command}'`,
dockerResult.error,
);
throw dockerResult.error;
}
return dockerResult;
} else {
console.error(`error executing '${command}'`, result.error);
throw result.error;
}
}
return result;
};
16 changes: 16 additions & 0 deletions apps/cli/test/exec/cartesi-machine.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { satisfies } from "semver";
import { describe, expect, it } from "vitest";
import { cartesiMachine } from "../../src/exec/index.js";

describe("cartesi-machine", () => {
it("should report version", async () => {
const version = await cartesiMachine.version({
forceDocker: true,
image: "cartesi/sdk:0.11.0",
});
expect(version).toBeDefined();
expect(
satisfies(version!.format(), cartesiMachine.requiredVersion),
).toBeTruthy();
});
});
16 changes: 16 additions & 0 deletions apps/cli/test/exec/crane.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { satisfies } from "semver";
import { describe, expect, it } from "vitest";
import { crane } from "../../src/exec/index.js";

describe("crane", () => {
it("should report version", async () => {
const version = await crane.version({
forceDocker: true,
image: "cartesi/sdk:0.11.0",
});
expect(version).toBeDefined();
expect(
satisfies(version!.format(), crane.requiredVersion),
).toBeTruthy();
});
});
Loading

0 comments on commit 6a34195

Please sign in to comment.