From d147dc77f06863c178d2d06eac0a9c6a48100402 Mon Sep 17 00:00:00 2001 From: Carlos Pereira Atencio Date: Tue, 27 Feb 2024 21:47:19 +0000 Subject: [PATCH] cli: Add command line interface to combine/split uhex files. --- config/rollup.config.js | 19 ++++- package-lock.json | 29 ++++++- package.json | 8 +- src/cli.ts | 163 ++++++++++++++++++++++++++++++++++++++++ src/universal-hex.ts | 3 + tslint.json | 1 + 6 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 src/cli.ts diff --git a/config/rollup.config.js b/config/rollup.config.js index 212b5a5..05b227c 100644 --- a/config/rollup.config.js +++ b/config/rollup.config.js @@ -1,4 +1,5 @@ import { resolve } from 'path'; +import builtins from 'builtin-modules'; import sourceMaps from 'rollup-plugin-sourcemaps'; import nodeResolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; @@ -28,7 +29,7 @@ const plugins = /** @type {Plugin[]} */ ([ commonjs(), // Allow node_modules resolution. Use 'external' to control // which external modules to include in the bundle - // https://github.com/rollup/rollup-plugin-node-resolve#usage + // https://github.com/rollup/plugins/blob/master/packages/node-resolve/ nodeResolve(), sourceMaps(), babel({ @@ -99,4 +100,18 @@ const umdConfigMin = createUmdConfig({ ], }); -export default [umdConfig, umdConfigMin]; +const cjsConfigCli = { + inlineDynamicImports: true, + external: [...external, ...Object.keys(pkg.dependencies), ...builtins], + input: resolve(dist, 'esm5', 'cli.js'), + output: { + banner: '#!/usr/bin/env node', + file: pkg.bin.uhex, + format: 'cjs', + name: pkg.config.umdName, + sourcemap: true, + }, + plugins: [commonjs(), nodeResolve(), sourceMaps()], +}; + +export default [umdConfig, umdConfigMin, cjsConfigCli]; diff --git a/package-lock.json b/package-lock.json index dc218e8..985d641 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,12 @@ "name": "@microbit/microbit-universal-hex", "version": "0.2.2", "license": "MIT", + "dependencies": { + "commander": "^12.0.0" + }, + "bin": { + "uhex": "dist/bundles/microbit-uh-cli.umd.js" + }, "devDependencies": { "@babel/core": "^7.23.9", "@babel/polyfill": "^7.12.1", @@ -3542,10 +3548,12 @@ } }, "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "engines": { + "node": ">=18" + } }, "node_modules/commondir": { "version": "1.0.1", @@ -7582,6 +7590,13 @@ "node": ">=0.10.0" } }, + "node_modules/tslint-config-standard/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "peer": true + }, "node_modules/tslint-config-standard/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -7788,6 +7803,12 @@ "node": ">=0.10.0" } }, + "node_modules/tslint/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/tslint/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", diff --git a/package.json b/package.json index 7b9a706..27924ac 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "universal hex", "uh" ], + "bin": { + "uhex": "./dist/bundles/microbit-uh-cli.cjs.js" + }, "main": "./dist/bundles/microbit-uh.umd.js", "mainMin": "./dist/bundles/microbit-uh.umd.min.js", "module": "./dist/esm5/index.js", @@ -46,6 +49,9 @@ "lint:fix": "npm run lint -- --fix", "docs": "typedoc --options config/typedoc.json" }, + "dependencies": { + "commander": "^12.0.0" + }, "devDependencies": { "@babel/core": "^7.23.9", "@babel/polyfill": "^7.12.1", @@ -72,4 +78,4 @@ "typedoc-neo-theme": "^1.1.1", "typescript": "^4.9.5" } -} +} \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..f427182 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,163 @@ +import * as fs from 'fs'; +import { sep } from 'path'; +import * as process from 'process'; +import { Command } from 'commander'; +import * as microbitUh from './universal-hex'; + +function combine( + v1IntelHexPath: string, + v2IntelHexPath: string, + universalHexPath: string | undefined, + overwrite: boolean | undefined +) { + console.log('Combining Intel Hex files into Universal Hex'); + console.log(`V1 Intel hex file: ${fs.realpathSync(v1IntelHexPath)}`); + console.log(`V2 Intel hex file: ${fs.realpathSync(v2IntelHexPath)}`); + + if (!universalHexPath) { + // If the output path is not specified, save it in the current working directory + universalHexPath = `${process.cwd()}${sep}universal.hex`; + } + if (!overwrite && fs.existsSync(universalHexPath)) { + throw new Error( + `Output file already exists: ${fs.realpathSync(universalHexPath)}\n` + + '\tUse "--overwrite" flag to replace it.' + ); + } + + const v1IntelHexStr = fs.readFileSync(v1IntelHexPath, 'ascii'); + const v2IntelHexStr = fs.readFileSync(v2IntelHexPath, 'ascii'); + const universalHexStr = microbitUh.createUniversalHex([ + { + hex: v1IntelHexStr, + boardId: microbitUh.microbitBoardId.V1, + }, + { + hex: v2IntelHexStr, + boardId: microbitUh.microbitBoardId.V2, + }, + ]); + fs.writeFileSync(universalHexPath, universalHexStr, { encoding: 'ascii' }); + + console.log(`Universal Hex saved to: ${fs.realpathSync(universalHexPath)}`); +} + +function separate( + universalHexPath: string, + v1IntelHexPath: string | undefined, + v2IntelHexPath: string | undefined, + overwrite: boolean | undefined +) { + console.log( + `Splitting Universal Hex file: ${fs.realpathSync(universalHexPath)}` + ); + if (!v1IntelHexPath) { + v1IntelHexPath = `${process.cwd()}${sep}v1-intel.hex`; + } + if (!v2IntelHexPath) { + v2IntelHexPath = `${process.cwd()}${sep}v2-intel.hex`; + } + if (!overwrite && fs.existsSync(v1IntelHexPath)) { + throw new Error( + `Output V1 file already exists: ${fs.realpathSync(v1IntelHexPath)}\n` + + '\tUse "--overwrite" flag to replace it.' + ); + } + if (!overwrite && fs.existsSync(v2IntelHexPath)) { + throw new Error( + `Output V2 file already exists: ${fs.realpathSync(v2IntelHexPath)}\n` + + '\tUse "--overwrite" flag to replace it.' + ); + } + + const universalHexStr = fs.readFileSync(universalHexPath, 'ascii'); + const separatedHexes = microbitUh.separateUniversalHex(universalHexStr); + if (separatedHexes.length !== 2) { + const boardIds = separatedHexes.map((hexObj) => hexObj.boardId); + const errorMsg = + 'Universal Hex should contain only two micro:bit Intel Hexes.\n' + + `Found ${separatedHexes.length}: ${boardIds.join(', ')}`; + throw new Error(errorMsg); + } + + let intelHexV1Str = ''; + let intelHexV2Str = ''; + separatedHexes.forEach((hexObj) => { + if (microbitUh.V1_BOARD_IDS.includes(hexObj.boardId)) { + intelHexV1Str = hexObj.hex; + } else if (microbitUh.V2_BOARD_IDS.includes(hexObj.boardId)) { + intelHexV2Str = hexObj.hex; + } + }); + if (!intelHexV1Str || !intelHexV2Str) { + const boardIds = separatedHexes.map((hexObj) => hexObj.boardId); + const errorMsg = + 'Universal Hex does not contain both micro:bit Intel Hexes.\n' + + `Found hexes for following board IDs: ${boardIds.join(', ')}`; + throw new Error(errorMsg); + } + fs.writeFileSync(v1IntelHexPath, intelHexV1Str, { encoding: 'ascii' }); + fs.writeFileSync(v2IntelHexPath, intelHexV2Str, { encoding: 'ascii' }); + + console.log(`V1 Intel Hex saved to: ${fs.realpathSync(v1IntelHexPath)}`); + console.log(`V2 Intel Hex saved to: ${fs.realpathSync(v2IntelHexPath)}`); +} + +function cli(args: string[]): number { + const uHexCli = new Command(); + + uHexCli + .command('combine') + .requiredOption('-v1, --v1 ', 'Path to micro:bit V1 input Intel Hex') + .requiredOption('-v2, --v2 ', 'Path to micro:bit V2 input Intel Hex') + .option('-u, --universal ', 'Path to output Universal Hex') + .option('-o, --overwrite', 'Overwrite output file if it exists', false) + .action( + (options: { + v1: string; + v2: string; + universal?: string; + overwrite: boolean; + }) => { + try { + combine(options.v1, options.v2, options.universal, options.overwrite); + } catch (e) { + console.error('Error:', e.message); + process.exit(1); + } + } + ); + + uHexCli + .command('split') + .requiredOption('-u, --universal ', 'Path to input Universal Hex') + .option('-v1, --v1 ', 'Path to micro:bit V1 output Intel Hex') + .option('-v2, --v2 ', 'Path to micro:bit V2 output Intel Hex') + .option('-o, --overwrite', 'Overwrite output files if they exist', false) + .action( + (options: { + v1?: string; + v2?: string; + universal: string; + overwrite: boolean; + }) => { + try { + separate( + options.universal, + options.v1, + options.v2, + options.overwrite + ); + } catch (e) { + console.error('Error:', e.message); + process.exit(1); + } + } + ); + + uHexCli.parse(args); + + return 0; +} + +process.exit(cli(process.argv)); diff --git a/src/universal-hex.ts b/src/universal-hex.ts index b1772cc..0fcc985 100644 --- a/src/universal-hex.ts +++ b/src/universal-hex.ts @@ -19,6 +19,7 @@ import * as ihex from './ihex'; const V1_BOARD_IDS = [0x9900, 0x9901]; +const V2_BOARD_IDS = [0x9903, 0x9904, 0x9905, 0x9906]; const BLOCK_SIZE = 512; /** @@ -490,6 +491,8 @@ function separateUniversalHex(universalHexStr: string): IndividualHex[] { } export { + V1_BOARD_IDS, + V2_BOARD_IDS, microbitBoardId, IndividualHex, iHexToCustomFormatBlocks, diff --git a/tslint.json b/tslint.json index 7f976b8..6b483b4 100644 --- a/tslint.json +++ b/tslint.json @@ -16,6 +16,7 @@ }, "object-literal-sort-keys": false, "interface-over-type-literal": false, + "no-console": false, "no-bitwise": false }, "jsRules": true