Skip to content

Commit

Permalink
feat: Update swanky check (#114)
Browse files Browse the repository at this point in the history
Co-authored-by: prxgr4mm3r <[email protected]>
  • Loading branch information
ipapandinas and prxgr4mm3r authored Feb 12, 2024
1 parent 23549e4 commit 6701f42
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 33 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
206 changes: 180 additions & 26 deletions src/commands/check/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,57 +26,123 @@ interface Ctx {
cargoDylint?: string | null;
cargoContract?: string | null;
};
supportedInk?: string;
missingTools: string[];
contracts: Record<string, Record<string, string>>;
node?: string | null;
swankyNode: string | null;
};
swankyConfig?: SwankyConfig;
mismatchedVersions?: Record<string, string>;
mismatchedVersions: Record<string, string>;
looseDefinitionDetected: boolean;
}

export default class Check extends SwankyCommand<typeof Check> {
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<void> {
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<Ctx>([
{
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;
Expand All @@ -86,7 +161,7 @@ export default class Check extends SwankyCommand<typeof Check> {
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];
Expand All @@ -96,43 +171,122 @@ export default class Check extends SwankyCommand<typeof Check> {
},
},
{
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<string, string> = {};
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<string, string> = {};
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)}`);
}
}
}

Expand Down
10 changes: 5 additions & 5 deletions src/commands/node/install.ts
Original file line number Diff line number Diff line change
@@ -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<typeof InstallNode> {
Expand Down Expand Up @@ -41,9 +41,9 @@ export class InstallNode extends SwankyCommand<typeof InstallNode> {
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;
}
Expand Down
13 changes: 13 additions & 0 deletions src/lib/cargoContractInfo.ts
Original file line number Diff line number Diff line change
@@ -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" },
];
2 changes: 1 addition & 1 deletion src/lib/nodeInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const swankyNodeVersions = new Map<string, nodeInfo>([
["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",
Expand Down

0 comments on commit 6701f42

Please sign in to comment.