ic-mops 2.15.2 → 2.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/bundle/cli.tgz +0 -0
  3. package/check-requirements.ts +53 -31
  4. package/cli.ts +18 -3
  5. package/commands/bench.ts +46 -11
  6. package/commands/build.ts +3 -2
  7. package/commands/check-stable.ts +5 -3
  8. package/commands/check.ts +5 -3
  9. package/commands/generate.ts +3 -2
  10. package/commands/test/test.ts +12 -4
  11. package/commands/test/utils.ts +6 -2
  12. package/commands/toolchain/index.ts +1 -3
  13. package/dist/check-requirements.js +31 -24
  14. package/dist/cli.js +5 -3
  15. package/dist/commands/bench.d.ts +2 -0
  16. package/dist/commands/bench.js +29 -6
  17. package/dist/commands/build.js +2 -2
  18. package/dist/commands/check-stable.js +3 -3
  19. package/dist/commands/check.js +3 -3
  20. package/dist/commands/generate.js +2 -2
  21. package/dist/commands/test/test.js +11 -4
  22. package/dist/commands/test/utils.d.ts +1 -1
  23. package/dist/commands/test/utils.js +2 -2
  24. package/dist/commands/toolchain/index.js +1 -3
  25. package/dist/error.d.ts +1 -0
  26. package/dist/error.js +4 -0
  27. package/dist/helpers/get-lintoko-version.d.ts +3 -0
  28. package/dist/helpers/get-lintoko-version.js +30 -0
  29. package/dist/mops.js +2 -2
  30. package/dist/package.json +1 -1
  31. package/dist/tests/bench.test.d.ts +1 -0
  32. package/dist/tests/bench.test.js +21 -0
  33. package/dist/tests/requirements.test.d.ts +1 -0
  34. package/dist/tests/requirements.test.js +46 -0
  35. package/dist/types.d.ts +1 -0
  36. package/error.ts +5 -0
  37. package/helpers/get-lintoko-version.ts +32 -0
  38. package/mops.ts +2 -2
  39. package/package.json +1 -1
  40. package/tests/bench/bench/sanity.bench.mo +27 -0
  41. package/tests/bench/mops.toml +6 -0
  42. package/tests/bench.test.ts +22 -0
  43. package/tests/lint-extra-example-rules/lint/migration-self-contained/migration-self-contained.toml +1 -1
  44. package/tests/lint-extra-example-rules/mops.toml +1 -1
  45. package/tests/requirements-lintoko/mops.toml +5 -0
  46. package/tests/requirements.test.ts +66 -0
  47. package/types.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Next
4
4
 
