From 5750e85c40ea500d62983c188bc458bd1540268e Mon Sep 17 00:00:00 2001 From: RetricSu Date: Mon, 2 Sep 2024 08:08:59 +0800 Subject: [PATCH] feat: add mol cmd with mol-related tools (#127) --- README.md | 1 + example-mol/attributes.mol | 13 ++++ example-mol/basic_types.mol | 25 ++++++++ example-mol/role.mol | 22 +++++++ example-mol/skills.mol | 33 ++++++++++ src/cfg/setting.ts | 14 +++++ src/cli.ts | 17 ++++++ src/cmd/mol.ts | 22 +++++++ src/molecule/mol.ts | 119 ++++++++++++++++++++++++++++++++++++ src/node/install.ts | 6 +- src/tools/moleculec-es.ts | 86 ++++++++++++++++++++++++++ src/tools/moleculec-go.ts | 33 ++++++++++ src/tools/moleculec-rust.ts | 33 ++++++++++ 13 files changed, 421 insertions(+), 3 deletions(-) create mode 100644 example-mol/attributes.mol create mode 100644 example-mol/basic_types.mol create mode 100644 example-mol/role.mol create mode 100644 example-mol/skills.mol create mode 100644 src/cmd/mol.ts create mode 100644 src/molecule/mol.ts create mode 100644 src/tools/moleculec-es.ts create mode 100644 src/tools/moleculec-go.ts create mode 100644 src/tools/moleculec-rust.ts diff --git a/README.md b/README.md index 6002125..49152b2 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ Commands: config [item] [value] do a configuration action debug [options] CKB Debugger for development system-scripts [options] Output system scripts of the local devnet + mol [options] Generate CKB Moleculec binding code for development help [command] display help for command ``` diff --git a/example-mol/attributes.mol b/example-mol/attributes.mol new file mode 100644 index 0000000..c20d088 --- /dev/null +++ b/example-mol/attributes.mol @@ -0,0 +1,13 @@ +import basic_types; + +// Each role has 8 attributes. The size is fixed. +struct Attributes { + strength: AttrValue, + dexterity: AttrValue, + endurance: AttrValue, + speed: AttrValue, + intelligence: AttrValue, + wisdom: AttrValue, + perception: AttrValue, + concentration: AttrValue, +} diff --git a/example-mol/basic_types.mol b/example-mol/basic_types.mol new file mode 100644 index 0000000..188b4f2 --- /dev/null +++ b/example-mol/basic_types.mol @@ -0,0 +1,25 @@ +// AttrValue is an alias of `byte`. +// +// Since Molecule data are strongly-typed, it can gives compile time guarantees +// that the right type of value is supplied to a method. +// +// In this example, we use this alias to define an unsigned integer which +// has an upper limit: 100. +// So it's easy to distinguish between this type and a real `byte`. +// Of course, the serialization wouldn't do any checks for this upper limit +// automatically. You have to implement it by yourself. +// +// **NOTE**: +// - This feature is dependent on the exact implementation. +// In official Rust generated code, we use new type to implement this feature. +array AttrValue [byte; 1]; + +// SkillLevel is an alias of `byte`, too. +// +// Each skill has only 10 levels, so we use another alias of `byte` to distinguish. +array SkillLevel [byte; 1]; + +// Define several unsigned integers. +array Uint8 [byte; 1]; +array Uint16 [byte; 2]; +array Uint32 [byte; 4]; diff --git a/example-mol/role.mol b/example-mol/role.mol new file mode 100644 index 0000000..663865b --- /dev/null +++ b/example-mol/role.mol @@ -0,0 +1,22 @@ +import attributes; +import skills; +import basic_types; + +// We have only 3 classes: Fighter, Ranger and Mage. A `byte` is enough. +array Class [byte; 1]; + +table Hero { + class: Class, + level: Uint8, + experiences: Uint32, + hp: Uint16, + mp: Uint16, + base_damage: Uint16, + attrs: Attributes, + skills: Skills, +} + +table Monster { + hp: Uint16, + damage: Uint16, +} diff --git a/example-mol/skills.mol b/example-mol/skills.mol new file mode 100644 index 0000000..bcb3e2b --- /dev/null +++ b/example-mol/skills.mol @@ -0,0 +1,33 @@ +import basic_types; + +// We define several skills. +// None means the role can learn a skill but he/she doesn't learn it. +option ArmorLight (SkillLevel); +option ArmorHeavy (SkillLevel); // only Fighter can learn this +option ArmorShields (SkillLevel); // only Fighter can learn this +option WeaponSwords (SkillLevel); // only Mage can't learn this +option WeaponBows (SkillLevel); // only Ranger can learn this +option WeaponBlunt (SkillLevel); +option Dodge (SkillLevel); +option PickLocks (SkillLevel); +option Mercantile (SkillLevel); +option Survival (SkillLevel); +// ... omit other skills ... + +// Any skill which is defined above. +union Skill { + ArmorLight, + ArmorHeavy, + ArmorShields, + WeaponSwords, + WeaponBows, + WeaponBlunt, + Dodge, + PickLocks, + Mercantile, + Survival, + // ... omit other skills ... +} + +// A hero can learn several skills. The size of learned skills is dynamic. +vector Skills ; diff --git a/src/cfg/setting.ts b/src/cfg/setting.ts index e57d6e0..cf87d6b 100644 --- a/src/cfg/setting.ts +++ b/src/cfg/setting.ts @@ -51,6 +51,13 @@ export interface Settings { gitFolder: string; downloadPath: string; }; + tools: { + moleculeES: { + downloadPath: string; + cachePath: string; + binFolder: string; + }; + }; } export const defaultSettings: Settings = { @@ -94,6 +101,13 @@ export const defaultSettings: Settings = { gitFolder: 'templates/v3', downloadPath: path.resolve(cachePath, 'download', 'dapp-template'), }, + tools: { + moleculeES: { + downloadPath: path.resolve(cachePath, 'download', 'molecule-es'), + cachePath: path.resolve(cachePath, 'tools', 'moleculec-es'), + binFolder: path.resolve(dataPath, 'tools', 'moleculec-es'), + }, + }, }; export function readSettings(): Settings { diff --git a/src/cli.ts b/src/cli.ts index 5dcc766..82f48f4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,6 +17,8 @@ import { Config, ConfigItem } from './cmd/config'; import { debugSingleScript, debugTransaction, parseSingleScriptOption } from './cmd/debug'; import { printSystemScripts } from './cmd/system-scripts'; import { proxyRpc, ProxyRpcOptions } from './cmd/proxy-rpc'; +import { molFiles, molSingleFile } from './cmd/mol'; +import * as fs from 'fs'; const version = require('../package.json').version; const description = require('../package.json').description; @@ -138,6 +140,21 @@ program return printSystemScripts(exportStyle); }); +program + .command('mol') + .requiredOption('--schema ', 'Specify the scheme .mol file/folders to generate bindings') + .option('--output ', 'Specify the output file/folder path') + .option('--output-folder ', 'Specify the output folder path, only valid when schema is a folder') + .option('--lang ', 'Specify the binding language, [ts, js, c, rs, go]', 'ts') + .description('Generate CKB Moleculec binding code for development') + .action(async (option) => { + if (fs.statSync(option.schema).isDirectory()) { + const outputFolderPath = option.outputFolder ?? './'; + return molFiles(option.schema, outputFolderPath, option.lang); + } + return molSingleFile(option.schema, option.output, option.lang); + }); + program.parse(process.argv); // If no command is specified, display help diff --git a/src/cmd/mol.ts b/src/cmd/mol.ts new file mode 100644 index 0000000..8b746b9 --- /dev/null +++ b/src/cmd/mol.ts @@ -0,0 +1,22 @@ +import { BindingLanguage, generateMolBindings } from '../molecule/mol'; +import fs from 'fs'; +import path from 'path'; + +export async function molSingleFile(schemeFilePath: string, outputFilePath: string, bindingLang: string) { + await generateMolBindings(schemeFilePath, outputFilePath, bindingLang as BindingLanguage); +} + +export async function molFiles(schemaFolderPath: string, outputFolderPath: string, bindingLang: string) { + const files = fs.readdirSync(schemaFolderPath).filter((file) => file.endsWith('.mol')); + + if (files.length === 0) { + throw new Error(`No .mol files found in the specified folder: ${schemaFolderPath}`); + } + + for (const file of files) { + const filePath = path.join(schemaFolderPath, file); + const outputFileName = path.basename(file, '.mol') + '.' + bindingLang; + const outputFilePath = path.join(outputFolderPath, outputFileName); + await molSingleFile(filePath, outputFilePath, bindingLang as BindingLanguage); + } +} diff --git a/src/molecule/mol.ts b/src/molecule/mol.ts new file mode 100644 index 0000000..c1b99fd --- /dev/null +++ b/src/molecule/mol.ts @@ -0,0 +1,119 @@ +import { execSync } from 'child_process'; +import { MoleculecES } from '../tools/moleculec-es'; +import { MoleculecRust } from '../tools/moleculec-rust'; +import { readSettings } from '../cfg/setting'; +import path from 'path'; +import * as fs from 'fs'; +import { MoleculecGo } from '../tools/moleculec-go'; + +export enum BindingLanguage { + rust = 'rs', + c = 'c', + typescript = 'ts', + javascript = 'js', + go = 'go', +} + +export async function generateMolBindings( + schemeFilePath: string, + outputFilePath: string | undefined, + bindingLanguage: BindingLanguage, +) { + await installMolToolsIfNeeded(); + const settings = readSettings(); + if (bindingLanguage === BindingLanguage.typescript) { + const jsonFilePath = path.join(settings.tools.moleculeES.cachePath, 'schema.json'); + fs.mkdirSync(path.dirname(jsonFilePath), { recursive: true }); + if (outputFilePath) { + fs.mkdirSync(path.dirname(outputFilePath), { recursive: true }); + } + + execSync(`moleculec --language - --schema-file ${schemeFilePath} --format json > ${jsonFilePath}`); + execSync( + `${MoleculecES.bin} -generateTypeScriptDefinition -hasBigInt -inputFile ${jsonFilePath} -outputFile ${outputFilePath || '-'}`, + { stdio: 'inherit' }, + ); + return; + } + + if (bindingLanguage === BindingLanguage.javascript) { + const jsonFilePath = path.join(settings.tools.moleculeES.cachePath, 'schema.json'); + fs.mkdirSync(path.dirname(jsonFilePath), { recursive: true }); + if (outputFilePath) { + fs.mkdirSync(path.dirname(outputFilePath), { recursive: true }); + } + + execSync(`moleculec --language - --schema-file ${schemeFilePath} --format json > ${jsonFilePath}`, { + stdio: 'inherit', + }); + execSync(`${MoleculecES.bin} -hasBigInt -inputFile ${jsonFilePath} -outputFile ${outputFilePath || '-'}`, { + stdio: 'inherit', + }); + return; + } + + if (bindingLanguage === BindingLanguage.c) { + if (!outputFilePath) { + execSync(`moleculec --language c --schema-file ${schemeFilePath}`, { + stdio: 'inherit', + }); + return; + } + + fs.mkdirSync(path.dirname(outputFilePath), { recursive: true }); + + execSync(`moleculec --language c --schema-file ${schemeFilePath} > ${outputFilePath}`, { + stdio: 'inherit', + }); + return; + } + + if (bindingLanguage === BindingLanguage.rust) { + if (!outputFilePath) { + execSync(`moleculec --language rust --schema-file ${schemeFilePath}`, { + stdio: 'inherit', + }); + return; + } + + fs.mkdirSync(path.dirname(outputFilePath), { recursive: true }); + + execSync(`moleculec --language rust --schema-file ${schemeFilePath} > ${outputFilePath}`, { + stdio: 'inherit', + }); + return; + } + + if (bindingLanguage === BindingLanguage.go) { + if (!outputFilePath) { + execSync(`moleculec --language go --schema-file ${schemeFilePath}`, { + stdio: 'inherit', + }); + return; + } + + fs.mkdirSync(path.dirname(outputFilePath), { recursive: true }); + + execSync(`moleculec --language go --schema-file ${schemeFilePath} | gofmt > ${outputFilePath}`, { + stdio: 'inherit', + }); + return; + } + + throw new Error(`Unsupported binding language: ${bindingLanguage}`); +} + +export async function installMolToolsIfNeeded() { + if (!MoleculecES.isBinaryInstalled()) { + const version = '0.4.6'; + await MoleculecES.installMoleculeES(version); + } + + if (!MoleculecRust.isBinaryInstalled()) { + MoleculecRust.installBinary(); + } + + if (!MoleculecGo.isBinaryInstalled()) { + MoleculecGo.installBinary(); + } +} diff --git a/src/node/install.ts b/src/node/install.ts index 7c84437..b2ec6d5 100644 --- a/src/node/install.ts +++ b/src/node/install.ts @@ -117,7 +117,7 @@ export function getVersionFromBinary(binPath: string): string | null { } } -export function getOS(): string { +function getOS(): string { const platform = os.platform(); if (platform === 'darwin') { return 'apple-darwin'; @@ -130,7 +130,7 @@ export function getOS(): string { } } -export function getArch(): string { +function getArch(): string { const arch = os.arch(); if (arch === 'x64') { return 'x86_64'; @@ -141,7 +141,7 @@ export function getArch(): string { } } -export function getExtension(): 'tar.gz' | 'zip' { +function getExtension(): 'tar.gz' | 'zip' { const platform = os.platform(); if (platform === 'linux') { return 'tar.gz'; diff --git a/src/tools/moleculec-es.ts b/src/tools/moleculec-es.ts new file mode 100644 index 0000000..5de1c34 --- /dev/null +++ b/src/tools/moleculec-es.ts @@ -0,0 +1,86 @@ +import path from 'path'; +import { readSettings } from '../cfg/setting'; +import { unZipFile } from '../node/install'; +import fs from 'fs'; +import os from 'os'; +import { Request } from '../util/request'; +import { encodeBinPathForTerminal } from '../util/encoding'; + +export class MoleculecES { + static isBinaryInstalled() { + return fs.existsSync(this.binPath); + } + + static async installMoleculeES(version: string) { + const arch = getArch(); + const osname = getOS(); + const fileName = `moleculec-es-v${version}-${osname}-${arch}.tar.gz`; + try { + const tempFilePath = path.join(os.tmpdir(), fileName); + await MoleculecES.downloadAndSaveMoleculeES(version, tempFilePath); + + // Unzip the file + const extractDir = path.join(readSettings().tools.moleculeES.downloadPath, `molecule-es_v${version}`); + await unZipFile(tempFilePath, extractDir, true); + + // Install the extracted files + const sourcePath = path.join(extractDir, 'moleculec-es'); + const targetPath = MoleculecES.binPath; + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.copyFileSync(sourcePath, targetPath); + fs.chmodSync(targetPath, '755'); // Make the binary executable + + console.log(`Molecule-ES ${version} installed successfully.`); + } catch (error) { + console.error('Error installing Molecule-ES:', error); + } + } + + static get binPath() { + return path.join(readSettings().tools.moleculeES.binFolder, 'moleculec-es'); + } + + static get bin() { + return encodeBinPathForTerminal(this.binPath); + } + + static async downloadAndSaveMoleculeES(version: string, tempFilePath: string) { + const downloadURL = MoleculecES.buildDownloadUrl(version); + const response = await Request.get(downloadURL, { + responseType: 'arraybuffer', + }); + fs.writeFileSync(tempFilePath, response.data); + } + + static buildDownloadUrl(version: string): string { + const osName: string = getOS(); + const archName: string = getArch(); + // https://github.com/nervosnetwork/moleculec-es/releases/download/0.4.6/moleculec-es_0.4.6_darwin_arm64.tar.gz + // https://github.com/nervosnetwork/moleculec-es/releases/download/0.4.6/moleculec-es_0.4.6_darwin_amd64.tar.gz + return `https://github.com/nervosnetwork/moleculec-es/releases/download/${version}/moleculec-es_${version}_${osName}_${archName}.tar.gz`; + } +} + +function getOS(): string { + const platform = os.platform(); + if (platform === 'darwin') { + return 'darwin'; + } else if (platform === 'linux') { + return 'linux'; + } else if (platform === 'win32') { + return 'windows'; + } else { + throw new Error('Unsupported operating system'); + } +} + +function getArch(): string { + const arch = os.arch(); + if (arch === 'x64') { + return 'amd64'; + } else if (arch === 'arm64') { + return 'arm64'; + } else { + throw new Error('Unsupported architecture'); + } +} diff --git a/src/tools/moleculec-go.ts b/src/tools/moleculec-go.ts new file mode 100644 index 0000000..c6f4d12 --- /dev/null +++ b/src/tools/moleculec-go.ts @@ -0,0 +1,33 @@ +import { spawnSync, execSync } from 'child_process'; + +export class MoleculecGo { + static isBinaryInstalled() { + const result = spawnSync('moleculec-go', ['--version'], { stdio: 'ignore' }); + return result.status === 0 && this.checkVersion(); + } + + static checkVersion() { + // cmd: moleculec-go --version + // output: Moleculec Plugin 0.1.11 + // check if the version is greater than or equal to 0.1.11 + const result = execSync('moleculec-go --version').toString(); + const version = result.split(' ')[2]; + const versionNumber = version.split('.'); + const major = parseInt(versionNumber[0]); + const minor = parseInt(versionNumber[1]); + const patch = parseInt(versionNumber[2]); + return major > 0 || (major === 0 && (minor > 1 || (minor === 1 && patch >= 11))); + } + + static installBinary() { + const command = `cargo install moleculec-go`; + try { + console.log('Installing ckb-moleculec-go...'); + execSync(command); + console.log('ckb-moleculec-go installed successfully.'); + } catch (error) { + console.error('Failed to install ckb-moleculec-go:', error); + process.exit(1); + } + } +} diff --git a/src/tools/moleculec-rust.ts b/src/tools/moleculec-rust.ts new file mode 100644 index 0000000..3c0400e --- /dev/null +++ b/src/tools/moleculec-rust.ts @@ -0,0 +1,33 @@ +import { spawnSync, execSync } from 'child_process'; + +export class MoleculecRust { + static isBinaryInstalled() { + const result = spawnSync('moleculec', ['--version'], { stdio: 'ignore' }); + return result.status === 0 && this.checkVersion(); + } + + static checkVersion() { + // cmd: moleculec --version + // output: Moleculec 0.8.0 + // check if the version is greater than or equal to 0.8.0 + const result = execSync('moleculec --version').toString(); + const version = result.split(' ')[1]; + const versionNumber = version.split('.'); + const major = parseInt(versionNumber[0]); + const minor = parseInt(versionNumber[1]); + const patch = parseInt(versionNumber[2]); + return major > 0 || (major === 0 && (minor > 8 || (minor === 8 && patch >= 0))); + } + + static installBinary() { + const command = `cargo install moleculec`; + try { + console.log('Installing ckb-moleculec...'); + execSync(command); + console.log('ckb-moleculec installed successfully.'); + } catch (error) { + console.error('Failed to install ckb-moleculec:', error); + process.exit(1); + } + } +}