import fs from 'fs'; import path from 'path'; import tmp from 'tmp'; import { logger } from './logger'; const { spawn } = require('child_process'); const dataDir = process.env['DATA_DIR'] || path.join(__dirname, 'tmp'); const JOB_TIMEOUT = 10000; export class CompilerError extends Error { constructor(message: string) { super(message); } } export class LigoCompiler { private ligoCmd = process.env['LIGO_CMD'] || [ 'docker', 'run', '-t', '--rm', '-v', `${dataDir}:${dataDir}`, '-w', dataDir, 'ligolang/ligo:next' ]; private execPromise(cmd: string | string[], args: string[]): Promise { let command: string[] = []; if (Array.isArray(cmd)) { command = cmd; } else { command = cmd.split(' '); } let program = command[0]; const argument = [...command.slice(1), ...args]; return new Promise((resolve, reject) => { try { const result = spawn(program, argument, { shell: false, cwd: dataDir }); let finalResult = ''; let finalError = ''; result.stdout.on('data', (data: Buffer) => { finalResult += data.toString(); }); result.stderr.on('data', (data: Buffer) => { finalError += data.toString(); }); result.on('close', (code: any) => { if (code === 0) { resolve(finalResult); } else { reject(new CompilerError(finalError)); } }); } catch (ex) { logger.error(`Unexpected compiler error ${ex}`); reject(ex); } setTimeout(() => { reject(new Error(`command: ${cmd} Timed out after ${JOB_TIMEOUT} ms`)); }, JOB_TIMEOUT); }); } private createTemporaryFile(fileContent: string) { return new Promise<{ name: string; remove: () => void }>( (resolve, reject) => { tmp.file( { dir: dataDir, postfix: '.ligo' }, (err, name, fd, remove) => { if (err) { reject(err); return; } fs.write(fd, Buffer.from(fileContent), err => { if (err) { reject(err); return; } resolve({ name, remove: () => { try { remove(); } catch (ex) { logger.error(`Unable to remove file ${name}`); } const ppFile = name.replace('.ligo', '.pp.ligo'); try { if (fs.existsSync(ppFile)) { fs.unlinkSync(ppFile); } } catch (ex) { logger.error(`Unable to remove file ${ppFile}`); } } }); }); } ); } ); } async compileContract( syntax: string, code: string, entrypoint: string, format: string ) { const { name, remove } = await this.createTemporaryFile(code); try { const result = await this.execPromise(this.ligoCmd, [ 'compile-contract', '--michelson-format', format, '-s', syntax, name, entrypoint ]); return result; } finally { remove(); } } async compileExpression(syntax: string, expression: string, format: string) { const result = await this.execPromise(this.ligoCmd, [ 'compile-expression', '--michelson-format', format, syntax, expression ]); return result; } async compileStorage( syntax: string, code: string, entrypoint: string, format: string, storage: string ) { const { name, remove } = await this.createTemporaryFile(code); try { const result = await this.execPromise(this.ligoCmd, [ 'compile-storage', '--michelson-format', format, '-s', syntax, name, entrypoint, storage ]); return result; } finally { remove(); } } async dryRun( syntax: string, code: string, entrypoint: string, parameter: string, storage: string ) { const { name, remove } = await this.createTemporaryFile(code); try { const result = await this.execPromise(this.ligoCmd, [ 'dry-run', '-s', syntax, name, entrypoint, parameter, storage ]); return result; } finally { remove(); } } async evaluateValue(syntax: string, code: string, entrypoint: string) { const { name, remove } = await this.createTemporaryFile(code); try { const result = await this.execPromise(this.ligoCmd, [ 'evaluate-value', '-s', syntax, name, entrypoint ]); return result; } finally { remove(); } } async runFunction( syntax: string, code: string, entrypoint: string, parameter: string ) { const { name, remove } = await this.createTemporaryFile(code); try { const result = await this.execPromise(this.ligoCmd, [ 'run-function', '-s', syntax, name, entrypoint, parameter ]); return result; } finally { remove(); } } }