UNPKG

@graphprotocol/graph-cli

Version:

CLI for building for and deploying to The Graph

322 lines (279 loc) • 10.8 kB
const { Binary } = require('binary-install-raw') const os = require('os') const chalk = require('chalk') const fetch = require('node-fetch') const { filesystem, patching, print, system } = require('gluegun') const { fixParameters } = require('../command-helpers/gluegun') const path = require('path') const semver = require('semver') const { spawn, exec } = require('child_process') const yaml = require('js-yaml') const HELP = ` ${chalk.bold('graph test')} ${chalk.dim('[options]')} ${chalk.bold('<datasource>')} ${chalk.dim('Options:')} -c, --coverage Run the tests in coverage mode -d, --docker Run the tests in a docker container(Note: Please execute from the root folder of the subgraph) -f --force Binary - overwrites folder + file when downloading. Docker - rebuilds the docker image -h, --help Show usage information -l, --logs Logs to the console information about the OS, CPU model and download url (debugging purposes) -r, --recompile Force-recompile tests -v, --version <tag> Choose the version of the rust binary that you want to be downloaded/used ` module.exports = { description: 'Runs rust binary for subgraph testing', run: async toolbox => { // Read CLI parameters let { c, coverage, d, docker, f, force, h, help, l, logs, r, recompile, v, version, } = toolbox.parameters.options let opts = {} // Support both long and short option variants opts.coverage = coverage || c opts.docker = docker || d opts.force = force || f opts.help = help || h opts.logs = logs || l opts.recompile = recompile || r opts.version = version || v opts.testsFolder = './tests' // Fix if a boolean flag (e.g -c, --coverage) has an argument try { fixParameters(toolbox.parameters, { h, help, c, coverage, d, docker, f, force, l, logs, r, recompile }) } catch (e) { print.error(e.message) process.exitCode = 1 return } let datasource = toolbox.parameters.first || toolbox.parameters.array[0] // Show help text if requested if (opts.help) { print.info(HELP) return } // Check if matchstick.yaml config exists if(filesystem.exists('matchstick.yaml')) { try { // Load the config let config = await yaml.load(filesystem.read('matchstick.yaml', 'utf8')) // Check if matchstick.yaml and testsFolder not null if(config && config.testsFolder) { // assign test folder from matchstick.yaml if present opts.testsFolder = config.testsFolder } } catch (error) { print.info('A problem occurred while reading matchstick.yaml. Please attend to the errors below:') print.error(error.message) process.exit(1) } } opts.cachePath = path.join(opts.testsFolder, '.latest.json') setVersionFromCache(opts) // Fetch the latest version tag if version is not specified with -v/--version or if the version is not cached if (opts.force || (!opts.version && !opts.latestVersion)) { print.info("Fetching latest version tag") let result = await fetch('https://api.github.com/repos/LimeChain/matchstick/releases/latest') let json = await result.json() opts.latestVersion = json.tag_name filesystem.file(opts.cachePath, { content: { version: json.tag_name, timestamp: Date.now() } }) } if(opts.docker) { runDocker(datasource, opts) } else { runBinary(datasource, opts) } } } async function setVersionFromCache(opts) { if(filesystem.exists(opts.cachePath) == 'file') { let cached = filesystem.read(opts.cachePath, 'json') // Get the cache age in days let cacheAge = (Date.now() - cached.timestamp) / (1000 * 60 * 60 * 24) // If cache age is less than 1 day, use the cached version if (cacheAge < 1) { opts.latestVersion = cached.version } } } async function runBinary(datasource, opts) { let coverageOpt = opts.coverage let forceOpt = opts.force let logsOpt = opts.logs let versionOpt = opts.version let latestVersion = opts.latestVersion let recompileOpt = opts.recompile const platform = await getPlatform(logsOpt) const url = `https://github.com/LimeChain/matchstick/releases/download/${versionOpt || latestVersion}/${platform}` if (logsOpt) { print.info(`Download link: ${url}`) } let binary = new Binary(platform, url, versionOpt || latestVersion) forceOpt ? await binary.install(true) : await binary.install(false) let args = new Array() if (coverageOpt) args.push('-c') if (recompileOpt) args.push('-r') if (datasource) args.push(datasource) args.length > 0 ? binary.run(...args) : binary.run() } async function getPlatform(logsOpt) { const type = os.type() const arch = os.arch() const cpuCore = os.cpus()[0] const isAppleSilicon = (arch === 'arm64' && /Apple (M1|M2|processor)/.test(cpuCore.model)) const linuxInfo = type === 'Linux' ? await getLinuxInfo() : {} const linuxDistro = linuxInfo.name const release = linuxInfo.version || os.release() const majorVersion = parseInt(linuxInfo.version, 10) || semver.major(release) if (logsOpt) { print.info(`OS type: ${linuxDistro || type}\nOS arch: ${arch}\nOS release: ${release}\nOS major version: ${majorVersion}\nCPU model: ${cpuCore.model}`) } if (arch === 'x64' || isAppleSilicon) { if (type === 'Darwin') { if (majorVersion === 18 || majorVersion === 19) { return 'binary-macos-10.15' // GitHub dropped support for macOS 10.14 in Actions, but it seems 10.15 binary works on 10.14 too } else if (isAppleSilicon) { return 'binary-macos-11-m1' } return 'binary-macos-11' } else if (type === 'Linux') { if (majorVersion === 18) { return 'binary-linux-18' } if (majorVersion === 22) { return 'binary-linux-22' } else { return 'binary-linux-20' } } } throw new Error(`Unsupported platform: ${type} ${arch} ${majorVersion}`) } async function getLinuxInfo() { try { let result = await system.run("cat /etc/*-release | grep -E '(^VERSION|^NAME)='", {trim: true}) let infoArray = result.replace(/['"]+/g, '').split('\n').map(p => p.split('=')) let linuxInfo = {} infoArray.forEach((val) => { linuxInfo[val[0].toLowerCase()] = val[1] }); return linuxInfo } catch (error) { print.error(`Error fetching the Linux version:\n ${error}`) process.exit(1) } } async function runDocker(datasource, opts) { let coverageOpt = opts.coverage let forceOpt = opts.force let versionOpt = opts.version let latestVersion = opts.latestVersion let recompileOpt = opts.recompile // Remove binary-install-raw binaries, because docker has permission issues // when building the docker images await filesystem.remove('./node_modules/binary-install-raw/bin') // Get current working directory let current_folder = await filesystem.cwd() // Declate dockerfilePath with default location let dockerfilePath = path.join(opts.testsFolder || 'tests', '.docker/Dockerfile') // Check if the Dockerfil already exists let dockerfileExists = filesystem.exists(dockerfilePath) // Generate the Dockerfile only if it doesn't exists, // version flag and/or force flag is passed. if(!dockerfileExists || versionOpt || forceOpt) { await dockerfile(dockerfilePath, versionOpt, latestVersion) } // Run a command to check if matchstick image already exists exec('docker images -q matchstick', (error, stdout, stderr) => { // Collect all(if any) flags and options that have to be passed to the matchstick binary let testArgs = '' if (coverageOpt) testArgs = testArgs + ' -c' if (recompileOpt) testArgs = testArgs + ' -r' if (datasource) testArgs = testArgs + ' ' + datasource // Build the `docker run` command options and flags let dockerRunOpts = ['run', '-it', '--rm', '--mount', `type=bind,source=${current_folder},target=/matchstick`] if(testArgs !== '') { dockerRunOpts.push('-e') dockerRunOpts.push(`ARGS=${testArgs.trim()}`) } dockerRunOpts.push('matchstick') // If a matchstick image does not exists, the command returns an empty string, // else it'll return the image ID. Skip `docker build` if an image already exists // Delete current image(if any) and rebuild. // Use spawn() and {stdio: 'inherit'} so we can see the logs in real time. if(!dockerfileExists || stdout === '' || versionOpt || forceOpt) { if (stdout !== '') { exec('docker image rm matchstick', (error, stdout, stderr) => { print.info(chalk.bold(`Removing matchstick image\n${stdout}`)) }) } // Build a docker image. If the process has executed successfully // run a container from that image. spawn( 'docker', ['build', '-f', dockerfilePath, '-t', 'matchstick', '.'], { stdio: 'inherit' } ).on('close', code => { if (code === 0) { spawn('docker', dockerRunOpts, { stdio: 'inherit' }) } }) } else { print.info("Docker image already exists. Skipping `docker build` command.") // Run the container from the existing matchstick docker image spawn('docker', dockerRunOpts, { stdio: 'inherit' }) } }) } // Downloads Dockerfile template from the demo-subgraph repo // Replaces the placeholders with their respective values async function dockerfile(dockerfilePath, versionOpt, latestVersion) { let spinner = print.spin("Generating Dockerfile...") try { // Fetch the Dockerfile template content from the demo-subgraph repo let content = await fetch('https://raw.githubusercontent.com/LimeChain/demo-subgraph/main/Dockerfile') .then((response) => { if (response.ok) { return response.text() } else { throw new Error(`Status Code: ${response.status}, with error: ${response.statusText}`); } }) // Write the Dockerfile await filesystem.write(dockerfilePath, content) // Replaces the version placeholders await patching.replace(dockerfilePath, '<MATCHSTICK_VERSION>', versionOpt || latestVersion) } catch (error) { spinner.fail(`A problem occurred while generating the Dockerfile. Please attend to the errors below:\n ${error.message}`) process.exit(1) } spinner.succeed('Successfully generated Dockerfile.') }