Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ruby 3.3 #11497

Merged
merged 14 commits into from Apr 30, 2024
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
@@ -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" }]
}