From 6701f4290e324795882fac2de7464367e1a7edc4 Mon Sep 17 00:00:00 2001 From: Igor Papandinas Date: Mon, 12 Feb 2024 12:01:55 +0100 Subject: [PATCH] feat: Update swanky check (#114) Co-authored-by: prxgr4mm3r --- README.md | 2 +- src/commands/check/index.ts | 206 ++++++++++++++++++++++++++++++----- src/commands/node/install.ts | 10 +- src/lib/cargoContractInfo.ts | 13 +++ src/lib/nodeInfo.ts | 2 +- 5 files changed, 200 insertions(+), 33 deletions(-) create mode 100644 src/lib/cargoContractInfo.ts diff --git a/README.md b/README.md index 51a6b4e4..6d707795 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ A newly generated project will have a `swanky.config.json` file that will get po "node": { "localPath": "/Users/sasapul/Work/astar/swanky-cli/temp_proj/bin/swanky-node", "polkadotPalletVersions": "polkadot-v0.9.39", - "supportedInk": "v4.2.0" + "supportedInk": "v4.3.0" }, "accounts": [ { diff --git a/src/commands/check/index.ts b/src/commands/check/index.ts index 8af906ae..ce45afe9 100644 --- a/src/commands/check/index.ts +++ b/src/commands/check/index.ts @@ -1,14 +1,23 @@ import { Listr } from "listr2"; import { commandStdoutOrNull } from "../../lib/index.js"; import { SwankyConfig } from "../../types/index.js"; -import { pathExistsSync, readJSON } from "fs-extra/esm"; +import { pathExistsSync, readJSON, writeJson } from "fs-extra/esm"; import { readFileSync } from "fs"; import path from "node:path"; import TOML from "@iarna/toml"; import semver from "semver"; import { SwankyCommand } from "../../lib/swankyCommand.js"; +import { Flags } from "@oclif/core"; +import chalk from "chalk"; +import { CARGO_CONTRACT_INK_DEPS } from "../../lib/cargoContractInfo.js"; +import { CLIError } from "@oclif/core/lib/errors/index.js"; +import Warn = CLIError.Warn; interface Ctx { + os: { + platform: string; + architecture: string; + }, versions: { tools: { rust?: string | null; @@ -17,57 +26,123 @@ interface Ctx { cargoDylint?: string | null; cargoContract?: string | null; }; + supportedInk?: string; + missingTools: string[]; contracts: Record>; - node?: string | null; + swankyNode: string | null; }; swankyConfig?: SwankyConfig; - mismatchedVersions?: Record; + mismatchedVersions: Record; looseDefinitionDetected: boolean; } export default class Check extends SwankyCommand { static description = "Check installed package versions and compatibility"; + static flags = { + print: Flags.string({ + char: "o", + description: "File to write output to", + }), + }; + public async run(): Promise { + const { flags } = await this.parse(Check); + const swankyNodeVersion = this.swankyConfig.node.version; + const isSwankyNodeInstalled = !!swankyNodeVersion; + const anyContracts = Object.keys(this.swankyConfig?.contracts).length > 0; const tasks = new Listr([ + { + title: "Check OS", + task: async (ctx, task) => { + ctx.os.platform = process.platform; + ctx.os.architecture = process.arch; + const supportedPlatforms = ["darwin", "linux"]; + const supportedArch = ["arm64", "x64"]; + + if (!supportedPlatforms.includes(ctx.os.platform)) { + throw new Error(`Platform ${ctx.os.platform} is not supported`); + } + if (!supportedArch.includes(ctx.os.architecture)) { + throw new Error(`Architecture ${ctx.os.architecture} is not supported`); + } + + task.title = `Check OS: '${ctx.os.platform}-${ctx.os.architecture}'`; + }, + exitOnError: false, + }, { title: "Check Rust", - task: async (ctx) => { - ctx.versions.tools.rust = await commandStdoutOrNull("rustc --version"); + task: async (ctx, task) => { + ctx.versions.tools.rust = (await commandStdoutOrNull("rustc --version"))?.match(/rustc (.*) \((.*)/)?.[1]; + if (!ctx.versions.tools.rust) { + throw new Error("Rust is not installed"); + } + task.title = `Check Rust: ${ctx.versions.tools.rust}`; }, + exitOnError: false, }, { title: "Check cargo", - task: async (ctx) => { - ctx.versions.tools.cargo = await commandStdoutOrNull("cargo -V"); + task: async (ctx, task) => { + ctx.versions.tools.cargo = (await commandStdoutOrNull("cargo -V"))?.match(/cargo (.*) \((.*)/)?.[1]; + if (!ctx.versions.tools.cargo) { + throw new Error("Cargo is not installed"); + } + task.title = `Check cargo: ${ctx.versions.tools.cargo}`; }, + exitOnError: false, }, { title: "Check cargo nightly", - task: async (ctx) => { - ctx.versions.tools.cargoNightly = await commandStdoutOrNull("cargo +nightly -V"); + task: async (ctx, task) => { + ctx.versions.tools.cargoNightly = (await commandStdoutOrNull("cargo +nightly -V"))?.match(/cargo (.*)-nightly \((.*)/)?.[1]; + if (!ctx.versions.tools.cargoNightly) { + throw new Error("Cargo nightly is not installed"); + } + task.title = `Check cargo nightly: ${ctx.versions.tools.cargoNightly}`; }, + exitOnError: false, }, { title: "Check cargo dylint", - task: async (ctx) => { - ctx.versions.tools.cargoDylint = await commandStdoutOrNull("cargo dylint -V"); + task: async (ctx, task) => { + ctx.versions.tools.cargoDylint = (await commandStdoutOrNull("cargo dylint -V"))?.match(/cargo-dylint (.*)/)?.[1]; + if (!ctx.versions.tools.cargoDylint) { + throw new Warn("Cargo dylint is not installed"); + } + task.title = `Check cargo dylint: ${ctx.versions.tools.cargoDylint}`; }, + exitOnError: false, }, { title: "Check cargo-contract", - task: async (ctx) => { + task: async (ctx, task) => { ctx.versions.tools.cargoContract = await commandStdoutOrNull("cargo contract -V"); + if (!ctx.versions.tools.cargoContract) { + throw new Error("Cargo contract is not installed"); + } + + const regex = /cargo-contract-contract (\d+\.\d+\.\d+(?:-[\w.]+)?)(?:-unknown-[\w-]+)/; + const match = ctx.versions.tools.cargoContract.match(regex); + if (match?.[1]) { + ctx.versions.tools.cargoContract = match[1]; + } else { + throw new Error("Cargo contract version not found"); + } + task.title = `Check cargo-contract: ${ctx.versions.tools.cargoContract}`; }, + exitOnError: false, }, { title: "Check swanky node", task: async (ctx) => { - ctx.versions.node = this.swankyConfig.node.version !== "" ? this.swankyConfig.node.version : null; + ctx.versions.swankyNode = this.swankyConfig.node.version !== "" ? this.swankyConfig.node.version : null; }, }, { title: "Read ink dependencies", + enabled: anyContracts, task: async (ctx) => { const swankyConfig = await readJSON("swanky.config.json"); ctx.swankyConfig = swankyConfig; @@ -86,7 +161,7 @@ export default class Check extends SwankyCommand { const cargoToml = TOML.parse(cargoTomlString); const inkDependencies = Object.entries(cargoToml.dependencies) - .filter((dependency) => dependency[0].includes("ink_")) + .filter((dependency) => dependency[0].includes("ink")) .map(([depName, depInfo]) => { const dependency = depInfo as Dependency; return [depName, dependency.version ?? dependency.tag]; @@ -96,43 +171,122 @@ export default class Check extends SwankyCommand { }, }, { - title: "Verify ink version", + title: "Verify ink version compatibility with Swanky node", + skip: (ctx) => Object.keys(ctx.versions.contracts).length === 0, + enabled: anyContracts && isSwankyNodeInstalled, task: async (ctx) => { - const supportedInk = ctx.swankyConfig?.node.supportedInk; - + const supportedInk = ctx.swankyConfig!.node.supportedInk; const mismatched: Record = {}; Object.entries(ctx.versions.contracts).forEach(([contract, inkPackages]) => { Object.entries(inkPackages).forEach(([inkPackage, version]) => { - if (semver.gt(version, supportedInk!)) { + if (semver.gt(version, supportedInk)) { mismatched[ `${contract}-${inkPackage}` - ] = `Version of ${inkPackage} (${version}) in ${contract} is higher than supported ink version (${supportedInk})`; + ] = `Version of ${inkPackage} (${version}) in ${chalk.yellowBright(contract)} is higher than supported ink version (${supportedInk}) in current Swanky node version (${swankyNodeVersion}). A Swanky node update can fix this warning.`; } - if (!(version.startsWith("=") || version.startsWith("v"))) { + if (version.startsWith(">") || version.startsWith("<") || version.startsWith("^") || version.startsWith("~")) { ctx.looseDefinitionDetected = true; } }); }); ctx.mismatchedVersions = mismatched; + if (Object.entries(mismatched).length > 0) { + throw new Warn("Ink versions in contracts don't match the Swanky node's supported version."); + } }, + exitOnError: false, + }, + { + title: "Verify cargo contract compatibility", + skip: (ctx) => !ctx.versions.tools.cargoContract, + enabled: anyContracts, + task: async (ctx) => { + const cargoContractVersion = ctx.versions.tools.cargoContract!; + const dependencyIdx = CARGO_CONTRACT_INK_DEPS.findIndex((dep) => + semver.satisfies(cargoContractVersion.replace(/-.*$/, ""), `>=${dep.minCargoContractVersion}`) + ); + + if (dependencyIdx === -1) { + throw new Warn(`cargo-contract version ${cargoContractVersion} is not supported`); + } + + const validInkVersionRange = CARGO_CONTRACT_INK_DEPS[dependencyIdx].validInkVersionRange; + const minCargoContractVersion = dependencyIdx === 0 + ? CARGO_CONTRACT_INK_DEPS[dependencyIdx].minCargoContractVersion + : CARGO_CONTRACT_INK_DEPS[dependencyIdx - 1].minCargoContractVersion + + const mismatched: Record = {}; + Object.entries(ctx.versions.contracts).forEach(([contract, inkPackages]) => { + Object.entries(inkPackages).forEach(([inkPackage, version]) => { + if (!semver.satisfies(version, validInkVersionRange)) { + mismatched[ + `${contract}-${inkPackage}` + ] = `Version of ${inkPackage} (${version}) in ${chalk.yellowBright(contract)} requires cargo-contract version >=${minCargoContractVersion}, but version ${cargoContractVersion} is installed`; + } + }); + }); + + ctx.mismatchedVersions = { ...ctx.mismatchedVersions, ...mismatched }; + if (Object.entries(mismatched).length > 0) { + throw new Warn("cargo-contract version mismatch"); + } + }, + exitOnError: false, + }, + { + title: "Check for missing tools", + task: async (ctx) => { + const missingTools: string[] = []; + for (const [toolName, toolVersion] of Object.entries(ctx.versions.tools)) { + if (!toolVersion) { + missingTools.push(toolName); + if (toolName === "cargoDylint") this.warn("Cargo dylint is not installed"); + else this.error(`${toolName} is not installed`); + } + } + ctx.versions.missingTools = missingTools; + if (Object.entries(missingTools).length > 0) { + throw new Warn(`Missing tools: ${missingTools.join(", ")}`); + } + }, + exitOnError: false, }, ]); + const context = await tasks.run({ - versions: { tools: {}, contracts: {} }, + os: { platform: "", architecture: "" }, + versions: { + tools: {}, + missingTools: [], + contracts: {}, + swankyNode: swankyNodeVersion || null, + }, looseDefinitionDetected: false, + mismatchedVersions: {} }); - console.log(context.versions); - Object.values(context.mismatchedVersions as any).forEach((mismatch) => - console.error(`[ERROR] ${mismatch as string}`) - ); + + Object.values(context.mismatchedVersions).forEach((mismatch) => this.warn(mismatch)); + if (context.looseDefinitionDetected) { - console.log(`\n[WARNING]Some of the ink dependencies do not have a fixed version. + this.warn(`Some of the ink dependencies do not have a fixed version. This can lead to accidentally installing version higher than supported by the node. Please use "=" to install a fixed version (Example: "=3.0.1") `); } + + const output = { + ...context.os, + ...context.versions + } + + const filePath = flags.print; + if (filePath !== undefined) { + await this.spinner.runCommand(async () => { + writeJson(filePath, output, { spaces: 2 }); + }, `Writing output to file ${chalk.yellowBright(filePath)}`); + } } } diff --git a/src/commands/node/install.ts b/src/commands/node/install.ts index af90048c..79beab3e 100644 --- a/src/commands/node/install.ts +++ b/src/commands/node/install.ts @@ -1,11 +1,11 @@ import { SwankyCommand } from "../../lib/swankyCommand.js"; -import { ux, Flags } from "@oclif/core"; +import { Flags } from "@oclif/core"; import { downloadNode, swankyNodeVersions } from "../../lib/index.js"; import path from "node:path"; import { writeJSON } from "fs-extra/esm"; import inquirer from "inquirer"; import { DEFAULT_NODE_INFO } from "../../lib/consts.js"; -import { pickNodeVersion } from "../../lib/prompts.js"; +import { choice, pickNodeVersion } from "../../lib/prompts.js"; import { InputError } from "../../lib/errors.js"; export class InstallNode extends SwankyCommand { @@ -41,9 +41,9 @@ export class InstallNode extends SwankyCommand { const projectPath = path.resolve(); if (this.swankyConfig.node.localPath !== "") { - const overwrite = await ux.confirm( - "Swanky node already installed. Do you want to overwrite it? (y/n)" - ); + const { overwrite } =await inquirer.prompt([ + choice("overwrite", "Swanky node already installed. Do you want to overwrite it?"), + ]); if (!overwrite) { return; } diff --git a/src/lib/cargoContractInfo.ts b/src/lib/cargoContractInfo.ts new file mode 100644 index 00000000..eafdd57d --- /dev/null +++ b/src/lib/cargoContractInfo.ts @@ -0,0 +1,13 @@ +export interface CargoContractInkDependency { + minCargoContractVersion: string; + validInkVersionRange: string; +} + +// Keep cargo-contract versions in descending order +// Ranges are supported by semver +export const CARGO_CONTRACT_INK_DEPS: CargoContractInkDependency[] = [ + { minCargoContractVersion: "4.0.0", validInkVersionRange: "<99.0.0" }, // Non-max version known yet: a very high version is used as fallback in the meantime + { minCargoContractVersion: "2.2.0", validInkVersionRange: "<5.0.0" }, + { minCargoContractVersion: "2.0.2", validInkVersionRange: "<4.2.0" }, + { minCargoContractVersion: "2.0.0", validInkVersionRange: "<4.0.1" }, +]; \ No newline at end of file diff --git a/src/lib/nodeInfo.ts b/src/lib/nodeInfo.ts index 60a6d1f0..59e2f415 100644 --- a/src/lib/nodeInfo.ts +++ b/src/lib/nodeInfo.ts @@ -18,7 +18,7 @@ export const swankyNodeVersions = new Map([ ["1.6.0", { version: "1.6.0", polkadotPalletVersions: "polkadot-v0.9.39", - supportedInk: "v4.2.0", + supportedInk: "v4.3.0", downloadUrl: { darwin: { "arm64": "https://github.com/AstarNetwork/swanky-node/releases/download/v1.6.0/swanky-node-v1.6.0-macOS-universal.tar.gz",