5
+ ## 2.16.1
6
+
7
+ - Fix `mops bench` crashing on moc 0.15+ with the default `--gc copying`: `--copying-gc` is rejected under enhanced orthogonal persistence, which became the default persistence mode in 2.16.0. The default GC is now `incremental` (moc's default and the only collector available under EOP). Selecting a legacy collector (`copying`, `compacting`, `generational`) now implies `--legacy-persistence`, since moc only accepts them there.
8
+ - `mops build`, `mops check`, `mops generate`, and `mops check-stable` now tunnel `moc`'s exit code instead of always exiting `1`. moc exits `2` on a compiler crash (uncaught internal error) versus `1` for normal user errors (type/compile errors, stable-compat mismatch, bad args), so CI and scripts can now distinguish "file a Motoko bug" from "fix your code" via `$?`. `mops test` likewise exits `2` when a `moc` process crashes during a test run, instead of reporting it as an ordinary test failure.
9
+
10
+ ## 2.16.0
11
+
12
+ - `mops bench` now compiles benchmark canisters under **enhanced orthogonal persistence** (moc's default) instead of forcing `--legacy-persistence` — measuring the persistence mode real canisters run. Pass `--legacy-persistence` to opt back into legacy persistence.
13
+ - `mops bench --query` measures each cell in a **query** call instead of an update call. Queries run no GC on the IC, so the instruction counts exclude GC work an update would incur — for benchmarking `query`/read-only workloads realistically. Only for synchronous benchmark runners (no inter-canister `await`).
14
+ - `[requirements].lintoko` declares a minimum lintoko version for package consumers. `mops install` (and `mops add`, `mops toolchain use`) warn when the project's lintoko is below a dependency's requirement, same as `moc` (#597).
15
+
5
16
  ## 2.15.2
6
17
 
7
18
  - `mops check-stable` (and the stable check inside `mops check`) reports when `[canisters.<name>.migrations].check-limit` is set but more migrations are pending than the limit allows. If the compatibility check failed, the check-limit diagnostic replaces the misleading `moc` error; if it passed anyway, a warning is shown. Compares the deployed `.most` baseline against the local chain; use `--no-check-limit` to suppress.
package/bundle/cli.tgz CHANGED
Binary file
@@ -5,47 +5,69 @@ import chalk from "chalk";
5
5
  import { getDependencyType, getRootDir, readConfig } from "./mops.js";
6
6
  import { resolvePackages } from "./resolve-packages.js";
7
7
  import { getMocSemVer } from "./helpers/get-moc-version.js";
8
+ import { getLintokoSemVer } from "./helpers/get-lintoko-version.js";
8
9
  import { getPackageId } from "./helpers/get-package-id.js";
10
+ import type { Requirements } from "./types.js";
11
+
12
+ type ToolRequirement = keyof Requirements;
13
+
14
+ const TOOL_REQUIREMENTS: {
15
+ tool: ToolRequirement;
16
+ getInstalled: () => SemVer | null;
17
+ }[] = [
18
+ { tool: "moc", getInstalled: getMocSemVer },
19
+ { tool: "lintoko", getInstalled: getLintokoSemVer },
20
+ ];
9
21
 
10
22
  export async function checkRequirements({ verbose = false } = {}) {
11
- let installedMoc = getMocSemVer();
12
- if (!installedMoc) {
13
- return;
14
- }
15
- let highestRequiredMoc = new SemVer("0.0.0");
16
- let highestRequiredMocPkgId = "";
17
23
  let rootDir = getRootDir();
18
-
19
24
  let resolvedPackages = await resolvePackages();
20
- for (let [name, version] of Object.entries(resolvedPackages)) {
21
- if (getDependencyType(version) === "mops") {
22
- let pkgId = getPackageId(name, version);
23
- let depConfig = readConfig(
24
- path.join(rootDir, ".mops", pkgId, "mops.toml"),
25
- );
26
- let moc = depConfig.requirements?.moc;
27
-
28
- if (moc) {
29
- let requiredMoc = new SemVer(moc);
30
- if (highestRequiredMoc.compare(requiredMoc) < 0) {
31
- highestRequiredMoc = requiredMoc;
32
- highestRequiredMocPkgId = pkgId;
25
+
26
+ for (let { tool, getInstalled } of TOOL_REQUIREMENTS) {
27
+ let installed = getInstalled();
28
+ if (!installed) {
29
+ continue;
30
+ }
31
+
32
+ let highestRequired = new SemVer("0.0.0");
33
+ let highestRequiredPkgId = "";
34
+
35
+ for (let [name, version] of Object.entries(resolvedPackages)) {
36
+ if (getDependencyType(version) === "mops") {
37
+ let pkgId = getPackageId(name, version);
38
+ let depConfig = readConfig(
39
+ path.join(rootDir, ".mops", pkgId, "mops.toml"),
40
+ );
41
+ let required = depConfig.requirements?.[tool];
42
+
43
+ if (required) {
44
+ let requiredVersion = new SemVer(required);
45
+ if (highestRequired.compare(requiredVersion) < 0) {
46
+ highestRequired = requiredVersion;
47
+ highestRequiredPkgId = pkgId;
48
+ }
49
+ verbose && _check(tool, pkgId, installed, requiredVersion);
33
50
  }
34
- verbose && _check(pkgId, installedMoc, requiredMoc);
35
51
  }
36
52
  }
37
- }
38
53
 
39
- verbose || _check(highestRequiredMocPkgId, installedMoc, highestRequiredMoc);
54
+ verbose || _check(tool, highestRequiredPkgId, installed, highestRequired);
55
+ }
40
56
  }
41
57
 
42
- function _check(pkgId: string, installedMoc: SemVer, requiredMoc: SemVer) {
43
- let comp = installedMoc.compare(requiredMoc);
44
- if (comp < 0) {
45
- console.log(
46
- chalk.yellow(`moc version does not meet the requirements of ${pkgId}`),
47
- );
48
- console.log(chalk.yellow(` Required: >= ${requiredMoc.format()}`));
49
- console.log(chalk.yellow(` Installed: ${installedMoc.format()}`));
58
+ function _check(
59
+ tool: ToolRequirement,
60
+ pkgId: string,
61
+ installed: SemVer,
62
+ required: SemVer,
63
+ ) {
64
+ if (!pkgId || installed.compare(required) >= 0) {
65
+ return;
50
66
  }
67
+
68
+ console.log(
69
+ chalk.yellow(`${tool} version does not meet the requirements of ${pkgId}`),
70
+ );
71
+ console.log(chalk.yellow(` Required: >= ${required.format()}`));
72
+ console.log(chalk.yellow(` Installed: ${installed.format()}`));
51
73
  }
package/cli.ts CHANGED
@@ -544,9 +544,12 @@ program
544
544
  ).choices(["dfx", "pocket-ic"]),
545
545
  )
546
546
  .addOption(
547
- new Option("--gc <gc>", "Garbage collector")
547
+ new Option(
548
+ "--gc <gc>",
549
+ "Garbage collector. Under enhanced orthogonal persistence (the default) the GC is fixed to `incremental`; selecting `copying`, `compacting`, or `generational` implies `--legacy-persistence`",
550
+ )
548
551
  .choices(["copying", "compacting", "generational", "incremental"])
549
- .default("copying"),
552
+ .default("incremental"),
550
553
  )
551
554
  .addOption(
552
555
  new Option("--save", "Save benchmark results to .bench/<filename>.json"),
@@ -558,10 +561,22 @@ program
558
561
  ),
559
562
  )
560
563
  // .addOption(new Option('--force-gc', 'Force GC'))
564
+ .addOption(
565
+ new Option(
566
+ "--query",
567
+ "Measure each cell in a query call (how `query` methods run on the IC: no GC). Only for benchmarks whose runner is synchronous (no inter-canister calls)",
568
+ ),
569
+ )
570
+ .addOption(
571
+ new Option(
572
+ "--legacy-persistence",
573
+ "Compile benchmark canisters under legacy persistence instead of enhanced orthogonal persistence (the default)",
574
+ ),
575
+ )
561
576
  .addOption(
562
577
  new Option(
563
578
  "--verbose",
564
- "Print the benchmark pipeline (compiler, replica, GC, optimization) and stream compiler/replica output, including dfx optimization warnings",
579
+ "Print the benchmark pipeline (compiler, replica, GC, context, persistence, profile, optimization) and stream compiler/replica output, including dfx optimization warnings",
565
580
  ),
566
581
  )
567
582
  .action(async (filter, options) => {
package/commands/bench.ts CHANGED
@@ -40,6 +40,8 @@ type BenchOptions = {
40
40
  compilerVersion: string;
41
41
  gc: "copying" | "compacting" | "generational" | "incremental";
42
42
  forceGc: boolean;
43
+ query: boolean;
44
+ legacyPersistence: boolean;
43
45
  save: boolean;
44
46
  compare: boolean;
45
47
  verbose: boolean;
@@ -59,8 +61,10 @@ export async function bench(
59
61
  replicaVersion: "",
60
62
  compiler: "moc",
61
63
  compilerVersion: getMocVersion(true),
62
- gc: "copying",
64
+ gc: "incremental",
63
65
  forceGc: true,
66
+ query: false,
67
+ legacyPersistence: false,
64
68
  save: false,
65
69
  compare: false,
66
70
  verbose: false,
@@ -108,6 +112,8 @@ export async function bench(
108
112
  replicaType === "dfx" || replicaType === "dfx-pocket-ic"
109
113
  ? 'dfx `optimize: "cycles"` (ic-wasm) on deploy'
110
114
  : "none (raw moc output)";
115
+ let legacyGc = isLegacyGc(options.gc);
116
+ let effectiveLegacyPersistence = options.legacyPersistence || legacyGc;
111
117
  console.log(chalk.gray("Benchmark pipeline:"));
112
118
  console.log(chalk.gray(` compiler: moc ${options.compilerVersion}`));
113
119
  console.log(
@@ -120,6 +126,21 @@ export async function bench(
120
126
  ` gc: ${options.gc}${options.forceGc ? " (forced)" : ""}`,
121
127
  ),
122
128
  );
129
+ console.log(
130
+ chalk.gray(` context: ${options.query ? "query" : "update"}`),
131
+ );
132
+ console.log(
133
+ chalk.gray(
134
+ ` persistence: ${effectiveLegacyPersistence ? "legacy" : "enhanced"}`,
135
+ ),
136
+ );
137
+ if (legacyGc && !options.legacyPersistence) {
138
+ console.log(
139
+ chalk.gray(
140
+ ` (gc '${options.gc}' only exists under legacy persistence; enabling --legacy-persistence)`,
141
+ ),
142
+ );
143
+ }
123
144
  console.log(chalk.gray(` profile: ${options.profile}`));
124
145
  console.log(chalk.gray(` optimize: ${optimize}`));
125
146
  }
@@ -267,13 +288,25 @@ function computeDiffAll(
267
288
  return diff;
268
289
  }
269
290
 
291
+ // Collectors that only exist under legacy persistence. moc 0.15+ fixes the GC to
292
+ // incremental under enhanced orthogonal persistence and rejects these there.
293
+ function isLegacyGc(gc: BenchOptions["gc"]): boolean {
294
+ return gc === "copying" || gc === "compacting" || gc === "generational";
295
+ }
296
+
270
297
  function getMocArgs(options: BenchOptions): string {
271
298
  let args = "";
272
299
 
273
- if (
274
- options.compilerVersion &&
275
- new SemVer(options.compilerVersion).compare("0.15.0") >= 0
276
- ) {
300
+ let mocAtLeast015 =
301
+ !!options.compilerVersion &&
302
+ new SemVer(options.compilerVersion).compare("0.15.0") >= 0;
303
+
304
+ // Legacy collectors require legacy persistence; moc < 0.15 is already legacy
305
+ // and has no --legacy-persistence flag.
306
+ let useLegacyPersistence =
307
+ options.legacyPersistence || isLegacyGc(options.gc);
308
+
309
+ if (useLegacyPersistence && mocAtLeast015) {
277
310
  args += " --legacy-persistence";
278
311
  }
279
312
 
@@ -281,7 +314,8 @@ function getMocArgs(options: BenchOptions): string {
281
314
  args += " --force-gc";
282
315
  }
283
316
 
284
- if (options.gc) {
317
+ // Under EOP the GC is fixed; only pass a collector flag where it's selectable.
318
+ if (options.gc && (useLegacyPersistence || !mocAtLeast015)) {
285
319
  args += ` --${options.gc}-gc`;
286
320
  }
287
321
 
@@ -533,13 +567,14 @@ async function runBenchFile(
533
567
  log();
534
568
  }
535
569
 
536
- // run all cells
570
+ // run all cells. `--query` measures in query context (how `query` methods
571
+ // actually run on the IC: no GC), at the cost of not supporting benches whose
572
+ // runner performs async calls. Otherwise use the update path.
537
573
  for (let [rowIndex, row] of schema.rows.entries()) {
538
574
  for (let [colIndex, col] of schema.cols.entries()) {
539
- let res = await actor.runCellUpdateAwait(
540
- BigInt(rowIndex),
541
- BigInt(colIndex),
542
- );
575
+ let res = options.query
576
+ ? await actor.runCellQuery(BigInt(rowIndex), BigInt(colIndex))
577
+ : await actor.runCellUpdateAwait(BigInt(rowIndex), BigInt(colIndex));
543
578
  results.set(`${row}:${col}`, res);
544
579
 
545
580
  // @ts-ignore
package/commands/build.ts CHANGED
@@ -4,7 +4,7 @@ import { exists } from "fs-extra";
4
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { join } from "node:path";
6
6
  import { lock, unlockSync } from "proper-lockfile";
7
- import { cliError } from "../error.js";
7
+ import { cliError, cliExit } from "../error.js";
8
8
  import { isCandidCompatible } from "../helpers/is-candid-compatible.js";
9
9
  import {
10
10
  filterCanisters,
@@ -136,7 +136,8 @@ export async function build(
136
136
  console.error(result.stdout);
137
137
  }
138
138
  }
139
- cliError(
139
+ cliExit(
140
+ result.exitCode ?? 1,
140
141
  `Build failed for canister ${canisterName} (exit code: ${result.exitCode})`,
141
142
  );
142
143
  }
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, mkdtempSync } from "node:fs";
3
3
  import { rm } from "node:fs/promises";
4
4
  import chalk from "chalk";
5
5
  import { execa } from "execa";
6
- import { cliError } from "../error.js";
6
+ import { cliError, cliExit } from "../error.js";
7
7
  import {
8
8
  getCheckLimitPendingIssue,
9
9
  prepareMigrationArgs,
@@ -261,7 +261,8 @@ export async function runStableCheck(
261
261
  if (result.stderr) {
262
262
  console.error(result.stderr);
263
263
  }
264
- cliError(
264
+ cliExit(
265
+ result.exitCode ?? 1,
265
266
  `✗ Stable compatibility check failed for canister '${canisterName}'`,
266
267
  );
267
268
  }
@@ -314,7 +315,8 @@ async function generateStableTypes(
314
315
  if (result.stderr) {
315
316
  console.error(result.stderr);
316
317
  }
317
- cliError(
318
+ cliExit(
319
+ result.exitCode ?? 1,
318
320
  `Failed to generate stable types for ${moFile} (exit code: ${result.exitCode})`,
319
321
  );
320
322
  }
package/commands/check.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import chalk from "chalk";
3
3
  import { execa } from "execa";
4
- import { cliError } from "../error.js";
4
+ import { cliError, cliExit } from "../error.js";
5
5
  import {
6
6
  getGlobalMocArgs,
7
7
  getRootDir,
@@ -208,7 +208,8 @@ async function checkCanisters(
208
208
  });
209
209
 
210
210
  if (result.exitCode !== 0) {
211
- cliError(
211
+ cliExit(
212
+ result.exitCode ?? 1,
212
213
  `✗ Check failed for canister ${canisterName} (exit code: ${result.exitCode})`,
213
214
  );
214
215
  }
@@ -285,7 +286,8 @@ async function checkFiles(
285
286
  });
286
287
 
287
288
  if (result.exitCode !== 0) {
288
- cliError(
289
+ cliExit(
290
+ result.exitCode ?? 1,
289
291
  `✗ Check failed for file ${file} (exit code: ${result.exitCode})`,
290
292
  );
291
293
  }
@@ -2,7 +2,7 @@ import chalk from "chalk";
2
2
  import { execa } from "execa";
3
3
  import { mkdir } from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { cliError } from "../error.js";
5
+ import { cliError, cliExit } from "../error.js";
6
6
  import {
7
7
  filterCanisters,
8
8
  resolveCanisterConfigs,
@@ -104,7 +104,8 @@ export async function generateCandid(
104
104
  console.error(result.stdout);
105
105
  }
106
106
  }
107
- cliError(
107
+ cliExit(
108
+ result.exitCode ?? 1,
108
109
  `Failed to generate Candid for canister ${canisterName} (exit code: ${result.exitCode})`,
109
110
  );
110
111
  }
@@ -159,7 +159,7 @@ export async function test(filter = "", options: Partial<TestOptions> = {}) {
159
159
  explicitReplica,
160
160
  );
161
161
  if (!passed) {
162
- process.exit(1);
162
+ process.exit(maxMocExit >= 2 ? 2 : 1);
163
163
  }
164
164
  }
165
165
  }
@@ -167,6 +167,13 @@ export async function test(filter = "", options: Partial<TestOptions> = {}) {
167
167
  let mocPath = "";
168
168
  let wasmtimePath = "";
169
169
 
170
+ let maxMocExit = 0;
171
+ const trackMocExit = (code: number | null) => {
172
+ if (code && code >= 2) {
173
+ maxMocExit = Math.max(maxMocExit, code);
174
+ }
175
+ };
176
+
170
177
  async function runAll(
171
178
  reporterName: ReporterName | undefined,
172
179
  filter = "",
@@ -197,6 +204,7 @@ export async function testWithReporter(
197
204
  signal?: AbortSignal,
198
205
  explicitReplica = false,
199
206
  ): Promise<boolean> {
207
+ maxMocExit = 0;
200
208
  let rootDir = getRootDir();
201
209
  let files: string[] = [];
202
210
  let libFiles = globSync("**/test?(s)/lib.mo", MOTOKO_GLOB_CONFIG);
@@ -328,7 +336,7 @@ export async function testWithReporter(
328
336
  }
329
337
  throw error;
330
338
  });
331
- pipeMMF(proc, mmf).then(resolve);
339
+ pipeMMF(proc, mmf, trackMocExit).then(resolve);
332
340
  }
333
341
  // build and run wasm
334
342
  else if (mode === "wasi") {
@@ -346,7 +354,7 @@ export async function testWithReporter(
346
354
  }
347
355
  throw error;
348
356
  });
349
- pipeMMF(buildProc, mmf)
357
+ pipeMMF(buildProc, mmf, trackMocExit)
350
358
  .then(async () => {
351
359
  if (mmf.failed > 0) {
352
360
  return;
@@ -413,7 +421,7 @@ export async function testWithReporter(
413
421
  throw error;
414
422
  });
415
423
 
416
- pipeMMF(buildProc, mmf)
424
+ pipeMMF(buildProc, mmf, trackMocExit)
417
425
  .then(async () => {
418
426
  if (mmf.failed > 0) {
419
427
  return;
@@ -68,18 +68,22 @@ export function pipeStderrToMMF(stderr: Readable, mmf: MMF1, dir = "") {
68
68
  });
69
69
  }
70
70
 
71
- export function pipeMMF(proc: ChildProcessWithoutNullStreams, mmf: MMF1) {
71
+ export function pipeMMF(
72
+ proc: ChildProcessWithoutNullStreams,
73
+ mmf: MMF1,
74
+ onClose?: (code: number | null) => void,
75
+ ) {
72
76
  return new Promise<void>((resolve) => {
73
77
  pipeStdoutToMMF(proc.stdout, mmf);
74
78
  pipeStderrToMMF(proc.stderr, mmf);
75
79
 
76
- // exit
77
80
  proc.on("close", (code) => {
78
81
  if (code === 0) {
79
82
  mmf.strategy !== "print" && mmf.pass();
80
83
  } else if (code !== 1) {
81
84
  mmf.fail(`unknown exit code: ${code}`);
82
85
  }
86
+ onClose?.(code ?? null);
83
87
  resolve();
84
88
  });
85
89
  });
@@ -337,9 +337,7 @@ async function update(tool?: Tool) {
337
337
  config.toolchain[tool] = version;
338
338
  writeConfig(config);
339
339
 
340
- if (tool === "moc") {
341
- await checkRequirements();
342
- }
340
+ await checkRequirements();
343
341
 
344
342
  if (oldVersion === version) {
345
343
  console.log(`Latest ${tool} ${version} is already installed`);
@@ -4,38 +4,45 @@ import chalk from "chalk";
4
4
  import { getDependencyType, getRootDir, readConfig } from "./mops.js";
5
5
  import { resolvePackages } from "./resolve-packages.js";
6
6
  import { getMocSemVer } from "./helpers/get-moc-version.js";
7
+ import { getLintokoSemVer } from "./helpers/get-lintoko-version.js";
7
8
  import { getPackageId } from "./helpers/get-package-id.js";
9
+ const TOOL_REQUIREMENTS = [
10
+ { tool: "moc", getInstalled: getMocSemVer },
11
+ { tool: "lintoko", getInstalled: getLintokoSemVer },
12
+ ];
8
13
  export async function checkRequirements({ verbose = false } = {}) {
9
- let installedMoc = getMocSemVer();
10
- if (!installedMoc) {
11
- return;
12
- }
13
- let highestRequiredMoc = new SemVer("0.0.0");
14
- let highestRequiredMocPkgId = "";
15
14
  let rootDir = getRootDir();
16
15
  let resolvedPackages = await resolvePackages();
17
- for (let [name, version] of Object.entries(resolvedPackages)) {
18
- if (getDependencyType(version) === "mops") {
19
- let pkgId = getPackageId(name, version);
20
- let depConfig = readConfig(path.join(rootDir, ".mops", pkgId, "mops.toml"));
21
- let moc = depConfig.requirements?.moc;
22
- if (moc) {
23
- let requiredMoc = new SemVer(moc);
24
- if (highestRequiredMoc.compare(requiredMoc) < 0) {
25
- highestRequiredMoc = requiredMoc;
26
- highestRequiredMocPkgId = pkgId;
16
+ for (let { tool, getInstalled } of TOOL_REQUIREMENTS) {
17
+ let installed = getInstalled();
18
+ if (!installed) {
19
+ continue;
20
+ }
21
+ let highestRequired = new SemVer("0.0.0");
22
+ let highestRequiredPkgId = "";
23
+ for (let [name, version] of Object.entries(resolvedPackages)) {
24
+ if (getDependencyType(version) === "mops") {
25
+ let pkgId = getPackageId(name, version);
26
+ let depConfig = readConfig(path.join(rootDir, ".mops", pkgId, "mops.toml"));
27
+ let required = depConfig.requirements?.[tool];
28
+ if (required) {
29
+ let requiredVersion = new SemVer(required);
30
+ if (highestRequired.compare(requiredVersion) < 0) {
31
+ highestRequired = requiredVersion;
32
+ highestRequiredPkgId = pkgId;
33
+ }
34
+ verbose && _check(tool, pkgId, installed, requiredVersion);
27
35
  }
28
- verbose && _check(pkgId, installedMoc, requiredMoc);
29
36
  }
30
37
  }
38
+ verbose || _check(tool, highestRequiredPkgId, installed, highestRequired);
31
39
  }
32
- verbose || _check(highestRequiredMocPkgId, installedMoc, highestRequiredMoc);
33
40
  }
34
- function _check(pkgId, installedMoc, requiredMoc) {
35
- let comp = installedMoc.compare(requiredMoc);
36
- if (comp < 0) {
37
- console.log(chalk.yellow(`moc version does not meet the requirements of ${pkgId}`));
38
- console.log(chalk.yellow(` Required: >= ${requiredMoc.format()}`));
39
- console.log(chalk.yellow(` Installed: ${installedMoc.format()}`));
41
+ function _check(tool, pkgId, installed, required) {
42
+ if (!pkgId || installed.compare(required) >= 0) {
43
+ return;
40
44
  }
45
+ console.log(chalk.yellow(`${tool} version does not meet the requirements of ${pkgId}`));
46
+ console.log(chalk.yellow(` Required: >= ${required.format()}`));
47
+ console.log(chalk.yellow(` Installed: ${installed.format()}`));
41
48
  }
package/dist/cli.js CHANGED
@@ -395,13 +395,15 @@ program
395
395
  .command("bench [filter]")
396
396
  .description("Run benchmarks")
397
397
  .addOption(new Option("--replica <replica>", "Which replica to use to run benchmarks (`dfx` is deprecated; prefer `pocket-ic`)").choices(["dfx", "pocket-ic"]))
398
- .addOption(new Option("--gc <gc>", "Garbage collector")
398
+ .addOption(new Option("--gc <gc>", "Garbage collector. Under enhanced orthogonal persistence (the default) the GC is fixed to `incremental`; selecting `copying`, `compacting`, or `generational` implies `--legacy-persistence`")
399
399
  .choices(["copying", "compacting", "generational", "incremental"])
400
- .default("copying"))
400
+ .default("incremental"))
401
401
  .addOption(new Option("--save", "Save benchmark results to .bench/<filename>.json"))
402
402
  .addOption(new Option("--compare", "Run benchmark and compare results with .bench/<filename>.json"))
403
403
  // .addOption(new Option('--force-gc', 'Force GC'))
404
- .addOption(new Option("--verbose", "Print the benchmark pipeline (compiler, replica, GC, optimization) and stream compiler/replica output, including dfx optimization warnings"))
404
+ .addOption(new Option("--query", "Measure each cell in a query call (how `query` methods run on the IC: no GC). Only for benchmarks whose runner is synchronous (no inter-canister calls)"))
405
+ .addOption(new Option("--legacy-persistence", "Compile benchmark canisters under legacy persistence instead of enhanced orthogonal persistence (the default)"))
406
+ .addOption(new Option("--verbose", "Print the benchmark pipeline (compiler, replica, GC, context, persistence, profile, optimization) and stream compiler/replica output, including dfx optimization warnings"))
405
407
  .action(async (filter, options) => {
406
408
  checkConfigFile(true);
407
409
  await installAll({
@@ -7,6 +7,8 @@ type BenchOptions = {
7
7
  compilerVersion: string;
8
8
  gc: "copying" | "compacting" | "generational" | "incremental";
9
9
  forceGc: boolean;
10
+ query: boolean;
11
+ legacyPersistence: boolean;
10
12
  save: boolean;
11
13
  compare: boolean;
12
14
  verbose: boolean;
@@ -29,8 +29,10 @@ export async function bench(filter = "", optionsArg = {}) {
29
29
  replicaVersion: "",
30
30
  compiler: "moc",
31
31
  compilerVersion: getMocVersion(true),
32
- gc: "copying",
32
+ gc: "incremental",
33
33
  forceGc: true,
34
+ query: false,
35
+ legacyPersistence: false,
34
36
  save: false,
35
37
  compare: false,
36
38
  verbose: false,
@@ -67,10 +69,17 @@ export async function bench(filter = "", optionsArg = {}) {
67
69
  let optimize = replicaType === "dfx" || replicaType === "dfx-pocket-ic"
68
70
  ? 'dfx `optimize: "cycles"` (ic-wasm) on deploy'
69
71
  : "none (raw moc output)";
72
+ let legacyGc = isLegacyGc(options.gc);
73
+ let effectiveLegacyPersistence = options.legacyPersistence || legacyGc;
70
74
  console.log(chalk.gray("Benchmark pipeline:"));
71
75
  console.log(chalk.gray(` compiler: moc ${options.compilerVersion}`));
72
76
  console.log(chalk.gray(` replica: ${replicaType}${options.replicaVersion ? ` ${options.replicaVersion}` : ""}`));
73
77
  console.log(chalk.gray(` gc: ${options.gc}${options.forceGc ? " (forced)" : ""}`));
78
+ console.log(chalk.gray(` context: ${options.query ? "query" : "update"}`));
79
+ console.log(chalk.gray(` persistence: ${effectiveLegacyPersistence ? "legacy" : "enhanced"}`));
80
+ if (legacyGc && !options.legacyPersistence) {
81
+ console.log(chalk.gray(` (gc '${options.gc}' only exists under legacy persistence; enabling --legacy-persistence)`));
82
+ }
74
83
  console.log(chalk.gray(` profile: ${options.profile}`));
75
84
  console.log(chalk.gray(` optimize: ${optimize}`));
76
85
  }
@@ -186,16 +195,26 @@ function computeDiffAll(currentResults, prevResults, rows, cols) {
186
195
  }
187
196
  return diff;
188
197
  }
198
+ // Collectors that only exist under legacy persistence. moc 0.15+ fixes the GC to
199
+ // incremental under enhanced orthogonal persistence and rejects these there.
200
+ function isLegacyGc(gc) {
201
+ return gc === "copying" || gc === "compacting" || gc === "generational";
202
+ }
189
203
  function getMocArgs(options) {
190
204
  let args = "";
191
- if (options.compilerVersion &&
192
- new SemVer(options.compilerVersion).compare("0.15.0") >= 0) {
205
+ let mocAtLeast015 = !!options.compilerVersion &&
206
+ new SemVer(options.compilerVersion).compare("0.15.0") >= 0;
207
+ // Legacy collectors require legacy persistence; moc < 0.15 is already legacy
208
+ // and has no --legacy-persistence flag.
209
+ let useLegacyPersistence = options.legacyPersistence || isLegacyGc(options.gc);
210
+ if (useLegacyPersistence && mocAtLeast015) {
193
211
  args += " --legacy-persistence";
194
212
  }
195
213
  if (options.forceGc) {
196
214
  args += " --force-gc";
197
215
  }
198
- if (options.gc) {
216
+ // Under EOP the GC is fixed; only pass a collector flag where it's selectable.
217
+ if (options.gc && (useLegacyPersistence || !mocAtLeast015)) {
199
218
  args += ` --${options.gc}-gc`;
200
219
  }
201
220
  if (options.profile === "Debug") {
@@ -386,10 +405,14 @@ async function runBenchFile(file, options, replica) {
386
405
  if (canUpdateLog) {
387
406
  log();
388
407
  }
389
- // run all cells
408
+ // run all cells. `--query` measures in query context (how `query` methods
409
+ // actually run on the IC: no GC), at the cost of not supporting benches whose
410
+ // runner performs async calls. Otherwise use the update path.
390
411
  for (let [rowIndex, row] of schema.rows.entries()) {
391
412
  for (let [colIndex, col] of schema.cols.entries()) {
392
- let res = await actor.runCellUpdateAwait(BigInt(rowIndex), BigInt(colIndex));
413
+ let res = options.query
414
+ ? await actor.runCellQuery(BigInt(rowIndex), BigInt(colIndex))
415
+ : await actor.runCellUpdateAwait(BigInt(rowIndex), BigInt(colIndex));
393
416
  results.set(`${row}:${col}`, res);
394
417
  // @ts-ignore
395
418
  instructionsCells[rowIndex][colIndex] = res.instructions;
@@ -4,7 +4,7 @@ import { exists } from "fs-extra";
4
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { join } from "node:path";
6
6
  import { lock, unlockSync } from "proper-lockfile";
7
- import { cliError } from "../error.js";
7
+ import { cliError, cliExit } from "../error.js";
8
8
  import { isCandidCompatible } from "../helpers/is-candid-compatible.js";
9
9
  import { filterCanisters, resolveCanisterConfigs, } from "../helpers/resolve-canisters.js";
10
10
  import { BUILD_MANAGED_FLAGS, prepareMocArgs } from "../helpers/moc-args.js";
@@ -107,7 +107,7 @@ export async function build(canisterNames, options) {
107
107
  console.error(result.stdout);
108
108
  }
109
109
  }
110
- cliError(`Build failed for canister ${canisterName} (exit code: ${result.exitCode})`);
110
+ cliExit(result.exitCode ?? 1, `Build failed for canister ${canisterName} (exit code: ${result.exitCode})`);
111
111
  }
112
112
  if (options.verbose && result.stdout && result.stdout.trim()) {
113
113
  console.log(result.stdout);
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, mkdtempSync } from "node:fs";
3
3
  import { rm } from "node:fs/promises";
4
4
  import chalk from "chalk";
5
5
  import { execa } from "execa";
6
- import { cliError } from "../error.js";
6
+ import { cliError, cliExit } from "../error.js";
7
7
  import { getCheckLimitPendingIssue, prepareMigrationArgs, reportCheckLimitPendingIssue, } from "../helpers/migrations.js";
8
8
  import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
9
9
  import { filterCanisters, looksLikeFile, resolveCanisterConfigs, resolveSingleCanister, validateCanisterArgs, } from "../helpers/resolve-canisters.js";
@@ -146,7 +146,7 @@ export async function runStableCheck(params) {
146
146
  if (result.stderr) {
147
147
  console.error(result.stderr);
148
148
  }
149
- cliError(`✗ Stable compatibility check failed for canister '${canisterName}'`);
149
+ cliExit(result.exitCode ?? 1, `✗ Stable compatibility check failed for canister '${canisterName}'`);
150
150
  }
151
151
  console.log(chalk.green(`✓ Stable compatibility check passed for canister '${canisterName}'`));
152
152
  }
@@ -178,7 +178,7 @@ async function generateStableTypes(mocPath, moFile, outputPath, sources, globalM
178
178
  if (result.stderr) {
179
179
  console.error(result.stderr);
180
180
  }
181
- cliError(`Failed to generate stable types for ${moFile} (exit code: ${result.exitCode})`);
181
+ cliExit(result.exitCode ?? 1, `Failed to generate stable types for ${moFile} (exit code: ${result.exitCode})`);
182
182
  }
183
183
  await rm(wasmPath, { force: true });
184
184
  return outputPath;
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import chalk from "chalk";
3
3
  import { execa } from "execa";
4
- import { cliError } from "../error.js";
4
+ import { cliError, cliExit } from "../error.js";
5
5
  import { getGlobalMocArgs, getRootDir, readConfig, resolveConfigPath, } from "../mops.js";
6
6
  import { autofixMotoko } from "../helpers/autofix-motoko.js";
7
7
  import { withFixLock } from "../helpers/fix-lock.js";
@@ -127,7 +127,7 @@ async function checkCanisters(config, canisters, options) {
127
127
  reject: false,
128
128
  });
129
129
  if (result.exitCode !== 0) {
130
- cliError(`✗ Check failed for canister ${canisterName} (exit code: ${result.exitCode})`);
130
+ cliExit(result.exitCode ?? 1, `✗ Check failed for canister ${canisterName} (exit code: ${result.exitCode})`);
131
131
  }
132
132
  console.log(chalk.green(`✓ ${canisterName}`));
133
133
  }
@@ -189,7 +189,7 @@ async function checkFiles(config, files, options) {
189
189
  reject: false,
190
190
  });
191
191
  if (result.exitCode !== 0) {
192
- cliError(`✗ Check failed for file ${file} (exit code: ${result.exitCode})`);
192
+ cliExit(result.exitCode ?? 1, `✗ Check failed for file ${file} (exit code: ${result.exitCode})`);
193
193
  }
194
194
  console.log(chalk.green(`✓ ${file}`));
195
195
  }
@@ -2,7 +2,7 @@ import chalk from "chalk";
2
2
  import { execa } from "execa";
3
3
  import { mkdir } from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { cliError } from "../error.js";
5
+ import { cliError, cliExit } from "../error.js";
6
6
  import { filterCanisters, resolveCanisterConfigs, } from "../helpers/resolve-canisters.js";
7
7
  import { GENERATE_CANDID_MANAGED_FLAGS, prepareMocArgs, } from "../helpers/moc-args.js";
8
8
  import { getRootDir, readConfig, resolveConfigPath, writeConfig, } from "../mops.js";
@@ -60,7 +60,7 @@ export async function generateCandid(canisterNames, options) {
60
60
  console.error(result.stdout);
61
61
  }
62
62
  }
63
- cliError(`Failed to generate Candid for canister ${canisterName} (exit code: ${result.exitCode})`);
63
+ cliExit(result.exitCode ?? 1, `Failed to generate Candid for canister ${canisterName} (exit code: ${result.exitCode})`);
64
64
  }
65
65
  if (dest.configPath !== null) {
66
66
  const c = (config.canisters ??= {});
@@ -98,17 +98,24 @@ export async function test(filter = "", options = {}) {
98
98
  else {
99
99
  let passed = await runAll(options.reporter, filter, options.mode, replicaType, false, undefined, explicitReplica);
100
100
  if (!passed) {
101
- process.exit(1);
101
+ process.exit(maxMocExit >= 2 ? 2 : 1);
102
102
  }
103
103
  }
104
104
  }
105
105
  let mocPath = "";
106
106
  let wasmtimePath = "";
107
+ let maxMocExit = 0;
108
+ const trackMocExit = (code) => {
109
+ if (code && code >= 2) {
110
+ maxMocExit = Math.max(maxMocExit, code);
111
+ }
112
+ };
107
113
  async function runAll(reporterName, filter = "", mode = "interpreter", replicaType, watch = false, signal, explicitReplica = false) {
108
114
  let done = await testWithReporter(reporterName, filter, mode, replicaType, watch, signal, explicitReplica);
109
115
  return done;
110
116
  }
111
117
  export async function testWithReporter(reporterName, filter = "", defaultMode = "interpreter", replicaType, watch = false, signal, explicitReplica = false) {
118
+ maxMocExit = 0;
112
119
  let rootDir = getRootDir();
113
120
  let files = [];
114
121
  let libFiles = globSync("**/test?(s)/lib.mo", MOTOKO_GLOB_CONFIG);
@@ -217,7 +224,7 @@ export async function testWithReporter(reporterName, filter = "", defaultMode =
217
224
  }
218
225
  throw error;
219
226
  });
220
- pipeMMF(proc, mmf).then(resolve);
227
+ pipeMMF(proc, mmf, trackMocExit).then(resolve);
221
228
  }
222
229
  // build and run wasm
223
230
  else if (mode === "wasi") {
@@ -230,7 +237,7 @@ export async function testWithReporter(reporterName, filter = "", defaultMode =
230
237
  }
231
238
  throw error;
232
239
  });
233
- pipeMMF(buildProc, mmf)
240
+ pipeMMF(buildProc, mmf, trackMocExit)
234
241
  .then(async () => {
235
242
  if (mmf.failed > 0) {
236
243
  return;
@@ -288,7 +295,7 @@ export async function testWithReporter(reporterName, filter = "", defaultMode =
288
295
  }
289
296
  throw error;
290
297
  });
291
- pipeMMF(buildProc, mmf)
298
+ pipeMMF(buildProc, mmf, trackMocExit)
292
299
  .then(async () => {
293
300
  if (mmf.failed > 0) {
294
301
  return;
@@ -4,4 +4,4 @@ import { MMF1 } from "./mmf1.js";
4
4
  export declare function absToRel(p: string): string;
5
5
  export declare function pipeStdoutToMMF(stdout: Readable, mmf: MMF1): void;
6
6
  export declare function pipeStderrToMMF(stderr: Readable, mmf: MMF1, dir?: string): void;
7
- export declare function pipeMMF(proc: ChildProcessWithoutNullStreams, mmf: MMF1): Promise<void>;
7
+ export declare function pipeMMF(proc: ChildProcessWithoutNullStreams, mmf: MMF1, onClose?: (code: number | null) => void): Promise<void>;
@@ -49,11 +49,10 @@ export function pipeStderrToMMF(stderr, mmf, dir = "") {
49
49
  mmf.fail(text);
50
50
  });
51
51
  }
52
- export function pipeMMF(proc, mmf) {
52
+ export function pipeMMF(proc, mmf, onClose) {
53
53
  return new Promise((resolve) => {
54
54
  pipeStdoutToMMF(proc.stdout, mmf);
55
55
  pipeStderrToMMF(proc.stderr, mmf);
56
- // exit
57
56
  proc.on("close", (code) => {
58
57
  if (code === 0) {
59
58
  mmf.strategy !== "print" && mmf.pass();
@@ -61,6 +60,7 @@ export function pipeMMF(proc, mmf) {
61
60
  else if (code !== 1) {
62
61
  mmf.fail(`unknown exit code: ${code}`);
63
62
  }
63
+ onClose?.(code ?? null);
64
64
  resolve();
65
65
  });
66
66
  });
@@ -259,9 +259,7 @@ async function update(tool) {
259
259
  let oldVersion = config.toolchain[tool];
260
260
  config.toolchain[tool] = version;
261
261
  writeConfig(config);
262
- if (tool === "moc") {
263
- await checkRequirements();
264
- }
262
+ await checkRequirements();
265
263
  if (oldVersion === version) {
266
264
  console.log(`Latest ${tool} ${version} is already installed`);
267
265
  }
package/dist/error.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export declare function cliError(...args: unknown[]): never;
2
+ export declare function cliExit(code: number, ...args: unknown[]): never;
package/dist/error.js CHANGED
@@ -3,3 +3,7 @@ export function cliError(...args) {
3
3
  console.error(chalk.red(...args));
4
4
  process.exit(1);
5
5
  }
6
+ export function cliExit(code, ...args) {
7
+ console.error(chalk.red(...args));
8
+ process.exit(code || 1);
9
+ }
@@ -0,0 +1,3 @@
1
+ import { type SemVer } from "semver";
2
+ export declare function getLintokoSemVer(): SemVer | null;
3
+ export declare function getLintokoVersion(throwOnError?: boolean): string;
@@ -0,0 +1,30 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { parse } from "semver";
3
+ import { FILE_PATH_REGEX } from "../constants.js";
4
+ import { readConfig } from "../mops.js";
5
+ export function getLintokoSemVer() {
6
+ return parse(getLintokoVersion(false));
7
+ }
8
+ export function getLintokoVersion(throwOnError = false) {
9
+ let configVersion = readConfig().toolchain?.lintoko;
10
+ if (!configVersion) {
11
+ return "";
12
+ }
13
+ if (!configVersion.match(FILE_PATH_REGEX)) {
14
+ return configVersion;
15
+ }
16
+ try {
17
+ let match = execFileSync(configVersion, ["--version"])
18
+ .toString()
19
+ .trim()
20
+ .match(/lintoko ([^\s]+)/);
21
+ return match?.[1] || "";
22
+ }
23
+ catch (e) {
24
+ if (throwOnError) {
25
+ console.error(e);
26
+ throw new Error("lintoko not found");
27
+ }
28
+ return "";
29
+ }
30
+ }
package/dist/mops.js CHANGED
@@ -177,9 +177,9 @@ export function readConfig(configFile = getClosestConfigFile()) {
177
177
  processDeps(toml["dev-dependencies"] || {});
178
178
  let config = { ...toml };
179
179
  Object.entries(config.requirements || {}).forEach(([name, value]) => {
180
- if (name === "moc") {
180
+ if (name === "moc" || name === "lintoko") {
181
181
  config.requirements = config.requirements || {};
182
- config.requirements.moc = value;
182
+ config.requirements[name] = value;
183
183
  }
184
184
  });
185
185
  return config;
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.15.2",
3
+ "version": "2.16.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "bin/mops.js",
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { describe, expect, jest, test } from "@jest/globals";
2
+ import { rmSync } from "node:fs";
3
+ import path from "path";
4
+ import { cli } from "./helpers";
5
+ // Pin moc 1.3.0 (≥ 0.15) to exercise the EOP path — this repo's own mops.toml
6
+ // uses moc 0.14.14, so the default bench run is never EOP-tested here.
7
+ describe("bench", () => {
8
+ jest.setTimeout(180_000);
9
+ test("runs under EOP with the default gc", async () => {
10
+ const cwd = path.join(import.meta.dirname, "bench");
11
+ try {
12
+ const result = await cli(["bench"], { cwd });
13
+ expect(result.stderr).not.toContain("--copying-gc is not supported");
14
+ expect(result.stderr).not.toContain("Invalid compiler flag combination");
15
+ expect(result.exitCode).toBe(0);
16
+ }
17
+ finally {
18
+ rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
19
+ }
20
+ });
21
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
2
+ import { cp, mkdir, rm, writeFile } from "node:fs/promises";
3
+ import path from "path";
4
+ import { bytesToHex } from "@noble/hashes/utils";
5
+ import { sha256 } from "@noble/hashes/sha256";
6
+ import { cli } from "./helpers";
7
+ describe("requirements", () => {
8
+ const fixtureDir = path.join(import.meta.dirname, "requirements-lintoko");
9
+ let tempDir;
10
+ beforeEach(async () => {
11
+ tempDir = path.join(import.meta.dirname, `_tmp_requirements_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`);
12
+ await cp(fixtureDir, tempDir, { recursive: true });
13
+ const depDir = path.join(tempDir, ".mops", "my-pkg@0.1.0");
14
+ await mkdir(depDir, { recursive: true });
15
+ await writeFile(path.join(depDir, "mops.toml"), `[package]
16
+ name = "my-pkg"
17
+ version = "0.1.0"
18
+
19
+ [requirements]
20
+ lintoko = "0.10.0"
21
+ `);
22
+ const mopsTomlDepsHash = bytesToHex(sha256(JSON.stringify({ "my-pkg": "0.1.0" })));
23
+ await writeFile(path.join(tempDir, "mops.lock"), JSON.stringify({
24
+ version: 3,
25
+ mopsTomlDepsHash,
26
+ deps: { "my-pkg": "0.1.0" },
27
+ hashes: {
28
+ "my-pkg@0.1.0": {
29
+ "mops.toml": "0000000000000000000000000000000000000000000000000000000000000000",
30
+ },
31
+ },
32
+ }));
33
+ });
34
+ afterEach(async () => {
35
+ await rm(tempDir, { recursive: true, force: true });
36
+ });
37
+ test("lintoko requirement warns when installed version is too old", async () => {
38
+ const result = await cli(["toolchain", "use", "lintoko", "0.7.0"], {
39
+ cwd: tempDir,
40
+ });
41
+ expect(result.exitCode).toBe(0);
42
+ expect(result.stdout).toMatch(/lintoko version does not meet the requirements of my-pkg@0\.1\.0/);
43
+ expect(result.stdout).toMatch(/Required: >= 0\.10\.0/);
44
+ expect(result.stdout).toMatch(/Installed:\s+0\.7\.0/);
45
+ });
46
+ });
package/dist/types.d.ts CHANGED
@@ -71,5 +71,6 @@ export type Toolchain = {
71
71
  export type Tool = "moc" | "wasmtime" | "pocket-ic" | "lintoko";
72
72
  export type Requirements = {
73
73
  moc?: string;
74
+ lintoko?: string;
74
75
  };
75
76
  export type TestMode = "interpreter" | "wasi" | "replica";
package/error.ts CHANGED
@@ -4,3 +4,8 @@ export function cliError(...args: unknown[]): never {
4
4
  console.error(chalk.red(...args));
5
5
  process.exit(1);
6
6
  }
7
+
8
+ export function cliExit(code: number, ...args: unknown[]): never {
9
+ console.error(chalk.red(...args));
10
+ process.exit(code || 1);
11
+ }
@@ -0,0 +1,32 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { type SemVer, parse } from "semver";
3
+ import { FILE_PATH_REGEX } from "../constants.js";
4
+ import { readConfig } from "../mops.js";
5
+
6
+ export function getLintokoSemVer(): SemVer | null {
7
+ return parse(getLintokoVersion(false));
8
+ }
9
+
10
+ export function getLintokoVersion(throwOnError = false): string {
11
+ let configVersion = readConfig().toolchain?.lintoko;
12
+ if (!configVersion) {
13
+ return "";
14
+ }
15
+ if (!configVersion.match(FILE_PATH_REGEX)) {
16
+ return configVersion;
17
+ }
18
+
19
+ try {
20
+ let match = execFileSync(configVersion, ["--version"])
21
+ .toString()
22
+ .trim()
23
+ .match(/lintoko ([^\s]+)/);
24
+ return match?.[1] || "";
25
+ } catch (e) {
26
+ if (throwOnError) {
27
+ console.error(e);
28
+ throw new Error("lintoko not found");
29
+ }
30
+ return "";
31
+ }
32
+ }
package/mops.ts CHANGED
@@ -205,9 +205,9 @@ export function readConfig(configFile = getClosestConfigFile()): Config {
205
205
  let config: Config = { ...toml };
206
206
 
207
207
  Object.entries(config.requirements || {}).forEach(([name, value]) => {
208
- if (name === "moc") {
208
+ if (name === "moc" || name === "lintoko") {
209
209
  config.requirements = config.requirements || {};
210
- config.requirements.moc = value;
210
+ config.requirements[name] = value;
211
211
  }
212
212
  });
213
213
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.15.2",
3
+ "version": "2.16.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "dist/bin/mops.js",
@@ -0,0 +1,27 @@
1
+ // Exercises the bench pipeline under EOP; the template only needs
2
+ // init() -> { getVersion; getSchema; runCell }, so no mo:bench dep.
3
+ module {
4
+ type Schema = {
5
+ name : Text;
6
+ description : Text;
7
+ rows : [Text];
8
+ cols : [Text];
9
+ };
10
+
11
+ class Bench(schema : Schema, run : (Nat, Nat) -> ()) {
12
+ public func getVersion() : Nat = 1;
13
+ public func getSchema() : Schema = schema;
14
+ public let runCell = run;
15
+ };
16
+
17
+ public func init() : Bench {
18
+ let schema : Schema = {
19
+ name = "Sanity";
20
+ description = "Trivial bench to exercise the mops bench pipeline under EOP";
21
+ rows = ["a"];
22
+ cols = ["1"];
23
+ };
24
+ func run(_ri : Nat, _ci : Nat) {};
25
+ Bench(schema, run);
26
+ };
27
+ };
@@ -0,0 +1,6 @@
1
+ [dependencies]
2
+ core = "2.0.0"
3
+
4
+ [toolchain]
5
+ moc = "1.3.0"
6
+ pocket-ic = "12.0.0"
@@ -0,0 +1,22 @@
1
+ import { describe, expect, jest, test } from "@jest/globals";
2
+ import { rmSync } from "node:fs";
3
+ import path from "path";
4
+ import { cli } from "./helpers";
5
+
6
+ // Pin moc 1.3.0 (≥ 0.15) to exercise the EOP path — this repo's own mops.toml
7
+ // uses moc 0.14.14, so the default bench run is never EOP-tested here.
8
+ describe("bench", () => {
9
+ jest.setTimeout(180_000);
10
+
11
+ test("runs under EOP with the default gc", async () => {
12
+ const cwd = path.join(import.meta.dirname, "bench");
13
+ try {
14
+ const result = await cli(["bench"], { cwd });
15
+ expect(result.stderr).not.toContain("--copying-gc is not supported");
16
+ expect(result.stderr).not.toContain("Invalid compiler flag combination");
17
+ expect(result.exitCode).toBe(0);
18
+ } finally {
19
+ rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
20
+ }
21
+ });
22
+ });
@@ -1,6 +1,6 @@
1
1
  name = "migration-self-contained"
2
2
  description = "Migration files must not import local modules (relative paths). Got: @path"
3
- includes = ["**/migrations/**"]
3
+ includes = ["migrations/*.mo"]
4
4
  query = '''
5
5
  (import (text_literal) @path @error
6
6
  (#match? @path "^\"[^:]*\"$"))
@@ -5,4 +5,4 @@ lintoko = "0.7.0"
5
5
  "src/Main.mo" = ["lint/no-types"]
6
6
  "src/Types.mo" = ["lint/types-only"]
7
7
  "src/Migration.mo" = ["lint/migration-only"]
8
- "migrations/**" = ["lint/migration-self-contained"]
8
+ "migrations/*.mo" = ["lint/migration-self-contained"]
@@ -0,0 +1,5 @@
1
+ [dependencies]
2
+ my-pkg = "0.1.0"
3
+
4
+ [toolchain]
5
+ lintoko = "0.7.0"
@@ -0,0 +1,66 @@
1
+ import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
2
+ import { cp, mkdir, rm, writeFile } from "node:fs/promises";
3
+ import path from "path";
4
+ import { bytesToHex } from "@noble/hashes/utils";
5
+ import { sha256 } from "@noble/hashes/sha256";
6
+ import { cli } from "./helpers";
7
+
8
+ describe("requirements", () => {
9
+ const fixtureDir = path.join(import.meta.dirname, "requirements-lintoko");
10
+ let tempDir: string;
11
+
12
+ beforeEach(async () => {
13
+ tempDir = path.join(
14
+ import.meta.dirname,
15
+ `_tmp_requirements_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
16
+ );
17
+ await cp(fixtureDir, tempDir, { recursive: true });
18
+
19
+ const depDir = path.join(tempDir, ".mops", "my-pkg@0.1.0");
20
+ await mkdir(depDir, { recursive: true });
21
+ await writeFile(
22
+ path.join(depDir, "mops.toml"),
23
+ `[package]
24
+ name = "my-pkg"
25
+ version = "0.1.0"
26
+
27
+ [requirements]
28
+ lintoko = "0.10.0"
29
+ `,
30
+ );
31
+
32
+ const mopsTomlDepsHash = bytesToHex(
33
+ sha256(JSON.stringify({ "my-pkg": "0.1.0" })),
34
+ );
35
+ await writeFile(
36
+ path.join(tempDir, "mops.lock"),
37
+ JSON.stringify({
38
+ version: 3,
39
+ mopsTomlDepsHash,
40
+ deps: { "my-pkg": "0.1.0" },
41
+ hashes: {
42
+ "my-pkg@0.1.0": {
43
+ "mops.toml":
44
+ "0000000000000000000000000000000000000000000000000000000000000000",
45
+ },
46
+ },
47
+ }),
48
+ );
49
+ });
50
+
51
+ afterEach(async () => {
52
+ await rm(tempDir, { recursive: true, force: true });
53
+ });
54
+
55
+ test("lintoko requirement warns when installed version is too old", async () => {
56
+ const result = await cli(["toolchain", "use", "lintoko", "0.7.0"], {
57
+ cwd: tempDir,
58
+ });
59
+ expect(result.exitCode).toBe(0);
60
+ expect(result.stdout).toMatch(
61
+ /lintoko version does not meet the requirements of my-pkg@0\.1\.0/,
62
+ );
63
+ expect(result.stdout).toMatch(/Required: >= 0\.10\.0/);
64
+ expect(result.stdout).toMatch(/Installed:\s+0\.7\.0/);
65
+ });
66
+ });
package/types.ts CHANGED
@@ -78,6 +78,7 @@ export type Tool = "moc" | "wasmtime" | "pocket-ic" | "lintoko";
78
78
 
79
79
  export type Requirements = {
80
80
  moc?: string;
81
+ lintoko?: string;
81
82
  };
82
83
 
83
84
  // export type Format = {