Skip to content

Commit

Permalink
Add support for ruby 3.3 (#11497)
Browse files Browse the repository at this point in the history
Add support for ruby3.3 when using AL2023 (Node v20). No support for
ruby3.3 on Node <20 planned.

Ruby integration tests will now running on AL2023

---------

Co-authored-by: Nathan Rajlich <n@n8.io>
  • Loading branch information
jeffsee55 and TooTallNate committed Apr 30, 2024
1 parent 21f5e73 commit 763a6d1
Show file tree
Hide file tree
Showing 26 changed files with 217 additions and 52 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-badgers-protect.md
@@ -0,0 +1,5 @@
---
'@vercel/ruby': minor
---

Add support for Ruby 3.3
4 changes: 3 additions & 1 deletion packages/ruby/package.json
Expand Up @@ -23,10 +23,12 @@
"devDependencies": {
"@types/fs-extra": "8.0.0",
"@types/semver": "6.0.0",
"@types/which": "3.0.0",
"@vercel/build-utils": "7.12.0",
"execa": "2.0.4",
"fs-extra": "^7.0.1",
"jest-junit": "16.0.0",
"semver": "6.3.1"
"semver": "6.3.1",
"which": "3.0.0"
}
}
21 changes: 15 additions & 6 deletions packages/ruby/src/index.ts
Expand Up @@ -49,7 +49,7 @@ async function bundleInstall(
bundleDir: string,
gemfilePath: string,
rubyPath: string,
runtime: string
major: number
) {
debug(`running "bundle install --deployment"...`);
const bundleAppConfig = await getWriteableDirectory();
Expand All @@ -73,6 +73,15 @@ async function bundleInstall(
gemfilePath,
gemfileContent.replace('ruby "~> 3.2.x"', 'ruby "~> 3.2.0"')
);
} else if (gemfileContent.includes('ruby "~> 3.3.x"')) {
// Gemfile contains "3.3.x" which will cause an error message:
// "Your Ruby patchlevel is 0, but your Gemfile specified -1"
// See https://github.com/rubygems/bundler/blob/3f0638c6c8d340c2f2405ecb84eb3b39c433e36e/lib/bundler/errors.rb#L49
// We must correct to the actual version in the build container.
await writeFile(
gemfilePath,
gemfileContent.replace('ruby "~> 3.3.x"', 'ruby "~> 3.3.0"')
);
}

