diff --git a/backend/main/types.mo b/backend/main/types.mo index 6a3a779f..4cc2b0e3 100644 --- a/backend/main/types.mo +++ b/backend/main/types.mo @@ -81,11 +81,11 @@ module { // legacy for backward compatibility public type PackageConfigV3_Publishing = PackageConfigV2 and { - requirements : ?[Requirement]; // max 1 item + requirements : ?[Requirement]; // moc, lintoko }; public type PackageConfigV3 = PackageConfigV2 and { - requirements : [Requirement]; // max 1 item + requirements : [Requirement]; // moc, lintoko }; public type PackageSummary = { diff --git a/backend/main/utils/validateConfig.mo b/backend/main/utils/validateConfig.mo index b5746822..9dfa1616 100644 --- a/backend/main/utils/validateConfig.mo +++ b/backend/main/utils/validateConfig.mo @@ -219,11 +219,8 @@ module { return depValid; }; }; - if (config.requirements.size() > 1) { - return #err("invalid config: max requirements is 1"); - }; for (req in config.requirements.vals()) { - if (req.name != "moc") { + if (req.name != "moc" and req.name != "lintoko") { return #err("invalid config: unknown requirement '" # req.name # "'"); }; let versionValid = Semver.validate(req.value); diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 56242f39..2fe2b225 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next +- `[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). + ## 2.15.2 - `mops check-stable` (and the stable check inside `mops check`) reports when `[canisters..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. diff --git a/cli/check-requirements.ts b/cli/check-requirements.ts index 5c2b14df..988fdc2c 100644 --- a/cli/check-requirements.ts +++ b/cli/check-requirements.ts @@ -5,47 +5,69 @@ import chalk from "chalk"; import { getDependencyType, getRootDir, readConfig } from "./mops.js"; import { resolvePackages } from "./resolve-packages.js"; import { getMocSemVer } from "./helpers/get-moc-version.js"; +import { getLintokoSemVer } from "./helpers/get-lintoko-version.js"; import { getPackageId } from "./helpers/get-package-id.js"; +import type { Requirements } from "./types.js"; + +type ToolRequirement = keyof Requirements; + +const TOOL_REQUIREMENTS: { + tool: ToolRequirement; + getInstalled: () => SemVer | null; +}[] = [ + { tool: "moc", getInstalled: getMocSemVer }, + { tool: "lintoko", getInstalled: getLintokoSemVer }, +]; export async function checkRequirements({ verbose = false } = {}) { - let installedMoc = getMocSemVer(); - if (!installedMoc) { - return; - } - let highestRequiredMoc = new SemVer("0.0.0"); - let highestRequiredMocPkgId = ""; let rootDir = getRootDir(); - let resolvedPackages = await resolvePackages(); - for (let [name, version] of Object.entries(resolvedPackages)) { - if (getDependencyType(version) === "mops") { - let pkgId = getPackageId(name, version); - let depConfig = readConfig( - path.join(rootDir, ".mops", pkgId, "mops.toml"), - ); - let moc = depConfig.requirements?.moc; - - if (moc) { - let requiredMoc = new SemVer(moc); - if (highestRequiredMoc.compare(requiredMoc) < 0) { - highestRequiredMoc = requiredMoc; - highestRequiredMocPkgId = pkgId; + + for (let { tool, getInstalled } of TOOL_REQUIREMENTS) { + let installed = getInstalled(); + if (!installed) { + continue; + } + + let highestRequired = new SemVer("0.0.0"); + let highestRequiredPkgId = ""; + + for (let [name, version] of Object.entries(resolvedPackages)) { + if (getDependencyType(version) === "mops") { + let pkgId = getPackageId(name, version); + let depConfig = readConfig( + path.join(rootDir, ".mops", pkgId, "mops.toml"), + ); + let required = depConfig.requirements?.[tool]; + + if (required) { + let requiredVersion = new SemVer(required); + if (highestRequired.compare(requiredVersion) < 0) { + highestRequired = requiredVersion; + highestRequiredPkgId = pkgId; + } + verbose && _check(tool, pkgId, installed, requiredVersion); } - verbose && _check(pkgId, installedMoc, requiredMoc); } } - } - verbose || _check(highestRequiredMocPkgId, installedMoc, highestRequiredMoc); + verbose || _check(tool, highestRequiredPkgId, installed, highestRequired); + } } -function _check(pkgId: string, installedMoc: SemVer, requiredMoc: SemVer) { - let comp = installedMoc.compare(requiredMoc); - if (comp < 0) { - console.log( - chalk.yellow(`moc version does not meet the requirements of ${pkgId}`), - ); - console.log(chalk.yellow(` Required: >= ${requiredMoc.format()}`)); - console.log(chalk.yellow(` Installed: ${installedMoc.format()}`)); +function _check( + tool: ToolRequirement, + pkgId: string, + installed: SemVer, + required: SemVer, +) { + if (!pkgId || installed.compare(required) >= 0) { + return; } + + console.log( + chalk.yellow(`${tool} version does not meet the requirements of ${pkgId}`), + ); + console.log(chalk.yellow(` Required: >= ${required.format()}`)); + console.log(chalk.yellow(` Installed: ${installed.format()}`)); } diff --git a/cli/commands/toolchain/index.ts b/cli/commands/toolchain/index.ts index b80ba127..544408f2 100644 --- a/cli/commands/toolchain/index.ts +++ b/cli/commands/toolchain/index.ts @@ -337,9 +337,7 @@ async function update(tool?: Tool) { config.toolchain[tool] = version; writeConfig(config); - if (tool === "moc") { - await checkRequirements(); - } + await checkRequirements(); if (oldVersion === version) { console.log(`Latest ${tool} ${version} is already installed`); diff --git a/cli/helpers/get-lintoko-version.ts b/cli/helpers/get-lintoko-version.ts new file mode 100644 index 00000000..8073aeeb --- /dev/null +++ b/cli/helpers/get-lintoko-version.ts @@ -0,0 +1,32 @@ +import { execFileSync } from "node:child_process"; +import { type SemVer, parse } from "semver"; +import { FILE_PATH_REGEX } from "../constants.js"; +import { readConfig } from "../mops.js"; + +export function getLintokoSemVer(): SemVer | null { + return parse(getLintokoVersion(false)); +} + +export function getLintokoVersion(throwOnError = false): string { + let configVersion = readConfig().toolchain?.lintoko; + if (!configVersion) { + return ""; + } + if (!configVersion.match(FILE_PATH_REGEX)) { + return configVersion; + } + + try { + let match = execFileSync(configVersion, ["--version"]) + .toString() + .trim() + .match(/lintoko ([^\s]+)/); + return match?.[1] || ""; + } catch (e) { + if (throwOnError) { + console.error(e); + throw new Error("lintoko not found"); + } + return ""; + } +} diff --git a/cli/mops.ts b/cli/mops.ts index 2b973025..4e035a29 100644 --- a/cli/mops.ts +++ b/cli/mops.ts @@ -205,9 +205,9 @@ export function readConfig(configFile = getClosestConfigFile()): Config { let config: Config = { ...toml }; Object.entries(config.requirements || {}).forEach(([name, value]) => { - if (name === "moc") { + if (name === "moc" || name === "lintoko") { config.requirements = config.requirements || {}; - config.requirements.moc = value; + config.requirements[name] = value; } }); diff --git a/cli/tests/requirements-lintoko/mops.toml b/cli/tests/requirements-lintoko/mops.toml new file mode 100644 index 00000000..4c016783 --- /dev/null +++ b/cli/tests/requirements-lintoko/mops.toml @@ -0,0 +1,5 @@ +[dependencies] +my-pkg = "0.1.0" + +[toolchain] +lintoko = "0.7.0" diff --git a/cli/tests/requirements.test.ts b/cli/tests/requirements.test.ts new file mode 100644 index 00000000..fe360471 --- /dev/null +++ b/cli/tests/requirements.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test, beforeEach, afterEach } from "@jest/globals"; +import { cp, mkdir, rm, writeFile } from "node:fs/promises"; +import path from "path"; +import { bytesToHex } from "@noble/hashes/utils"; +import { sha256 } from "@noble/hashes/sha256"; +import { cli } from "./helpers"; + +describe("requirements", () => { + const fixtureDir = path.join(import.meta.dirname, "requirements-lintoko"); + let tempDir: string; + + beforeEach(async () => { + tempDir = path.join( + import.meta.dirname, + `_tmp_requirements_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + ); + await cp(fixtureDir, tempDir, { recursive: true }); + + const depDir = path.join(tempDir, ".mops", "my-pkg@0.1.0"); + await mkdir(depDir, { recursive: true }); + await writeFile( + path.join(depDir, "mops.toml"), + `[package] +name = "my-pkg" +version = "0.1.0" + +[requirements] +lintoko = "0.10.0" +`, + ); + + const mopsTomlDepsHash = bytesToHex( + sha256(JSON.stringify({ "my-pkg": "0.1.0" })), + ); + await writeFile( + path.join(tempDir, "mops.lock"), + JSON.stringify({ + version: 3, + mopsTomlDepsHash, + deps: { "my-pkg": "0.1.0" }, + hashes: { + "my-pkg@0.1.0": { + "mops.toml": + "0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + }), + ); + }); + + afterEach(async () => { + await rm(tempDir, { recursive: true, force: true }); + }); + + test("lintoko requirement warns when installed version is too old", async () => { + const result = await cli(["toolchain", "use", "lintoko", "0.7.0"], { + cwd: tempDir, + }); + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch( + /lintoko version does not meet the requirements of my-pkg@0\.1\.0/, + ); + expect(result.stdout).toMatch(/Required: >= 0\.10\.0/); + expect(result.stdout).toMatch(/Installed:\s+0\.7\.0/); + }); +}); diff --git a/cli/types.ts b/cli/types.ts index ebbb08bb..861dd948 100644 --- a/cli/types.ts +++ b/cli/types.ts @@ -78,6 +78,7 @@ export type Tool = "moc" | "wasmtime" | "pocket-ic" | "lintoko"; export type Requirements = { moc?: string; + lintoko?: string; }; // export type Format = { diff --git a/docs/docs/09-mops.toml.md b/docs/docs/09-mops.toml.md index 7387238c..25150747 100644 --- a/docs/docs/09-mops.toml.md +++ b/docs/docs/09-mops.toml.md @@ -252,11 +252,12 @@ Globs that match no files are skipped with a warning. All runs (base and extra) When a user installs your package(as a transitive dependency too), Mops will check if the requirements are met and display a warning if they are not. -Use only if your package will not work with older versions of the `moc`. +Use when your package will not work with older versions of the `moc` compiler or `lintoko` linter (e.g. rules that depend on lintoko features from a specific release). | Field | Description | | -------------------- | ------------------------------------------------ | | moc | Motoko compiler version (e.g. `0.11.0` which means `>=0.11.0`) | +| lintoko | Lintoko linter version (e.g. `0.10.0` which means `>=0.10.0`) | ## Advanced Configuration