text: const os = require('os')
  const fs = require('fs')
  const path = require('path')
  const core = require('@actions/core')
  const exec = require('@actions/exec')
  const cache = require('@actions/cache')
  const common = require('./common')
  
  const windows = common.windows
  
  const inputDefaults = {
    'ruby-version': 'default',
    'bundler': 'default',
    'bundler-cache': 'true',
    'working-directory': '.',
  }
  
  // entry point when this action is run on its own
  export async function run() {
    try {
      await setupRuby()
    } catch (error) {
      core.setFailed(error.message)
    }
  }
  
  // entry point when this action is run from other actions
  export async function setupRuby(options = {}) {
    const inputs = { ...options }
    for (const key in inputDefaults) {
      if (!Object.prototype.hasOwnProperty.call(inputs, key)) {
        inputs[key] = core.getInput(key) || inputDefaults[key]
      }
    }
  
    process.chdir(inputs['working-directory'])
  
    const platform = common.getVirtualEnvironmentName()
    const [engine, parsedVersion] = parseRubyEngineAndVersion(inputs['ruby-version'])
  
    let installer
    if (platform.startsWith('windows-') && engine === 'ruby') {
      installer = require('./windows')
    } else {
      installer = require('./ruby-builder')
    }
  
    const engineVersions = installer.getAvailableVersions(platform, engine)
    const version = validateRubyEngineAndVersion(platform, engineVersions, engine, parsedVersion)
  
    createGemRC()
    envPreInstall()
  
    const rubyPrefix = await installer.install(platform, engine, version)
  
    // When setup-ruby is used by other actions, this allows code in them to run
    // before 'bundle install'.  Installed dependencies may require additional
    // libraries & headers, build tools, etc.
    if (inputs['afterSetupPathHook'] instanceof Function) {
      await inputs['afterSetupPathHook']({ platform, rubyPrefix, engine, version })
    }
  
    if (inputs['bundler'] !== 'none') {
      const [gemfile, lockFile] = detectGemfiles()
  
      const bundlerVersion = await common.measure('Installing Bundler', async () =>
        installBundler(inputs['bundler'], lockFile, platform, rubyPrefix, engine, version))
  
      if (inputs['bundler-cache'] === 'true') {
        await common.measure('bundle install', async () =>
            bundleInstall(gemfile, lockFile, platform, engine, version, bundlerVersion))
      }
    }
  
    core.setOutput('ruby-prefix', rubyPrefix)
  }
  
  // The returned gemfile is guaranteed to exist, the lockfile might not exist
  function detectGemfiles() {
    const gemfilePath = process.env['BUNDLE_GEMFILE'] || 'Gemfile'
    if (fs.existsSync(gemfilePath)) {
      return [gemfilePath, `${gemfilePath}.lock`]
    }
  
    if (fs.existsSync("gems.rb")) {
      return ["gems.rb", "gems.locked"]
    }
  
    return [null, null]
  }
  
  function parseRubyEngineAndVersion(rubyVersion) {
    if (rubyVersion === 'default') {
      if (fs.existsSync('.ruby-version')) {
        rubyVersion = '.ruby-version'
      } else if (fs.existsSync('.tool-versions')) {
        rubyVersion = '.tool-versions'
      } else {
        throw new Error('input ruby-version needs to be specified if no .ruby-version or .tool-versions file exists')
      }
    }
  
    if (rubyVersion === '.ruby-version') { // Read from .ruby-version
      rubyVersion = fs.readFileSync('.ruby-version', 'utf8').trim()
      console.log(`Using ${rubyVersion} as input from file .ruby-version`)
    } else if (rubyVersion === '.tool-versions') { // Read from .tool-versions
      const toolVersions = fs.readFileSync('.tool-versions', 'utf8').trim()
      const rubyLine = toolVersions.split(/\r?\n/).filter(e => e.match(/^ruby\s/))[0]
      rubyVersion = rubyLine.match(/^ruby\s+(.+)$/)[1]
      console.log(`Using ${rubyVersion} as input from file .tool-versions`)
    }
  
    let engine, version
    if (rubyVersion.match(/^(\d+)/) || common.isHeadVersion(rubyVersion)) { // X.Y.Z => ruby-X.Y.Z
      engine = 'ruby'
      version = rubyVersion
    } else if (!rubyVersion.includes('-')) { // myruby -> myruby-stableVersion
      engine = rubyVersion
      version = '' // Let the logic in validateRubyEngineAndVersion() find the version
    } else { // engine-X.Y.Z
      [engine, version] = common.partition(rubyVersion, '-')
    }
  
    return [engine, version]
  }
  
  function validateRubyEngineAndVersion(platform, engineVersions, engine, parsedVersion) {
    if (!engineVersions) {
      throw new Error(`Unknown engine ${engine} on ${platform}`)
    }
  
    let version = parsedVersion
    if (!engineVersions.includes(parsedVersion)) {
      const latestToFirstVersion = engineVersions.slice().reverse()
      // Try to match stable versions first, so an empty version (engine-only) matches the latest stable version
      let found = latestToFirstVersion.find(v => common.isStableVersion(v) && v.startsWith(parsedVersion))
      if (!found) {
        // Exclude head versions, they must be exact matches
        found = latestToFirstVersion.find(v => !common.isHeadVersion(v) && v.startsWith(parsedVersion))
      }
  
      if (found) {
        version = found
      } else {
        throw new Error(`Unknown version ${parsedVersion} for ${engine} on ${platform}
          available versions for ${engine} on ${platform}: ${engineVersions.join(', ')}
          File an issue at https://github.com/ruby/setup-ruby/issues if would like support for a new version`)
      }
    }
  
    return version
  }
  
  function createGemRC() {
    const gemrc = path.join(os.homedir(), '.gemrc')
    if (!fs.existsSync(gemrc)) {
      fs.writeFileSync(gemrc, `gem: --no-document${os.EOL}`)
    }
  }
  
  // sets up ENV variables
  // currently only used on Windows runners
  function envPreInstall() {
    const ENV = process.env
    if (windows) {
      // puts normal Ruby temp folder on SSD
      core.exportVariable('TMPDIR', ENV['RUNNER_TEMP'])
      // bash - sets home to match native windows, normally C:\Users\<user name>
      core.exportVariable('HOME', ENV['HOMEDRIVE'] + ENV['HOMEPATH'])
      // bash - needed to maintain Path from Windows
      core.exportVariable('MSYS2_PATH_TYPE', 'inherit')
    }
  }
  
  function readBundledWithFromGemfileLock(lockFile) {
    if (lockFile !== null && fs.existsSync(lockFile)) {
      const contents = fs.readFileSync(lockFile, 'utf8')
      const lines = contents.split(/\r?\n/)
      const bundledWithLine = lines.findIndex(line => /^BUNDLED WITH$/.test(line.trim()))
      if (bundledWithLine !== -1) {
        const nextLine = lines[bundledWithLine+1]
        if (nextLine && /^\d+/.test(nextLine.trim())) {
          const bundlerVersion = nextLine.trim()
          const majorVersion = bundlerVersion.match(/^\d+/)[0]
          console.log(`Using Bundler ${majorVersion} from ${lockFile} BUNDLED WITH ${bundlerVersion}`)
          return majorVersion
        }
      }
    }
    return null
  }
  
  async function installBundler(bundlerVersionInput, lockFile, platform, rubyPrefix, engine, rubyVersion) {
    let bundlerVersion = bundlerVersionInput
  
    if (bundlerVersion === 'default' || bundlerVersion === 'Gemfile.lock') {
      bundlerVersion = readBundledWithFromGemfileLock(lockFile)
  
      if (!bundlerVersion) {
        bundlerVersion = 'latest'
      }
    }
  
    if (bundlerVersion === 'latest') {
      bundlerVersion = '2'
    }
  
    if (/^\d+/.test(bundlerVersion)) {
      // OK
    } else {
      throw new Error(`Cannot parse bundler input: ${bundlerVersion}`)
    }
  
    if (engine === 'ruby' && rubyVersion.match(/^2\.[12]/)) {
      console.log('Bundler 2 requires Ruby 2.3+, using Bundler 1 on Ruby <= 2.2')
      bundlerVersion = '1'
    } else if (engine === 'ruby' && rubyVersion.startsWith('2.3')) {
      console.log('Ruby 2.3 has a bug with Bundler 2 (https://github.com/rubygems/rubygems/issues/3570), using Bundler 1 instead on Ruby 2.3')
      bundlerVersion = '1'
    } else if (engine === 'jruby' && rubyVersion.startsWith('9.1.')) { // JRuby 9.1 targets Ruby 2.3, treat it the same
      console.log('JRuby 9.1 has a bug with Bundler 2 (https://github.com/ruby/setup-ruby/issues/108), using Bundler 1 instead on JRuby 9.1')
      bundlerVersion = '1'
    }
  
    if (common.isHeadVersion(rubyVersion) && common.isBundler2Default(engine, rubyVersion) && bundlerVersion === '2') {
      console.log(`Using Bundler 2 shipped with ${engine}-${rubyVersion}`)
    } else if (engine === 'truffleruby' && !common.isHeadVersion(rubyVersion) && bundlerVersion === '1') {
      console.log(`Using Bundler 1 shipped with ${engine}`)
    } else {
      const gem = path.join(rubyPrefix, 'bin', 'gem')
      await exec.exec(gem, ['install', 'bundler', '-v', `~> ${bundlerVersion}`, '--no-document'])
    }
  
    return bundlerVersion
  }
  
  async function bundleInstall(gemfile, lockFile, platform, engine, rubyVersion, bundlerVersion) {
    if (gemfile === null) {
      console.log('Could not determine gemfile path, skipping "bundle install" and caching')
      return false
    }
  
    let envOptions = {}
    if (bundlerVersion.startsWith('1') && common.isBundler2Default(engine, rubyVersion)) {
      // If Bundler 1 is specified on Rubies which ship with Bundler 2,
      // we need to specify which Bundler version to use explicitly until the lockfile exists.
      console.log(`Setting BUNDLER_VERSION=${bundlerVersion} for "bundle config|lock" commands below to ensure Bundler 1 is used`)
      envOptions = { env: { ...process.env, BUNDLER_VERSION: bundlerVersion } }
    }
  
    // config
    const path = 'vendor/bundle'
  
    await exec.exec('bundle', ['config', '--local', 'path', path], envOptions)
  
    if (fs.existsSync(lockFile)) {
      await exec.exec('bundle', ['config', '--local', 'deployment', 'true'], envOptions)
    } else {
      // Generate the lockfile so we can use it to compute the cache key.
      // This will also automatically pick up the latest gem versions compatible with the Gemfile.
      await exec.exec('bundle', ['lock'], envOptions)
    }
  
    // cache key
    const paths = [path]
    const baseKey = await computeBaseKey(platform, engine, rubyVersion, lockFile)
    const key = `${baseKey}-${await common.hashFile(lockFile)}`
    // If only Gemfile.lock changes we can reuse part of the cache, and clean old gem versions below
    const restoreKeys = [`${baseKey}-`]
    console.log(`Cache key: ${key}`)
  
    // restore cache & install
    let cachedKey = null
    try {
      cachedKey = await cache.restoreCache(paths, key, restoreKeys)
    } catch (error) {
      if (error.name === cache.ValidationError.name) {
        throw error;
      } else {
        core.info(`[warning] There was an error restoring the cache ${error.message}`)
      }
    }
  
    if (cachedKey) {
      console.log(`Found cache for key: ${cachedKey}`)
    }
  
    // Always run 'bundle install' to list the gems
    await exec.exec('bundle', ['install', '--jobs', '4'])
  
    // @actions/cache only allows to save for non-existing keys
    if (cachedKey !== key) {
      if (cachedKey) { // existing cache but Gemfile.lock differs, clean old gems
        await exec.exec('bundle', ['clean'])
      }
  
      // Error handling from https://github.com/actions/cache/blob/master/src/save.ts
      console.log('Saving cache')
      try {
        await cache.saveCache(paths, key)
      } catch (error) {
        if (error.name === cache.ValidationError.name) {
          throw error;
        } else if (error.name === cache.ReserveCacheError.name) {
          core.info(error.message);
        } else {
          core.info(`[warning]${error.message}`)
        }
      }
    }
  
    return true
  }
  
  async function computeBaseKey(platform, engine, version, lockFile) {
    let key = `setup-ruby-bundler-cache-v2-${platform}-${engine}-${version}`
  
    if (engine !== 'jruby' && common.isHeadVersion(version)) {
      let revision = '';
      await exec.exec('ruby', ['-e', 'print RUBY_REVISION'], {
        silent: true,
        listeners: {
          stdout: (data) => {
            revision += data.toString();
          }
        }
      });
      key += `-revision-${revision}`
    }
  
    key += `-${lockFile}`
    return key
  }
  
  if (__filename.endsWith('index.js')) { run() }
  