const bundlerEnv = cloneEnv(process.env, {
Expand All @@ -83,9 +92,9 @@ async function bundleInstall(
BUNDLE_JOBS: '4',
});

// Lambda "ruby3.2" runtime does not include "webrick",
// which is needed for the `vc_init.rb` entrypoint file
if (runtime === 'ruby3.2') {
// "webrick" needs to be installed for Ruby 3+ to fix runtime error:
// webrick is not part of the default gems since Ruby 3.0.0. Install webrick from RubyGems.
if (major >= 3) {
const result = await execa('bundler', ['add', 'webrick'], {
cwd: dirname(gemfilePath),
stdio: 'pipe',
Expand Down Expand Up @@ -143,7 +152,7 @@ export const build: BuildV3 = async ({
const gemfileContents = gemfilePath
? await readFile(gemfilePath, 'utf8')
: '';
const { gemHome, bundlerPath, vendorPath, runtime, rubyPath } =
const { gemHome, bundlerPath, vendorPath, runtime, rubyPath, major } =
await installBundler(meta, gemfileContents);
process.env.GEM_HOME = gemHome;
debug(`Checking existing vendor directory at "${vendorPath}"`);
Expand Down Expand Up @@ -193,7 +202,7 @@ export const build: BuildV3 = async ({
bundleDir,
gemfilePath,
rubyPath,
runtime
major
);
}
}
Expand Down
81 changes: 51 additions & 30 deletions packages/ruby/src/install-ruby.ts
@@ -1,35 +1,42 @@
import execa from 'execa';
import which from 'which';
import { join } from 'path';
import { intersects } from 'semver';
import execa from 'execa';
import { Meta, NodeVersion, debug, NowBuildError } from '@vercel/build-utils';

interface RubyVersion extends NodeVersion {
minor: number;
}

function getOptions() {
const options = [
{ major: 3, minor: 2, range: '3.2.x', runtime: 'ruby3.2' },
{
major: 2,
minor: 7,
range: '2.7.x',
runtime: 'ruby2.7',
discontinueDate: new Date('2023-12-07'),
},
{
major: 2,
minor: 5,
range: '2.5.x',
runtime: 'ruby2.5',
discontinueDate: new Date('2021-11-30'),
},
] as const;
return options;
}
const allOptions: RubyVersion[] = [
{ major: 3, minor: 3, range: '3.3.x', runtime: 'ruby3.3' },
{ major: 3, minor: 2, range: '3.2.x', runtime: 'ruby3.2' },
{
major: 2,
minor: 7,
range: '2.7.x',
runtime: 'ruby2.7',
discontinueDate: new Date('2023-12-07'),
},
{
major: 2,
minor: 5,
range: '2.5.x',
runtime: 'ruby2.5',
discontinueDate: new Date('2021-11-30'),
},
];

function getLatestRubyVersion(): RubyVersion {
return getOptions()[0];
const selection = allOptions.find(isInstalled);
if (!selection) {
throw new NowBuildError({
code: 'RUBY_INVALID_VERSION',
link: 'http://vercel.link/ruby-version',
message: `Unable to find any supported Ruby versions.`,
});
}
return selection;
}

function isDiscontinued({ discontinueDate }: RubyVersion): boolean {
Expand All @@ -49,7 +56,7 @@ function getRubyPath(meta: Meta, gemfileContents: string) {
.find(line => line.startsWith('ruby'));
if (line) {
const strVersion = line.slice(4).trim().slice(1, -1).replace('~>', '');
const found = getOptions().some(o => {
const found = allOptions.some(o => {
// The array is already in order so return the first
// match which will be the newest version.
selection = o;
Expand All @@ -62,12 +69,17 @@ function getRubyPath(meta: Meta, gemfileContents: string) {
link: 'http://vercel.link/ruby-version',
});
}
if (isDiscontinued(selection)) {
const discontinued = isDiscontinued(selection);
if (discontinued || !isInstalled(selection)) {
const latest = getLatestRubyVersion();
const intro = `Found \`Gemfile\` with discontinued Ruby version: \`${line}.\``;
const intro = `Found \`Gemfile\` with ${
discontinued ? 'discontinued' : 'invalid'
} Ruby version: \`${line}.\``;
const hint = `Please set \`ruby "~> ${latest.range}"\` in your \`Gemfile\` to use Ruby ${latest.range}.`;
throw new NowBuildError({
code: 'RUBY_DISCONTINUED_VERSION',
code: discontinued
? 'RUBY_DISCONTINUED_VERSION'
: 'RUBY_INVALID_VERSION',
link: 'http://vercel.link/ruby-version',
message: `${intro} ${hint}`,
});
Expand All @@ -78,6 +90,7 @@ function getRubyPath(meta: Meta, gemfileContents: string) {
const { major, minor, runtime } = selection;
const gemHome = '/ruby' + major + minor;
const result = {
major,
gemHome,
runtime,
rubyPath: join(gemHome, 'bin', 'ruby'),
Expand All @@ -92,10 +105,8 @@ function getRubyPath(meta: Meta, gemfileContents: string) {
// process.env.GEM_HOME), and returns
// the absolute path to it
export async function installBundler(meta: Meta, gemfileContents: string) {
const { gemHome, rubyPath, gemPath, vendorPath, runtime } = getRubyPath(
meta,
gemfileContents
);
const { gemHome, rubyPath, gemPath, vendorPath, runtime, major } =
getRubyPath(meta, gemfileContents);

// If the new File System API is used (`avoidTopLevelInstall`), the Install Command
// will have already installed the dependencies, so we don't need to do it again.
Expand All @@ -105,6 +116,7 @@ export async function installBundler(meta: Meta, gemfileContents: string) {
);

return {
major,
gemHome,
rubyPath,
gemPath,
Expand All @@ -123,6 +135,7 @@ export async function installBundler(meta: Meta, gemfileContents: string) {
});

return {
major,
gemHome,
rubyPath,
gemPath,
Expand All @@ -131,3 +144,11 @@ export async function installBundler(meta: Meta, gemfileContents: string) {
bundlerPath: join(gemHome, 'bin', 'bundler'),
};
}

function isInstalled({ major, minor }: RubyVersion): boolean {
const gemHome = '/ruby' + major + minor;
return (
Boolean(which.sync(join(gemHome, 'bin/ruby'), { nothrow: true })) &&
Boolean(which.sync(join(gemHome, 'bin/gem'), { nothrow: true }))
);
}
2 changes: 1 addition & 1 deletion packages/ruby/test/fixtures/01-cowsay/Gemfile
Expand Up @@ -2,6 +2,6 @@

source "https://rubygems.org"

ruby "~> 3.2.x"
ruby "~> 3.3.x"

gem "cowsay", "~> 0.3.0"
7 changes: 7 additions & 0 deletions packages/ruby/test/fixtures/01a-cowsay-ruby-3.2/Gemfile
@@ -0,0 +1,7 @@
# frozen_string_literal: true

source "https://rubygems.org"

ruby "~> 3.2.x"

gem "cowsay", "~> 0.3.0"
13 changes: 13 additions & 0 deletions packages/ruby/test/fixtures/01a-cowsay-ruby-3.2/Gemfile.lock
@@ -0,0 +1,13 @@
GEM
remote: https://rubygems.org/
specs:
cowsay (0.3.0)

PLATFORMS
ruby

DEPENDENCIES
cowsay (~> 0.3.0)

BUNDLED WITH
2.0.1
10 changes: 10 additions & 0 deletions packages/ruby/test/fixtures/01a-cowsay-ruby-3.2/index.rb
@@ -0,0 +1,10 @@
require 'webrick'
require 'cowsay'

class Handler < WEBrick::HTTPServlet::AbstractServlet
def do_GET req, res
res.status = 200
res['Content-Type'] = 'text/plain'
res.body = Cowsay.say('gem:RANDOMNESS_PLACEHOLDER', 'cow')
end
end
5 changes: 5 additions & 0 deletions packages/ruby/test/fixtures/01a-cowsay-ruby-3.2/package.json
@@ -0,0 +1,5 @@
{
"engines": {
"node": "18.x"
}
}
8 changes: 8 additions & 0 deletions packages/ruby/test/fixtures/01a-cowsay-ruby-3.2/vercel.json
@@ -0,0 +1,8 @@
{
"version": 2,
"builds": [{ "src": "index.rb", "use": "@vercel/ruby" }],
"probes": [
{ "path": "/", "mustContain": "gem:RANDOMNESS_PLACEHOLDER" },
{ "path": "/", "method": "HEAD", "status": 200 }
]
}
2 changes: 1 addition & 1 deletion packages/ruby/test/fixtures/02-cowsay-vendored/Gemfile
Expand Up @@ -2,6 +2,6 @@

source "https://rubygems.org"

ruby "~> 3.2.x"
ruby "~> 3.3.x"

gem "cowsay", "~> 0.3.0"
Expand Up @@ -2,6 +2,6 @@

source "https://rubygems.org"

ruby "~> 3.2.x"
ruby "~> 3.3.x"

gem "cowsay", "~> 0.3.0"
Expand Up @@ -5,6 +5,7 @@ GEM

PLATFORMS
x86_64-darwin-21
x86_64-linux

DEPENDENCIES
cowsay (~> 0.3.0)
Expand Down
2 changes: 1 addition & 1 deletion packages/ruby/test/fixtures/05-sinatra/Gemfile
Expand Up @@ -2,7 +2,7 @@

source "https://rubygems.org"

ruby "~> 3.2.x"
ruby "~> 3.3.x"

gem "cowsay", "~> 0.3.0"

Expand Down
@@ -0,0 +1,7 @@
# frozen_string_literal: true

source "https://rubygems.org"

ruby "~> 3.3.x"

gem "cowsay", "~> 0.3.0"
@@ -0,0 +1,16 @@
GEM
remote: https://rubygems.org/
specs:
cowsay (0.3.0)

PLATFORMS
x86_64-linux

DEPENDENCIES
cowsay (~> 0.3.0)

RUBY VERSION
ruby 2.5.5p157

BUNDLED WITH
2.2.22
@@ -0,0 +1,9 @@
require 'cowsay'

Handler = Proc.new do |req, res|
name = req.query['name'] || 'World'

res.status = 200
res['Content-Type'] = 'text/text; charset=utf-8'
res.body = Cowsay.say("Hello #{name}", 'cow')
end
@@ -0,0 +1,5 @@
{
"engines": {
"node": "18.x"
}
}
@@ -0,0 +1,4 @@
{
"version": 2,
"builds": [{ "src": "index.rb", "use": "@vercel/ruby" }]
}
@@ -0,0 +1,7 @@
# frozen_string_literal: true

source "https://rubygems.org"

ruby "~> 3.2.x"

gem "cowsay", "~> 0.3.0"
@@ -0,0 +1,16 @@
GEM
remote: https://rubygems.org/
specs:
cowsay (0.3.0)

PLATFORMS
x86_64-linux

DEPENDENCIES
cowsay (~> 0.3.0)

RUBY VERSION
ruby 2.5.5p157

BUNDLED WITH
2.2.22
@@ -0,0 +1,9 @@
require 'cowsay'

Handler = Proc.new do |req, res|
name = req.query['name'] || 'World'

res.status = 200
res['Content-Type'] = 'text/text; charset=utf-8'
res.body = Cowsay.say("Hello #{name}", 'cow')
end
@@ -0,0 +1,4 @@
{
"version": 2,
"builds": [{ "src": "index.rb", "use": "@vercel/ruby" }]
}

0 comments on commit 763a6d1

Please sign in to comment.