Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { installAll } from "./commands/install/install-all.js";
import { search } from "./commands/search.js";
import { add } from "./commands/add.js";
import { cacheSize, cleanCache, show } from "./cache.js";
import { build, DEFAULT_BUILD_OUTPUT_DIR } from "./commands/build.js";
import { test } from "./commands/test/test.js";
import { template } from "./commands/template.js";
import { remove } from "./commands/remove.js";
Expand Down Expand Up @@ -47,6 +48,7 @@ import {
import { format } from "./commands/format.js";
import { docs } from "./commands/docs.js";
import { docsCoverage } from "./commands/docs-coverage.js";
import { resolve } from "node:path";

declare global {
// eslint-disable-next-line no-var
Expand All @@ -57,6 +59,12 @@ declare global {

events.setMaxListeners(20);

// Change working directory for `npm run mops`
let cwd = process.env["MOPS_CWD"];
if (cwd) {
process.chdir(resolve(cwd));
}

let networkFile = getNetworkFile();
if (fs.existsSync(networkFile)) {
globalThis.MOPS_NETWORK = fs.readFileSync(networkFile).toString() || "ic";
Expand Down Expand Up @@ -252,6 +260,32 @@ program
}
});

// build
program
.command("build [canisters...]")
.description("Build a canister")
.addOption(new Option("--verbose", "Verbose console output"))
.addOption(
new Option("--output, -o <output>", "Output directory").default(
DEFAULT_BUILD_OUTPUT_DIR,
),
)
.allowUnknownOption(true) // TODO: restrict unknown before "--"
.action(async (canisters, options, command) => {
checkConfigFile(true);
const extraArgsIndex = command.args.indexOf("--");
await installAll({
silent: true,
lock: "ignore",
installFromLockFile: true,
});
await build(canisters.length ? canisters : undefined, {
...options,
extraArgs:
extraArgsIndex !== -1 ? command.args.slice(extraArgsIndex + 1) : [],
});
});

// test
program
.command("test [filter]")
Expand Down Expand Up @@ -630,7 +664,7 @@ docsCommand
.description("Generate documentation for Motoko code")
.addOption(new Option("--source <source>", "Source directory").default("src"))
.addOption(
new Option("--output <output>", "Output directory").default("docs"),
new Option("--output, -o <output>", "Output directory").default("docs"),
)
.addOption(
new Option("--format <format>", "Output format")
Expand Down
142 changes: 142 additions & 0 deletions cli/commands/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import chalk from "chalk";
import { execa } from "execa";
import { exists } from "fs-extra";
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import { getMocPath } from "../helpers/get-moc-path.js";
import { readConfig } from "../mops.js";
import { CanisterConfig } from "../types.js";
import { sourcesArgs } from "./sources.js";

export interface BuildOptions {
outputDir: string;
verbose: boolean;
extraArgs: string[];
}

export const DEFAULT_BUILD_OUTPUT_DIR = ".mops/.build";

export async function build(
canisterNames: string[] | undefined,
options: Partial<BuildOptions>,
): Promise<void> {
if (canisterNames?.length == 0) {
throw new Error("No canisters specified to build");
}

let outputDir = options.outputDir ?? DEFAULT_BUILD_OUTPUT_DIR;
let mocPath = getMocPath();
let canisters: Record<string, CanisterConfig> = {};
let config = readConfig();
if (config.canisters) {
canisters =
Object.fromEntries(
Object.entries(config.canisters).map(([name, c]) =>
typeof c === "string" ? [name, { main: c }] : [name, c],
),
) ?? {};
}
if (!Object.keys(canisters).length) {
throw new Error(`No Motoko canisters found in mops.toml configuration`);
}

if (canisterNames) {
canisterNames = canisterNames.filter((name) => name in canisters);
if (canisterNames.length === 0) {
throw new Error("No valid canister names specified");
}
for (let name of canisterNames) {
if (!(name in canisters)) {
throw new Error(
`Motoko canister '${name}' not found in mops.toml configuration`,
);
}
}
}

if (!(await exists(outputDir))) {
await mkdir(outputDir, { recursive: true });
}
for (let [canisterName, canister] of Object.entries(canisters)) {
options.verbose && console.time(`build canister ${canisterName}`);
console.log(chalk.blue("build canister"), chalk.bold(canisterName));
let motokoPath = canister.main;
if (!motokoPath) {
throw new Error(`No main file is specified for canister ${canisterName}`);
}
let args = [
"-c",
"--idl",
"-o",
join(outputDir, `${canisterName}.wasm`),
motokoPath,
...(options.extraArgs ?? []),
...(await sourcesArgs()).flat(),
];
if (config.build?.args) {
if (typeof config.build.args === "string") {
throw new Error(
`[build] config 'args' should be an array of strings in mops.toml config file`,
);
}
args.push(...config.build.args);
}
if (canister.args) {
if (typeof canister.args === "string") {
throw new Error(
`Canister config 'args' should be an array of strings for canister ${canisterName}`,
);
}
args.push(...canister.args);
}
try {
if (options.verbose) {
console.log(chalk.gray(mocPath, JSON.stringify(args)));
}
const result = await execa(mocPath, args, {
stdio: options.verbose ? "inherit" : "pipe",
reject: false,
});

if (result.exitCode !== 0) {
console.error(
chalk.red(`Error: Failed to build canister ${canisterName}`),
);
if (!options.verbose) {
if (result.stderr) {
console.error(chalk.red(result.stderr));
}
if (result.stdout?.trim()) {
console.error(chalk.yellow("Build output:"));
console.error(result.stdout);
}
}
// throw new Error(
// `Build failed for canister ${canisterName} (exit code: ${result.exitCode})`,
// );
process.exit(1);
}

if (options.verbose && result.stdout && result.stdout.trim()) {
console.log(result.stdout);
}
} catch (error: any) {
if (error.message?.includes("Build failed for canister")) {
throw error;
}
console.error(
chalk.red(`Error while compiling canister ${canisterName}`),
);
if (error.message) {
console.error(chalk.red(`Details: ${error.message}`));
}

throw new Error(`Build execution failed for canister ${canisterName}`);
}
options.verbose && console.timeEnd(`build canister ${canisterName}`);
}

console.log(
chalk.green(`\n✓ Built ${Object.keys(canisters).length} canisters`),
);
}
13 changes: 10 additions & 3 deletions cli/commands/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import {
} from "../mops.js";
import { resolvePackages } from "../resolve-packages.js";

export async function sources({
export async function sourcesArgs({
conflicts = "ignore" as "warning" | "error" | "ignore",
cwd = process.cwd(),
} = {}) {
} = {}): Promise<string[][]> {
if (!checkConfigFile()) {
return [];
}
Expand Down Expand Up @@ -53,7 +53,14 @@ export async function sources({
pkgBaseDir = pkgDir;
}

return `--package ${name} ${pkgBaseDir}`;
return ["--package", name, pkgBaseDir];
})
.filter((x) => x != null);
}

export async function sources({
conflicts = "ignore" as "warning" | "error" | "ignore",
cwd = process.cwd(),
} = {}): Promise<string[]> {
return (await sourcesArgs({ conflicts, cwd })).map((args) => args.join(" "));
}
1 change: 0 additions & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"node": ">=18.0.0"
},
"scripts": {
"mops": "tsx ./cli.ts",
"build": "npm run prepare && npm run bundle",
"dist": "tsc",
"bundle": "rm -rf ./bundle && bun build ./cli.ts --outdir ./bundle --target node --minify --external @napi-rs/lzma --external fsevents --format esm --define '__dirname=import.meta.dirname' && npm run bundle:fix && npm run bundle:copy && npm run bundle:package-json && npm run bundle:tar",
Expand Down
10 changes: 10 additions & 0 deletions cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@ export type Config = {
"dev-dependencies"?: Dependencies;
toolchain?: Toolchain;
requirements?: Requirements;
canisters?: Record<string, string | CanisterConfig>;
build?: {
outputDir?: string;
args?: string[];
};
// format ?: Format;
};

export type CanisterConfig = {
main: string;
args?: string[];
};

export type Dependencies = Record<string, Dependency>;

export type Dependency = {
Expand Down
Loading
Loading