Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/update check 2 #206

Merged
merged 18 commits into from
Feb 12, 2024
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
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
Loading