packages/ros-cdk-cli/lib/api/exec.ts (155 lines of code) (raw):

import * as cxschema from '@alicloud/ros-cdk-assembly-schema'; import * as cxapi from '@alicloud/ros-cdk-cxapi'; import * as childProcess from 'child_process'; import * as fs from 'fs-extra'; import * as path from 'path'; import { debug } from '../logging'; import { Configuration, PROJECT_CONFIG, USER_DEFAULTS } from '../settings'; import { versionNumber } from '../version'; import * as crypto from 'crypto'; /** Invokes the cloud executable and returns JSON output */ export async function execProgram(config: Configuration): Promise<cxapi.CloudAssembly> { const env: { [key: string]: string } = {}; const context = config.context.all; debug('context:', context); env[cxapi.CONTEXT_ENV] = JSON.stringify(context); const app = config.settings.get(['app']); if (!app) { throw new Error(`--app is required either in command-line, in ${PROJECT_CONFIG} or in ${USER_DEFAULTS}`); } const commandLine = await guessExecutable(appToArray(app)); const outdir = config.settings.get(['output']); if (!outdir) { throw new Error('unexpected: --output is required'); } await fs.mkdirp(outdir); debug('outdir:', outdir); env[cxapi.OUTDIR_ENV] = outdir; // Send version information env[cxapi.CLI_ASM_VERSION_ENV] = cxschema.Manifest.version(); env[cxapi.CLI_VERSION_ENV] = versionNumber(); debug('env:', env); if(ifModify()) { await exec(); } return createAssembly(outdir); function createAssembly(appDir: string) { try { return new cxapi.CloudAssembly(appDir); } catch (error) { if (error.message.includes(cxschema.VERSION_MISMATCH)) { // this means the CLI version is too old. // we instruct the user to upgrade. throw new Error( `This CDK CLI is not compatible with the CDK library used by your application. Please upgrade the CLI to the latest version.\n(${error.message})`, ); } throw error; } } async function ifModify() { const curPath = path.resolve('./'); const md5Path = curPath + '/.md5'; let preMd5: string; preMd5 = fs.existsSync(md5Path) ? fs.readFileSync(md5Path).toString() : ''; let pathAndMd5: string[] = []; let hasher = crypto.createHash('md5'); let pkgInfoPath; if(fs.existsSync('./tsconfig.json') || fs.existsSync('./package.json')) { // when using ts or js to synth pkgInfoPath = './package.json'; const binDir = curPath + '/bin'; const libDir = curPath + '/lib'; await readDirForMd5(binDir); await readDirForMd5(libDir); } else if(fs.existsSync('./requirements.txt')) { // when using python to synth pkgInfoPath = './requirements.txt'; await readDirForMd5(curPath); } else if (fs.existsSync('./pom.xml')) { // when using java to synth pkgInfoPath = './pom.xml'; const srcDir = curPath + '/src'; await readDirForMd5(srcDir); } else if(fs.existsSync('./global.sln')) { // when using c# to synth pkgInfoPath = './global.sln'; const srcDir = curPath + '/src'; await readDirForMd5(srcDir); } else if (fs.existsSync('./go.mod')) { // when using go to synth pkgInfoPath = './go.mod'; await readDirForMd5(curPath); } else { throw new Error('This CDK CLI init project is not allowed, please check project directory information'); } pathAndMd5.sort(); for(let data of pathAndMd5) { hasher.update(data); } // add pkg info to generate md5 if (pkgInfoPath && fs.existsSync(pkgInfoPath)) { let pkg = fs.readFileSync(pkgInfoPath); hasher.update(pkg); } const curMd5 = hasher.digest('hex'); fs.writeFileSync(md5Path, curMd5); return curMd5 !== preMd5; async function readDirForMd5(path: string) { let arr = fs.readdirSync(path); for(let i in arr){ let stats = fs.statSync(path + '/' + arr[i]); if(stats.isFile()){ let filePath = path + '/' + arr[i]; let fileContent = fs.readFileSync(filePath); let fileMd5 = crypto.createHash('md5').update(fileContent).digest('hex'); pathAndMd5.push(filePath + ':' + fileMd5); }else{ readDirForMd5(path + '/' + arr[i]); } } } } async function exec() { return new Promise<string | void>((ok, fail) => { // We use a slightly lower-level interface to: // // - Pass arguments in an array instead of a string, to get around a // number of quoting issues introduced by the intermediate shell layer // (which would be different between Linux and Windows). // // - Inherit stderr from controlling terminal. We don't use the captured value // anyway, and if the subprocess is printing to it for debugging purposes the // user gets to see it sooner. Plus, capturing doesn't interact nicely with some // processes like Maven. const proc = childProcess.spawn(commandLine[0], commandLine.slice(1), { stdio: ['ignore', 'inherit', 'inherit'], detached: false, shell: true, env: { ...process.env, ...env, }, }); proc.on('error', fail); proc.on('exit', (code) => { if (code === 0) { return ok(); } else { return fail(new Error(`Subprocess exited with error ${code}`)); } }); }); } } /** * Make sure the 'app' is an array * * If it's a string, split on spaces as a trivial way of tokenizing the command line. */ function appToArray(app: any) { return typeof app === 'string' ? app.split(' ') : app; } type CommandGenerator = (file: string) => string[]; /** * Execute the given file with the same 'node' process as is running the current process */ function executeNode(scriptFile: string): string[] { return [process.execPath, scriptFile]; } /** * Mapping of extensions to command-line generators */ const EXTENSION_MAP = new Map<string, CommandGenerator>([['.js', executeNode]]); /** * Guess the executable from the command-line argument * * Only do this if the file is NOT marked as executable. If it is, * we'll defer to the shebang inside the file itself. * * If we're on Windows, we ALWAYS take the handler, since it's hard to * verify if registry associations have or have not been set up for this * file type, so we'll assume the worst and take control. */ async function guessExecutable(commandLine: string[]) { if (commandLine.length === 1) { let fstat; try { fstat = await fs.stat(commandLine[0]); } catch (error) { debug(`Not a file: '${commandLine[0]}'. Using '${commandLine}' as command-line`); return commandLine; } // tslint:disable-next-line:no-bitwise const isExecutable = (fstat.mode & fs.constants.X_OK) !== 0; const isWindows = process.platform === 'win32'; const handler = EXTENSION_MAP.get(path.extname(commandLine[0])); if (handler && (!isExecutable || isWindows)) { return handler(commandLine[0]); } } return commandLine; }