From 49534d9122b991620397f585586300ff3292078f Mon Sep 17 00:00:00 2001 From: Bo Anderson Date: Tue, 5 Dec 2023 15:40:16 +0000 Subject: [PATCH] Support bundler-only runs --- .github/workflows/test.yml | 9 ++- action.yml | 5 +- bundler.js | 67 ++++++++++++++++++-- dist/index.js | 126 +++++++++++++++++++++++++++++++------ index.js | 59 +++++++++++++---- 5 files changed, 228 insertions(+), 38 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index baa6c0578..3524f737d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,8 @@ jobs: '1.9', '2.0', '2.1', '2.2', '2.3', '2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2', '3.3', ruby-head, jruby, jruby-head, truffleruby, truffleruby-head, - truffleruby+graalvm, truffleruby+graalvm-head + truffleruby+graalvm, truffleruby+graalvm-head, + system ] include: - { os: windows-2019, ruby: mingw } @@ -56,6 +57,7 @@ jobs: - { os: windows-2022, ruby: truffleruby-head } - { os: windows-2022, ruby: truffleruby+graalvm } - { os: windows-2022, ruby: truffleruby+graalvm-head } + - { os: windows-2022, ruby: system } name: ${{ matrix.os }} ${{ matrix.ruby }} runs-on: ${{ matrix.os }} @@ -63,6 +65,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./ + id: setup-ruby with: ruby-version: ${{ matrix.ruby }} bundler-cache: true @@ -72,6 +75,8 @@ jobs: run: | # Show PATH with Powershell $f, $r = $env:PATH.split([IO.Path]::PathSeparator); $r + - name: Ruby prefix output + run: echo "${{ steps.setup-ruby.outputs.ruby-prefix }}" - name: build compiler run: | @@ -107,6 +112,7 @@ jobs: - run: gem env - name: C extension test + if: matrix.ruby != 'system' run: gem install json -v 2.2.0 - run: bundle --version # This step is redundant with `bundler-cache: true` but is there to check a redundant `bundle install` still works @@ -129,6 +135,7 @@ jobs: if: startsWith(matrix.os, 'windows') - name: Test `gem github:` in a Gemfile + if: matrix.ruby != 'system' || !startsWith(matrix.os, 'ubuntu') run: bundle install env: BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/gem_from_github.gemfile diff --git a/action.yml b/action.yml index c36fafdb2..5eba2de8b 100644 --- a/action.yml +++ b/action.yml @@ -6,7 +6,10 @@ branding: icon: download inputs: ruby-version: - description: 'Engine and version to use, see the syntax in the README. Reads from .ruby-version or .tool-versions if unset.' + description: | + Engine and version to use. Either 'default' (the default), 'system', or a engine/version syntax described in the README. + For 'default', reads from .ruby-version or .tool-versions. + For 'system', don't install any Ruby and use the first available Ruby on the PATH. default: 'default' rubygems: description: | diff --git a/bundler.js b/bundler.js index d0f40eca3..909f3541b 100644 --- a/bundler.js +++ b/bundler.js @@ -2,7 +2,9 @@ const fs = require('fs') const path = require('path') const core = require('@actions/core') const exec = require('@actions/exec') +const io = require('@actions/io') const cache = require('@actions/cache') +const semver = require('semver') const common = require('./common') export const DEFAULT_CACHE_VERSION = '0' @@ -58,7 +60,7 @@ async function afterLockFile(lockFile, platform, engine, rubyVersion) { } } -export async function installBundler(bundlerVersionInput, rubygemsInputSet, lockFile, platform, rubyPrefix, engine, rubyVersion) { +export async function installBundler(bundlerVersionInput, rubygemsInputSet, systemRubyUsed, lockFile, platform, rubyPrefix, engine, rubyVersion) { let bundlerVersion = bundlerVersionInput if (rubygemsInputSet && (bundlerVersion === 'default' || bundlerVersion === 'Gemfile.lock')) { @@ -79,7 +81,21 @@ export async function installBundler(bundlerVersionInput, rubygemsInputSet, lock const floatVersion = common.floatVersion(rubyVersion) if (bundlerVersion === 'default') { - if (common.isBundler2dot2Default(engine, rubyVersion)) { + if (systemRubyUsed) { + if (await io.which('bundle', false)) { + bundlerVersion = await getBundlerVersion() + if (semver.lt(semver.coerce(bundlerVersion), '2.2.0')) { + console.log('Using latest Bundler because the system Bundler is too old') + bundlerVersion = 'latest' + } else { + console.log(`Using system Bundler ${bundlerVersion}`) + return bundlerVersion + } + } else { + console.log('Installing latest Bundler as we could not find a system Bundler') + bundlerVersion = 'latest' + } + } else if (common.isBundler2dot2Default(engine, rubyVersion)) { if (common.windows && engine === 'ruby' && (common.isStableVersion(engine, rubyVersion) || rubyVersion === 'head')) { // https://github.com/ruby/setup-ruby/issues/371 console.log(`Installing latest Bundler for ${engine}-${rubyVersion} on Windows because bin/bundle does not work in bash otherwise`) @@ -144,12 +160,38 @@ export async function installBundler(bundlerVersionInput, rubygemsInputSet, lock const versionParts = [...bundlerVersion.matchAll(/\d+/g)].length const bundlerVersionConstraint = versionParts >= 3 ? bundlerVersion : `~> ${bundlerVersion}.0` - await exec.exec(gem, ['install', 'bundler', ...force, '-v', bundlerVersionConstraint]) + const args = ['install', 'bundler', ...force, '-v', bundlerVersionConstraint] + + let stderr = '' + try { + await exec.exec(gem, args, { + listeners: { + stderr: (data) => (stderr += data.toString()) + } + }) + } catch (error) { + if (systemRubyUsed && stderr.includes('Gem::FilePermissionError')) { + await exec.exec('sudo', [gem, ...args]); + } else { + throw error + } + } return bundlerVersion } -export async function bundleInstall(gemfile, lockFile, platform, engine, rubyVersion, bundlerVersion, cacheVersion) { +async function getBundlerVersion() { + let bundlerVersion = '' + await exec.exec('bundle', ['--version'], { + silent: true, + listeners: { + stdout: (data) => (bundlerVersion += data.toString()) + } + }) + return bundlerVersion.replace(/^Bundler version /, '').trim() +} + +export async function bundleInstall(gemfile, lockFile, platform, engine, rubyVersion, bundlerVersion, cacheVersion, systemRubyUsed) { if (gemfile === null) { console.log('Could not determine gemfile path, skipping "bundle install" and caching') return false @@ -182,7 +224,7 @@ export async function bundleInstall(gemfile, lockFile, platform, engine, rubyVer // cache key const paths = [cachePath] - const baseKey = await computeBaseKey(platform, engine, rubyVersion, lockFile, cacheVersion) + const baseKey = await computeBaseKey(platform, engine, rubyVersion, lockFile, cacheVersion, systemRubyUsed) 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}-`] @@ -232,7 +274,7 @@ export async function bundleInstall(gemfile, lockFile, platform, engine, rubyVer return true } -async function computeBaseKey(platform, engine, version, lockFile, cacheVersion) { +async function computeBaseKey(platform, engine, version, lockFile, cacheVersion, systemRubyUsed) { const cwd = process.cwd() const bundleWith = process.env['BUNDLE_WITH'] || '' const bundleWithout = process.env['BUNDLE_WITHOUT'] || '' @@ -259,6 +301,19 @@ async function computeBaseKey(platform, engine, version, lockFile, cacheVersion) } } + if (systemRubyUsed) { + let platform = '' + await exec.exec('ruby', ['-e', 'print RUBY_PLATFORM'], { + silent: true, + listeners: { + stdout: (data) => { + platform += data.toString(); + } + } + }); + key += `-platform-${platform}` + } + key += `-${lockFile}` return key } diff --git a/dist/index.js b/dist/index.js index 65f3bbfe0..621eecc02 100644 --- a/dist/index.js +++ b/dist/index.js @@ -16,7 +16,9 @@ const fs = __nccwpck_require__(7147) const path = __nccwpck_require__(1017) const core = __nccwpck_require__(2186) const exec = __nccwpck_require__(1514) +const io = __nccwpck_require__(7436) const cache = __nccwpck_require__(7799) +const semver = __nccwpck_require__(1383) const common = __nccwpck_require__(3143) const DEFAULT_CACHE_VERSION = '0' @@ -72,7 +74,7 @@ async function afterLockFile(lockFile, platform, engine, rubyVersion) { } } -async function installBundler(bundlerVersionInput, rubygemsInputSet, lockFile, platform, rubyPrefix, engine, rubyVersion) { +async function installBundler(bundlerVersionInput, rubygemsInputSet, systemRubyUsed, lockFile, platform, rubyPrefix, engine, rubyVersion) { let bundlerVersion = bundlerVersionInput if (rubygemsInputSet && (bundlerVersion === 'default' || bundlerVersion === 'Gemfile.lock')) { @@ -93,7 +95,21 @@ async function installBundler(bundlerVersionInput, rubygemsInputSet, lockFile, p const floatVersion = common.floatVersion(rubyVersion) if (bundlerVersion === 'default') { - if (common.isBundler2dot2Default(engine, rubyVersion)) { + if (systemRubyUsed) { + if (await io.which('bundle', false)) { + bundlerVersion = await getBundlerVersion() + if (semver.lt(semver.coerce(bundlerVersion), '2.2.0')) { + console.log('Using latest Bundler because the system Bundler is too old') + bundlerVersion = 'latest' + } else { + console.log(`Using system Bundler ${bundlerVersion}`) + return bundlerVersion + } + } else { + console.log('Installing latest Bundler as we could not find a system Bundler') + bundlerVersion = 'latest' + } + } else if (common.isBundler2dot2Default(engine, rubyVersion)) { if (common.windows && engine === 'ruby' && (common.isStableVersion(engine, rubyVersion) || rubyVersion === 'head')) { // https://github.com/ruby/setup-ruby/issues/371 console.log(`Installing latest Bundler for ${engine}-${rubyVersion} on Windows because bin/bundle does not work in bash otherwise`) @@ -158,12 +174,38 @@ async function installBundler(bundlerVersionInput, rubygemsInputSet, lockFile, p const versionParts = [...bundlerVersion.matchAll(/\d+/g)].length const bundlerVersionConstraint = versionParts >= 3 ? bundlerVersion : `~> ${bundlerVersion}.0` - await exec.exec(gem, ['install', 'bundler', ...force, '-v', bundlerVersionConstraint]) + const args = ['install', 'bundler', ...force, '-v', bundlerVersionConstraint] + + let stderr = '' + try { + await exec.exec(gem, args, { + listeners: { + stderr: (data) => (stderr += data.toString()) + } + }) + } catch (error) { + if (systemRubyUsed && stderr.includes('Gem::FilePermissionError')) { + await exec.exec('sudo', [gem, ...args]); + } else { + throw error + } + } return bundlerVersion } -async function bundleInstall(gemfile, lockFile, platform, engine, rubyVersion, bundlerVersion, cacheVersion) { +async function getBundlerVersion() { + let bundlerVersion = '' + await exec.exec('bundle', ['--version'], { + silent: true, + listeners: { + stdout: (data) => (bundlerVersion += data.toString()) + } + }) + return bundlerVersion.replace(/^Bundler version /, '').trim() +} + +async function bundleInstall(gemfile, lockFile, platform, engine, rubyVersion, bundlerVersion, cacheVersion, systemRubyUsed) { if (gemfile === null) { console.log('Could not determine gemfile path, skipping "bundle install" and caching') return false @@ -196,7 +238,7 @@ async function bundleInstall(gemfile, lockFile, platform, engine, rubyVersion, b // cache key const paths = [cachePath] - const baseKey = await computeBaseKey(platform, engine, rubyVersion, lockFile, cacheVersion) + const baseKey = await computeBaseKey(platform, engine, rubyVersion, lockFile, cacheVersion, systemRubyUsed) 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}-`] @@ -246,7 +288,7 @@ async function bundleInstall(gemfile, lockFile, platform, engine, rubyVersion, b return true } -async function computeBaseKey(platform, engine, version, lockFile, cacheVersion) { +async function computeBaseKey(platform, engine, version, lockFile, cacheVersion, systemRubyUsed) { const cwd = process.cwd() const bundleWith = process.env['BUNDLE_WITH'] || '' const bundleWithout = process.env['BUNDLE_WITHOUT'] || '' @@ -273,6 +315,19 @@ async function computeBaseKey(platform, engine, version, lockFile, cacheVersion) } } + if (systemRubyUsed) { + let platform = '' + await exec.exec('ruby', ['-e', 'print RUBY_PLATFORM'], { + silent: true, + listeners: { + stdout: (data) => { + platform += data.toString(); + } + } + }); + key += `-platform-${platform}` + } + key += `-${lockFile}` return key } @@ -65745,17 +65800,22 @@ async function setupRuby(options = {}) { process.chdir(inputs['working-directory']) const platform = common.getOSNameVersion() - const [engine, parsedVersion] = parseRubyEngineAndVersion(inputs['ruby-version']) + const [engine, parsedVersion] = await parseRubyEngineAndVersion(inputs['ruby-version']) + const systemRuby = inputs['ruby-version'] === 'system' - let installer - if (platform.startsWith('windows-') && engine === 'ruby' && !common.isSelfHostedRunner()) { - installer = __nccwpck_require__(3216) + let installer, version + if (systemRuby) { + version = parsedVersion } else { - installer = __nccwpck_require__(9974) - } + if (platform.startsWith('windows-') && engine === 'ruby' && !common.isSelfHostedRunner()) { + installer = __nccwpck_require__(3216) + } else { + installer = __nccwpck_require__(9974) + } - const engineVersions = installer.getAvailableVersions(platform, engine) - const version = validateRubyEngineAndVersion(platform, engineVersions, engine, parsedVersion) + const engineVersions = installer.getAvailableVersions(platform, engine) + version = validateRubyEngineAndVersion(platform, engineVersions, engine, parsedVersion) + } createGemRC(engine, version) envPreInstall() @@ -65767,7 +65827,12 @@ async function setupRuby(options = {}) { await (__nccwpck_require__(3216).installJRubyTools)() } - const rubyPrefix = await installer.install(platform, engine, version) + let rubyPrefix + if (systemRuby) { + rubyPrefix = await getSystemRubyPrefix() + } else { + rubyPrefix = await installer.install(platform, engine, version) + } await common.measure('Print Ruby version', async () => await exec.exec('ruby', ['--version'])) @@ -65790,18 +65855,18 @@ async function setupRuby(options = {}) { if (inputs['bundler'] !== 'none') { bundlerVersion = await common.measure('Installing Bundler', async () => - bundler.installBundler(inputs['bundler'], rubygemsInputSet, lockFile, platform, rubyPrefix, engine, version)) + bundler.installBundler(inputs['bundler'], rubygemsInputSet, systemRuby, lockFile, platform, rubyPrefix, engine, version)) } if (inputs['bundler-cache'] === 'true') { await common.time('bundle install', async () => - bundler.bundleInstall(gemfile, lockFile, platform, engine, version, bundlerVersion, inputs['cache-version'])) + bundler.bundleInstall(gemfile, lockFile, platform, engine, version, bundlerVersion, inputs['cache-version'], systemRuby)) } core.setOutput('ruby-prefix', rubyPrefix) } -function parseRubyEngineAndVersion(rubyVersion) { +async function parseRubyEngineAndVersion(rubyVersion) { if (rubyVersion === 'default') { if (fs.existsSync('.ruby-version')) { rubyVersion = '.ruby-version' @@ -65810,6 +65875,17 @@ function parseRubyEngineAndVersion(rubyVersion) { } else { throw new Error('input ruby-version needs to be specified if no .ruby-version or .tool-versions file exists') } + } else if (rubyVersion === 'system') { + rubyVersion = '' + await exec.exec('ruby', ['-e', 'print "#{RUBY_ENGINE}-#{RUBY_VERSION}"'], { + silent: true, + listeners: { + stdout: (data) => (rubyVersion += data.toString()) + } + }) + if (!rubyVersion.includes('-')) { + throw new Error('Could not determine system Ruby engine and version') + } } if (rubyVersion === '.ruby-version') { // Read from .ruby-version @@ -65888,6 +65964,20 @@ function envPreInstall() { } } +async function getSystemRubyPrefix() { + let rubyPrefix = '' + await exec.exec('ruby', ['-rrbconfig', '-e', 'print RbConfig::CONFIG["prefix"]'], { + silent: true, + listeners: { + stdout: (data) => (rubyPrefix += data.toString()) + } + }) + if (!rubyPrefix) { + throw new Error('Could not determine system Ruby prefix') + } + return rubyPrefix +} + if (__filename.endsWith('index.js')) { run() } })(); diff --git a/index.js b/index.js index a9735b567..0d6cd7f90 100644 --- a/index.js +++ b/index.js @@ -48,17 +48,22 @@ export async function setupRuby(options = {}) { process.chdir(inputs['working-directory']) const platform = common.getOSNameVersion() - const [engine, parsedVersion] = parseRubyEngineAndVersion(inputs['ruby-version']) + const [engine, parsedVersion] = await parseRubyEngineAndVersion(inputs['ruby-version']) + const systemRuby = inputs['ruby-version'] === 'system' - let installer - if (platform.startsWith('windows-') && engine === 'ruby' && !common.isSelfHostedRunner()) { - installer = require('./windows') + let installer, version + if (systemRuby) { + version = parsedVersion } else { - installer = require('./ruby-builder') - } + if (platform.startsWith('windows-') && engine === 'ruby' && !common.isSelfHostedRunner()) { + installer = require('./windows') + } else { + installer = require('./ruby-builder') + } - const engineVersions = installer.getAvailableVersions(platform, engine) - const version = validateRubyEngineAndVersion(platform, engineVersions, engine, parsedVersion) + const engineVersions = installer.getAvailableVersions(platform, engine) + version = validateRubyEngineAndVersion(platform, engineVersions, engine, parsedVersion) + } createGemRC(engine, version) envPreInstall() @@ -70,7 +75,12 @@ export async function setupRuby(options = {}) { await require('./windows').installJRubyTools() } - const rubyPrefix = await installer.install(platform, engine, version) + let rubyPrefix + if (systemRuby) { + rubyPrefix = await getSystemRubyPrefix() + } else { + rubyPrefix = await installer.install(platform, engine, version) + } await common.measure('Print Ruby version', async () => await exec.exec('ruby', ['--version'])) @@ -93,18 +103,18 @@ export async function setupRuby(options = {}) { if (inputs['bundler'] !== 'none') { bundlerVersion = await common.measure('Installing Bundler', async () => - bundler.installBundler(inputs['bundler'], rubygemsInputSet, lockFile, platform, rubyPrefix, engine, version)) + bundler.installBundler(inputs['bundler'], rubygemsInputSet, systemRuby, lockFile, platform, rubyPrefix, engine, version)) } if (inputs['bundler-cache'] === 'true') { await common.time('bundle install', async () => - bundler.bundleInstall(gemfile, lockFile, platform, engine, version, bundlerVersion, inputs['cache-version'])) + bundler.bundleInstall(gemfile, lockFile, platform, engine, version, bundlerVersion, inputs['cache-version'], systemRuby)) } core.setOutput('ruby-prefix', rubyPrefix) } -function parseRubyEngineAndVersion(rubyVersion) { +async function parseRubyEngineAndVersion(rubyVersion) { if (rubyVersion === 'default') { if (fs.existsSync('.ruby-version')) { rubyVersion = '.ruby-version' @@ -113,6 +123,17 @@ function parseRubyEngineAndVersion(rubyVersion) { } else { throw new Error('input ruby-version needs to be specified if no .ruby-version or .tool-versions file exists') } + } else if (rubyVersion === 'system') { + rubyVersion = '' + await exec.exec('ruby', ['-e', 'print "#{RUBY_ENGINE}-#{RUBY_VERSION}"'], { + silent: true, + listeners: { + stdout: (data) => (rubyVersion += data.toString()) + } + }) + if (!rubyVersion.includes('-')) { + throw new Error('Could not determine system Ruby engine and version') + } } if (rubyVersion === '.ruby-version') { // Read from .ruby-version @@ -191,4 +212,18 @@ function envPreInstall() { } } +async function getSystemRubyPrefix() { + let rubyPrefix = '' + await exec.exec('ruby', ['-rrbconfig', '-e', 'print RbConfig::CONFIG["prefix"]'], { + silent: true, + listeners: { + stdout: (data) => (rubyPrefix += data.toString()) + } + }) + if (!rubyPrefix) { + throw new Error('Could not determine system Ruby prefix') + } + return rubyPrefix +} + if (__filename.endsWith('index.js')) { run() }