From d697be2f83c6234b20877c3b5eac7a7f342f0d0c Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Thu, 4 Dec 2025 21:23:50 +0100 Subject: [PATCH] Account for Bundler 4 * Fixes https://github.com/ruby/setup-ruby/issues/827 --- bundler.js | 52 +++++++++++++++++++++++------------ common.js | 19 +++++++++---- dist/index.js | 76 +++++++++++++++++++++++++++++++++++---------------- 3 files changed, 101 insertions(+), 46 deletions(-) diff --git a/bundler.js b/bundler.js index e0d117931..988360850 100644 --- a/bundler.js +++ b/bundler.js @@ -67,17 +67,15 @@ export async function installBundler(bundlerVersionInput, rubygemsInputSet, lock } } - const floatVersion = common.floatVersion(rubyVersion) - if (bundlerVersion === 'default') { - if (common.isBundler2dot2Default(engine, rubyVersion)) { + if (common.isBundler2dot2PlusDefault(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`) bundlerVersion = 'latest' } else { - console.log(`Using Bundler 2 shipped with ${engine}-${rubyVersion}`) - return '2' + console.log(`Using Bundler shipped with ${engine}-${rubyVersion}`) + return common.isBundler4PlusDefault(engine, rubyVersion) ? '4' : '2' } } else if (common.hasBundlerDefaultGem(engine, rubyVersion)) { // Those Rubies have a old Bundler default gem < 2.2 which does not work well for `gem 'foo', github: 'foo/foo'`: @@ -90,8 +88,15 @@ export async function installBundler(bundlerVersionInput, rubygemsInputSet, lock } } + const targetRubyVersion = common.targetRubyVersion(engine, rubyVersion) + if (bundlerVersion === 'latest') { - bundlerVersion = '2' + // Bundler 4 requires Ruby 3.2+ + if (targetRubyVersion < 3.2) { + bundlerVersion = '2' + } else { + bundlerVersion = '4' + } } if (isValidBundlerVersion(bundlerVersion)) { @@ -100,24 +105,29 @@ export async function installBundler(bundlerVersionInput, rubygemsInputSet, lock throw new Error(`Cannot parse bundler input: ${bundlerVersion}`) } - // Use Bundler 1 when we know Bundler 2 does not work - if (bundlerVersion.startsWith('2')) { - if (engine === 'ruby' && floatVersion <= 2.2) { - console.log('Bundler 2 requires Ruby 2.3+, using Bundler 1 on Ruby <= 2.2') + // Only use Bundler 4 on Ruby 3.2+ + if (common.floatVersion(bundlerVersion) >= 4 && targetRubyVersion < 3.2) { + console.log('Bundler 4 requires Ruby 3.2+, using Bundler 2 instead on Ruby < 3.2') + bundlerVersion = '2' + } + + // Use Bundler 1 when we know Bundler 2+ does not work + if (common.floatVersion(bundlerVersion) >= 2) { + if (engine === 'ruby' && targetRubyVersion <= 2.2) { + console.log(`Bundler 2+ requires Ruby 2.3+, using Bundler 1 on Ruby <= 2.2`) bundlerVersion = '1' } else if (engine === 'ruby' && /^2\.3\.[01]/.test(rubyVersion)) { console.log('Ruby 2.3.0 and 2.3.1 have shipped with an old rubygems that only works with Bundler 1') 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') + 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' } } - const targetRubyVersion = common.targetRubyVersion(engine, rubyVersion) // Use Bundler 2.3 when we use Ruby 2.3.2 - 2.5 // Use Bundler 2.4 when we use Ruby 2.6-2.7 - if (bundlerVersion == '2') { + if (bundlerVersion === '2') { if (targetRubyVersion <= 2.5) { // < 2.3.2 already handled above console.log('Ruby 2.3.2 - 2.5 only works with Bundler 2.3') bundlerVersion = '2.3' @@ -137,6 +147,14 @@ export async function installBundler(bundlerVersionInput, rubygemsInputSet, lock return bundlerVersion } +function bundlerConfigSetArgs(bundlerVersion, key, value) { + if (bundlerVersion.startsWith('1')) { + return ['config', '--local', key, value] + } else { + return ['config', 'set', '--local', key, value] + } +} + export async function bundleInstall(gemfile, lockFile, platform, engine, rubyVersion, bundlerVersion, cacheVersion) { if (gemfile === null) { console.log('Could not determine gemfile path, skipping "bundle install" and caching') @@ -144,8 +162,8 @@ export async function bundleInstall(gemfile, lockFile, platform, engine, rubyVer } let envOptions = {} - if (bundlerVersion.startsWith('1') && common.isBundler2Default(engine, rubyVersion)) { - // If Bundler 1 is specified on Rubies which ship with Bundler 2, + if (bundlerVersion.startsWith('1') && common.isBundler2PlusDefault(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 } } @@ -156,10 +174,10 @@ export async function bundleInstall(gemfile, lockFile, platform, engine, rubyVer // An absolute path, so it is reliably under $PWD/vendor/bundle, and not relative to the gemfile's directory const bundleCachePath = path.join(process.cwd(), cachePath) - await exec.exec('bundle', ['config', '--local', 'path', bundleCachePath], envOptions) + await exec.exec('bundle', bundlerConfigSetArgs(bundlerVersion, 'path', bundleCachePath), envOptions) if (fs.existsSync(lockFile)) { - await exec.exec('bundle', ['config', '--local', 'deployment', 'true'], envOptions) + await exec.exec('bundle', bundlerConfigSetArgs(bundlerVersion, '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. diff --git a/common.js b/common.js index 247d99eed..8898eb890 100644 --- a/common.js +++ b/common.js @@ -80,7 +80,7 @@ export function isStableVersion(engine, rubyVersion) { } export function hasBundlerDefaultGem(engine, rubyVersion) { - return isBundler1Default(engine, rubyVersion) || isBundler2Default(engine, rubyVersion) + return isBundler1Default(engine, rubyVersion) || isBundler2PlusDefault(engine, rubyVersion) } export function isBundler1Default(engine, rubyVersion) { @@ -95,7 +95,7 @@ export function isBundler1Default(engine, rubyVersion) { } } -export function isBundler2Default(engine, rubyVersion) { +export function isBundler2PlusDefault(engine, rubyVersion) { if (engine === 'ruby') { return floatVersion(rubyVersion) >= 2.7 } else if (engine.startsWith('truffleruby')) { @@ -107,7 +107,7 @@ export function isBundler2Default(engine, rubyVersion) { } } -export function isBundler2dot2Default(engine, rubyVersion) { +export function isBundler2dot2PlusDefault(engine, rubyVersion) { if (engine === 'ruby') { return floatVersion(rubyVersion) >= 3.0 } else if (engine.startsWith('truffleruby')) { @@ -119,6 +119,13 @@ export function isBundler2dot2Default(engine, rubyVersion) { } } +const UNKNOWN_TARGET_RUBY_VERSION = 9.9 + +export function isBundler4PlusDefault(engine, rubyVersion) { + const version = targetRubyVersion(engine, rubyVersion) + return version != UNKNOWN_TARGET_RUBY_VERSION && version >= 4.0 +} + export function targetRubyVersion(engine, rubyVersion) { const version = floatVersion(rubyVersion) if (engine === 'ruby') { @@ -132,6 +139,8 @@ export function targetRubyVersion(engine, rubyVersion) { return 2.6 } else if (version === 9.4) { return 3.1 + } else if (version === 10.0) { + return 3.4 } } else if (engine.startsWith('truffleruby')) { if (version < 21.0) { @@ -147,11 +156,11 @@ export function targetRubyVersion(engine, rubyVersion) { } } - return 9.9 // unknown, assume recent + return UNKNOWN_TARGET_RUBY_VERSION // unknown, assume recent } export function floatVersion(rubyVersion) { - const match = rubyVersion.match(/^\d+\.\d+/) + const match = rubyVersion.match(/^\d+(\.\d+|$)/) if (match) { return parseFloat(match[0]) } else if (isHeadVersion(rubyVersion)) { diff --git a/dist/index.js b/dist/index.js index 5f32d1cb1..5e0aa8653 100644 --- a/dist/index.js +++ b/dist/index.js @@ -81,17 +81,15 @@ async function installBundler(bundlerVersionInput, rubygemsInputSet, lockFile, p } } - const floatVersion = common.floatVersion(rubyVersion) - if (bundlerVersion === 'default') { - if (common.isBundler2dot2Default(engine, rubyVersion)) { + if (common.isBundler2dot2PlusDefault(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`) bundlerVersion = 'latest' } else { - console.log(`Using Bundler 2 shipped with ${engine}-${rubyVersion}`) - return '2' + console.log(`Using Bundler shipped with ${engine}-${rubyVersion}`) + return common.isBundler4PlusDefault(engine, rubyVersion) ? '4' : '2' } } else if (common.hasBundlerDefaultGem(engine, rubyVersion)) { // Those Rubies have a old Bundler default gem < 2.2 which does not work well for `gem 'foo', github: 'foo/foo'`: @@ -104,8 +102,15 @@ async function installBundler(bundlerVersionInput, rubygemsInputSet, lockFile, p } } + const targetRubyVersion = common.targetRubyVersion(engine, rubyVersion) + if (bundlerVersion === 'latest') { - bundlerVersion = '2' + // Bundler 4 requires Ruby 3.2+ + if (targetRubyVersion < 3.2) { + bundlerVersion = '2' + } else { + bundlerVersion = '4' + } } if (isValidBundlerVersion(bundlerVersion)) { @@ -114,24 +119,29 @@ async function installBundler(bundlerVersionInput, rubygemsInputSet, lockFile, p throw new Error(`Cannot parse bundler input: ${bundlerVersion}`) } - // Use Bundler 1 when we know Bundler 2 does not work - if (bundlerVersion.startsWith('2')) { - if (engine === 'ruby' && floatVersion <= 2.2) { - console.log('Bundler 2 requires Ruby 2.3+, using Bundler 1 on Ruby <= 2.2') + // Only use Bundler 4 on Ruby 3.2+ + if (common.floatVersion(bundlerVersion) >= 4 && targetRubyVersion < 3.2) { + console.log('Bundler 4 requires Ruby 3.2+, using Bundler 2 instead on Ruby < 3.2') + bundlerVersion = '2' + } + + // Use Bundler 1 when we know Bundler 2+ does not work + if (common.floatVersion(bundlerVersion) >= 2) { + if (engine === 'ruby' && targetRubyVersion <= 2.2) { + console.log(`Bundler 2+ requires Ruby 2.3+, using Bundler 1 on Ruby <= 2.2`) bundlerVersion = '1' } else if (engine === 'ruby' && /^2\.3\.[01]/.test(rubyVersion)) { console.log('Ruby 2.3.0 and 2.3.1 have shipped with an old rubygems that only works with Bundler 1') 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') + 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' } } - const targetRubyVersion = common.targetRubyVersion(engine, rubyVersion) // Use Bundler 2.3 when we use Ruby 2.3.2 - 2.5 // Use Bundler 2.4 when we use Ruby 2.6-2.7 - if (bundlerVersion == '2') { + if (bundlerVersion === '2') { if (targetRubyVersion <= 2.5) { // < 2.3.2 already handled above console.log('Ruby 2.3.2 - 2.5 only works with Bundler 2.3') bundlerVersion = '2.3' @@ -151,6 +161,14 @@ async function installBundler(bundlerVersionInput, rubygemsInputSet, lockFile, p return bundlerVersion } +function bundlerConfigSetArgs(bundlerVersion, key, value) { + if (bundlerVersion.startsWith('1')) { + return ['config', '--local', key, value] + } else { + return ['config', 'set', '--local', key, value] + } +} + async function bundleInstall(gemfile, lockFile, platform, engine, rubyVersion, bundlerVersion, cacheVersion) { if (gemfile === null) { console.log('Could not determine gemfile path, skipping "bundle install" and caching') @@ -158,8 +176,8 @@ async function bundleInstall(gemfile, lockFile, platform, engine, rubyVersion, b } let envOptions = {} - if (bundlerVersion.startsWith('1') && common.isBundler2Default(engine, rubyVersion)) { - // If Bundler 1 is specified on Rubies which ship with Bundler 2, + if (bundlerVersion.startsWith('1') && common.isBundler2PlusDefault(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 } } @@ -170,10 +188,10 @@ async function bundleInstall(gemfile, lockFile, platform, engine, rubyVersion, b // An absolute path, so it is reliably under $PWD/vendor/bundle, and not relative to the gemfile's directory const bundleCachePath = path.join(process.cwd(), cachePath) - await exec.exec('bundle', ['config', '--local', 'path', bundleCachePath], envOptions) + await exec.exec('bundle', bundlerConfigSetArgs(bundlerVersion, 'path', bundleCachePath), envOptions) if (fs.existsSync(lockFile)) { - await exec.exec('bundle', ['config', '--local', 'deployment', 'true'], envOptions) + await exec.exec('bundle', bundlerConfigSetArgs(bundlerVersion, '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. @@ -291,8 +309,9 @@ __nccwpck_require__.r(__webpack_exports__); /* harmony export */ hashFile: () => (/* binding */ hashFile), /* harmony export */ inputs: () => (/* binding */ inputs), /* harmony export */ isBundler1Default: () => (/* binding */ isBundler1Default), -/* harmony export */ isBundler2Default: () => (/* binding */ isBundler2Default), -/* harmony export */ isBundler2dot2Default: () => (/* binding */ isBundler2dot2Default), +/* harmony export */ isBundler2PlusDefault: () => (/* binding */ isBundler2PlusDefault), +/* harmony export */ isBundler2dot2PlusDefault: () => (/* binding */ isBundler2dot2PlusDefault), +/* harmony export */ isBundler4PlusDefault: () => (/* binding */ isBundler4PlusDefault), /* harmony export */ isExactCacheKeyMatch: () => (/* binding */ isExactCacheKeyMatch), /* harmony export */ isHeadVersion: () => (/* binding */ isHeadVersion), /* harmony export */ isSelfHostedRunner: () => (/* binding */ isSelfHostedRunner), @@ -392,7 +411,7 @@ function isStableVersion(engine, rubyVersion) { } function hasBundlerDefaultGem(engine, rubyVersion) { - return isBundler1Default(engine, rubyVersion) || isBundler2Default(engine, rubyVersion) + return isBundler1Default(engine, rubyVersion) || isBundler2PlusDefault(engine, rubyVersion) } function isBundler1Default(engine, rubyVersion) { @@ -407,7 +426,7 @@ function isBundler1Default(engine, rubyVersion) { } } -function isBundler2Default(engine, rubyVersion) { +function isBundler2PlusDefault(engine, rubyVersion) { if (engine === 'ruby') { return floatVersion(rubyVersion) >= 2.7 } else if (engine.startsWith('truffleruby')) { @@ -419,7 +438,7 @@ function isBundler2Default(engine, rubyVersion) { } } -function isBundler2dot2Default(engine, rubyVersion) { +function isBundler2dot2PlusDefault(engine, rubyVersion) { if (engine === 'ruby') { return floatVersion(rubyVersion) >= 3.0 } else if (engine.startsWith('truffleruby')) { @@ -431,6 +450,13 @@ function isBundler2dot2Default(engine, rubyVersion) { } } +const UNKNOWN_TARGET_RUBY_VERSION = 9.9 + +function isBundler4PlusDefault(engine, rubyVersion) { + const version = targetRubyVersion(engine, rubyVersion) + return version != UNKNOWN_TARGET_RUBY_VERSION && version >= 4.0 +} + function targetRubyVersion(engine, rubyVersion) { const version = floatVersion(rubyVersion) if (engine === 'ruby') { @@ -444,6 +470,8 @@ function targetRubyVersion(engine, rubyVersion) { return 2.6 } else if (version === 9.4) { return 3.1 + } else if (version === 10.0) { + return 3.4 } } else if (engine.startsWith('truffleruby')) { if (version < 21.0) { @@ -459,11 +487,11 @@ function targetRubyVersion(engine, rubyVersion) { } } - return 9.9 // unknown, assume recent + return UNKNOWN_TARGET_RUBY_VERSION // unknown, assume recent } function floatVersion(rubyVersion) { - const match = rubyVersion.match(/^\d+\.\d+/) + const match = rubyVersion.match(/^\d+(\.\d+|$)/) if (match) { return parseFloat(match[0]) } else if (isHeadVersion(rubyVersion)) {