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
4 changes: 2 additions & 2 deletions backend/main/types.mo
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
5 changes: 1 addition & 4 deletions backend/main/utils/validateConfig.mo
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<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.
Expand Down
84 changes: 53 additions & 31 deletions cli/check-requirements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()}`));
}
4 changes: 1 addition & 3 deletions cli/commands/toolchain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand Down
32 changes: 32 additions & 0 deletions cli/helpers/get-lintoko-version.ts
Original file line number Diff line number Diff line change
@@ -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 "";
}
}
4 changes: 2 additions & 2 deletions cli/mops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
});

Expand Down
5 changes: 5 additions & 0 deletions cli/tests/requirements-lintoko/mops.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[dependencies]
my-pkg = "0.1.0"

[toolchain]
lintoko = "0.7.0"
66 changes: 66 additions & 0 deletions cli/tests/requirements.test.ts
Original file line number Diff line number Diff line change
@@ -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/);
});
});
1 change: 1 addition & 0 deletions cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export type Tool = "moc" | "wasmtime" | "pocket-ic" | "lintoko";

export type Requirements = {
moc?: string;
lintoko?: string;
};

// export type Format = {
Expand Down
3 changes: 2 additions & 1 deletion docs/docs/09-mops.toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading