Skip to content

Commit

Permalink
Support Heroku-24 and multi-arch (#1439)
Browse files Browse the repository at this point in the history
Heroku-24 base image supports two architectures amd64 and arm64. We've built binaries for these two architectures heroku/docker-heroku-ruby-builder#38. Effectively this means that the s3 bucket that holds the Ruby binaries has an additional folder. Previously files were at `<stack>/ruby-<version>.tgz` now they are at `<stack>/<arch>/ruby-<version>.tgz` but only for `heroku-24` and future stacks moving forward.

To support multiple architectures, the buildpack needs to detect the current architecture and whether or not the current stack supports multiple architectures.

Beyond downloading binaries, the buildpack is aware of the S3 structure to travers version numbers in order to warn customers when a newer version of a ruby version is available. We also warn customers if their current Ruby version is not available on the next stack. This behavior is implemented and tested in this commit, however we're not turning on warnings for `heroku-24` yet as customers cannot currently use it.
  • Loading branch information
schneems authored Apr 17, 2024
1 parent 14cfdcc commit 935fd0f
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [Unreleased]

- Heroku-24 stack initial support. Includes multi-architecture (arm64/amd64) logic that has not been tested on the platform (https://github.com/heroku/heroku-buildpack-ruby/pull/1439)
- Remove unused Rubinius and Ruby 1.9.2 codepaths (https://github.com/heroku/heroku-buildpack-ruby/pull/1440)

## [v267] - 2024-02-28
Expand Down
17 changes: 17 additions & 0 deletions lib/language_pack/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class LanguagePack::Base
VENDOR_URL = ENV['BUILDPACK_VENDOR_URL'] || "https://heroku-buildpack-ruby.s3.us-east-1.amazonaws.com"
DEFAULT_LEGACY_STACK = "cedar"
ROOT_DIR = File.expand_path("../../..", __FILE__)
MULTI_ARCH_STACKS = ["heroku-24"]
KNOWN_ARCHITECTURES = ["amd64", "arm64"]

attr_reader :build_path, :cache, :stack

Expand All @@ -35,10 +37,25 @@ def initialize(build_path, cache_path = nil, layer_dir=nil)
@id = Digest::SHA1.hexdigest("#{Time.now.to_f}-#{rand(1000000)}")[0..10]
@fetchers = {:buildpack => LanguagePack::Fetcher.new(VENDOR_URL) }
@layer_dir = layer_dir
@arch = get_arch

Dir.chdir build_path
end

def get_arch
command = "dpkg --print-architecture"
arch = run!(command, silent: true).strip

if !KNOWN_ARCHITECTURES.include?(arch)
raise <<~EOF
Architecture '#{arch}' returned from command `#{command}` is unknown.
Known architectures include: #{KNOWN_ARCHITECTURES.inspect}"
EOF
end

arch
end

def self.===(build_path)
raise "must subclass"
end
Expand Down
3 changes: 2 additions & 1 deletion lib/language_pack/fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ class FetchError < StandardError; end

include ShellHelpers

def initialize(host_url, stack: nil)
def initialize(host_url, stack: nil, arch: nil)
@host_url = Pathname.new(host_url)
# File.basename prevents accidental directory traversal
@host_url += File.basename(stack) if stack
@host_url += File.basename(arch) if arch
end

def exists?(path, max_attempts = 1)
Expand Down
8 changes: 6 additions & 2 deletions lib/language_pack/helpers/download_presence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@
class LanguagePack::Helpers::DownloadPresence
STACKS = ['heroku-20', 'heroku-22']

def initialize(file_name:, stacks: STACKS)
def initialize(file_name:, arch: , multi_arch_stacks:, stacks: STACKS )
@file_name = file_name
@stacks = stacks
@fetchers = []
@threads = []
@stacks.each do |stack|
@fetchers << LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL, stack: stack)
if multi_arch_stacks.include?(stack)
@fetchers << LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL, stack: stack, arch: arch)
else
@fetchers << LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL, stack: stack)
end
end
end

Expand Down
1 change: 1 addition & 0 deletions lib/language_pack/helpers/outdated_ruby_version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# outdated = LanguagePack::Helpers::OutdatedRubyVersion.new(
# current_ruby_version: ruby_version,
# fetcher: LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL, stack: "heroku-22")
# fetcher: LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL, stack: "heroku-22", arch: "amd64")
# )
#
# outdated.call
Expand Down
8 changes: 6 additions & 2 deletions lib/language_pack/installers/heroku_ruby_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ class LanguagePack::Installers::HerokuRubyInstaller
include LanguagePack::ShellHelpers
attr_reader :fetcher

def initialize(stack: )
@fetcher = LanguagePack::Fetcher.new(BASE_URL, stack: stack)
def initialize(stack: , multi_arch_stacks: , arch: )
if multi_arch_stacks.include?(stack)
@fetcher = LanguagePack::Fetcher.new(BASE_URL, stack: stack, arch: arch)
else
@fetcher = LanguagePack::Fetcher.new(BASE_URL, stack: stack)
end
end

def install(ruby_version, install_dir)
Expand Down
4 changes: 4 additions & 0 deletions lib/language_pack/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -514,11 +514,15 @@ def install_ruby(install_path)
return false unless ruby_version

installer = LanguagePack::Installers::HerokuRubyInstaller.new(
multi_arch_stacks: MULTI_ARCH_STACKS,
stack: @stack,
arch: @arch
)

@ruby_download_check = LanguagePack::Helpers::DownloadPresence.new(
multi_arch_stacks: MULTI_ARCH_STACKS,
file_name: ruby_version.file_name,
arch: @arch
)
@ruby_download_check.call

Expand Down
46 changes: 46 additions & 0 deletions spec/helpers/download_presence_spec.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,44 @@
require "spec_helper"

describe LanguagePack::Helpers::DownloadPresence do
it "handles multi-arch transitions for files that exist" do
download = LanguagePack::Helpers::DownloadPresence.new(
multi_arch_stacks: ["heroku-24"],
file_name: 'ruby-3.1.4.tgz',
stacks: ["heroku-22", "heroku-24"],
arch: "amd64"
)

download.call

expect(download.next_stack(current_stack: "heroku-22")).to eq("heroku-24")
expect(download.next_stack(current_stack: "heroku-24")).to be_falsey

expect(download.exists_on_next_stack?(current_stack:"heroku-22")).to be_truthy
end

it "handles multi-arch transitions for files that do not exist" do
download = LanguagePack::Helpers::DownloadPresence.new(
multi_arch_stacks: ["heroku-24"],
file_name: 'ruby-3.0.5.tgz',
stacks: ["heroku-20", "heroku-24"],
arch: "amd64"
)

download.call

expect(download.next_stack(current_stack: "heroku-20")).to eq("heroku-24")
expect(download.next_stack(current_stack: "heroku-24")).to be_falsey

expect(download.exists_on_next_stack?(current_stack:"heroku-20")).to be_falsey
end

it "knows if exists on the next stack" do
download = LanguagePack::Helpers::DownloadPresence.new(
multi_arch_stacks: [],
file_name: 'ruby-3.1.4.tgz',
stacks: ['heroku-20', 'heroku-22'],
arch: nil
)

download.call
Expand All @@ -17,8 +51,10 @@

it "detects when a package is present on higher stacks" do
download = LanguagePack::Helpers::DownloadPresence.new(
multi_arch_stacks: [],
file_name: 'ruby-2.6.5.tgz',
stacks: ['cedar-14', 'heroku-16', 'heroku-18'],
arch: nil
)

download.call
Expand All @@ -32,8 +68,10 @@

it "detects when a package is not present on higher stacks" do
download = LanguagePack::Helpers::DownloadPresence.new(
multi_arch_stacks: [],
file_name: 'ruby-1.9.3.tgz',
stacks: ['cedar-14', 'heroku-16', 'heroku-18'],
arch: nil
)

download.call
Expand All @@ -44,8 +82,10 @@

it "detects when a package is present on two stacks but not a third" do
download = LanguagePack::Helpers::DownloadPresence.new(
multi_arch_stacks: [],
file_name: 'ruby-2.3.0.tgz',
stacks: ['cedar-14', 'heroku-16', 'heroku-18'],
arch: nil
)

download.call
Expand All @@ -56,8 +96,10 @@

it "detects when a package does not exist" do
download = LanguagePack::Helpers::DownloadPresence.new(
multi_arch_stacks: [],
file_name: 'does-not-exist.tgz',
stacks: ['cedar-14', 'heroku-16', 'heroku-18'],
arch: nil
)

download.call
Expand All @@ -68,7 +110,9 @@

it "detects default ruby version" do
download = LanguagePack::Helpers::DownloadPresence.new(
multi_arch_stacks: [],
file_name: "ruby-3.1.1.tgz",
arch: nil
)

download.call
Expand All @@ -79,7 +123,9 @@

it "handles the current stack not being in the known stacks list" do
download = LanguagePack::Helpers::DownloadPresence.new(
multi_arch_stacks: [],
file_name: "#{LanguagePack::RubyVersion::DEFAULT_VERSION}.tgz",
arch: nil
)

download.call
Expand Down
32 changes: 32 additions & 0 deletions spec/helpers/outdated_ruby_version_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,38 @@
LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL, stack: stack)
}

it "handles amd ↗️ architecture on heroku-24" do
ruby_version = LanguagePack::RubyVersion.new("ruby-3.1.0")
fetcher = LanguagePack::Fetcher.new(
LanguagePack::Base::VENDOR_URL,
stack: "heroku-24",
arch: "amd64"
)
outdated = LanguagePack::Helpers::OutdatedRubyVersion.new(
current_ruby_version: ruby_version,
fetcher: fetcher
)

outdated.call
expect(outdated.suggested_ruby_minor_version).to eq("3.1.4")
end

it "handles arm 💪 architecture on heroku-24" do
ruby_version = LanguagePack::RubyVersion.new("ruby-3.1.0")
fetcher = LanguagePack::Fetcher.new(
LanguagePack::Base::VENDOR_URL,
stack: "heroku-24",
arch: "arm64"
)
outdated = LanguagePack::Helpers::OutdatedRubyVersion.new(
current_ruby_version: ruby_version,
fetcher: fetcher
)

outdated.call
expect(outdated.suggested_ruby_minor_version).to eq("3.1.4")
end

it "finds the latest version on a stack" do
ruby_version = LanguagePack::RubyVersion.new("ruby-2.2.5")
outdated = LanguagePack::Helpers::OutdatedRubyVersion.new(
Expand Down
10 changes: 7 additions & 3 deletions spec/installers/heroku_ruby_installer_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
require "spec_helper"

describe LanguagePack::Installers::HerokuRubyInstaller do
let(:installer) { LanguagePack::Installers::HerokuRubyInstaller.new(
stack: "cedar-14"
) }
let(:installer) {
LanguagePack::Installers::HerokuRubyInstaller.new(
multi_arch_stacks: [],
stack: "cedar-14",
arch: nil,
)
}
let(:ruby_version) { LanguagePack::RubyVersion.new("ruby-2.3.3") }

describe "#fetch_unpack" do
Expand Down

0 comments on commit 935fd0f

Please sign in to comment.