Skip to content

Commit

Permalink
Support Heroku-24 and ARM architecture (#284)
Browse files Browse the repository at this point in the history
* Support Heroku-24 and ARM architecture

Heroku 24 stack image supports both arm64 and amd64. The Ruby binaries are available on heroku-24 compiled to these two stacks heroku/docker-heroku-ruby-builder#38. The buildpack adds support by:

- Updating the `buildpackl.toml` to include ARM architecture
- Using architecture information to build the URL for Ubuntu 24.04 (heroku-24)
- Integration testing on all supported Ubuntu versions

* Fix multi arch test compilation


From: https://github.com/heroku/buildpacks-ruby/pull/284/files#r1603946649. The tests currently fail locally on a Mac (arm64/aarch64) because tests are always being compiled for amd64/x86. This change is a workaround to force compilation to the current target architecture.

* Apply suggestions from code review

Co-authored-by: Ed Morley <[email protected]>

* Fix variable name

---------

Co-authored-by: Ed Morley <[email protected]>
  • Loading branch information
schneems and edmorley authored May 17, 2024
1 parent 6966b88 commit b863136
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 15 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion buildpacks/ruby/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- The buildpack now implements Buildpack API 0.10 instead of 0.9, and so requires `lifecycle` 0.17.x or newer. ([#283](https://github.com/heroku/buildpacks-ruby/pull/283/files#commit-suggestions))
### Changed

- The buildpack now implements Buildpack API 0.10 instead of 0.9, and so requires `lifecycle` 0.17.x or newer. ([#283](https://github.com/heroku/buildpacks-ruby/pull/283))

### Added

- Added support for Ubuntu 24.04 (and thus Heroku-24 / `heroku/builder:24`). ([#284](https://github.com/heroku/buildpacks-ruby/pull/284))

## [2.1.3] - 2024-03-18

Expand Down
12 changes: 12 additions & 0 deletions buildpacks/ruby/buildpack.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,17 @@ version = "20.04"
name = "ubuntu"
version = "22.04"

[[targets.distros]]
name = "ubuntu"
version = "24.04"

[[targets]]
os = "linux"
arch = "arm64"

[[targets.distros]]
name = "ubuntu"
version = "24.04"

[metadata.release]
image = { repository = "docker.io/heroku/buildpack-ruby" }
15 changes: 11 additions & 4 deletions buildpacks/ruby/src/layers/ruby_install_layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,18 @@ fn download_url(
let filename = format!("ruby-{version}.tgz");
let base = "https://heroku-buildpack-ruby.s3.us-east-1.amazonaws.com";
let mut url = Url::parse(base).map_err(RubyInstallError::UrlParseError)?;
{
let mut segments = url
.path_segments_mut()
.map_err(|()| RubyInstallError::InvalidBaseUrl(String::from(base)))?;

segments.push(&target.stack_name().map_err(RubyInstallError::TargetError)?);
if target.is_arch_aware() {
segments.push(&target.cpu_architecture);
}
segments.push(&filename);
}

url.path_segments_mut()
.map_err(|()| RubyInstallError::InvalidBaseUrl(String::from(base)))?
.push(&target.stack_name().map_err(RubyInstallError::TargetError)?)
.push(&filename);
Ok(url)
}

Expand Down
23 changes: 16 additions & 7 deletions buildpacks/ruby/src/target_id.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TargetId {
pub(crate) distro_name: String,
pub(crate) distro_version: String,
pub(crate) cpu_architecture: String,
}

const ARCH_AWARE_VERSIONS: &[&str] = &["24.04"];
const DISTRO_VERSION_STACK: &[(&str, &str, &str)] = &[
("ubuntu", "20.04", "heroku-20"),
("ubuntu", "22.04", "heroku-22"),
("ubuntu", "24.04", "heroku-24"),
];

#[derive(Debug, thiserror::Error)]
pub(crate) enum TargetIdError {
#[error("Distro name and version {0}-{1} is not supported. Must be one of: {}", DISTRO_VERSION_STACK.iter().map(|&(name, version, _)| format!("{name}-{version}")).collect::<Vec<_>>().join(", "))]
#[error("Distro name and version '{0}-{1}' is not supported. Must be one of: {}", DISTRO_VERSION_STACK.iter().map(|&(name, version, _)| format!("'{name}-{version}'")).collect::<Vec<_>>().join(", "))]
UnknownDistroNameVersionCombo(String, String),

#[error("Cannot convert stack name {0} into a target OS. Must be one of: {}", DISTRO_VERSION_STACK.iter().map(|&(_, _, stack)| String::from(stack)).collect::<Vec<_>>().join(", "))]
#[error("Cannot convert stack name '{0}' into a target OS. Must be one of: {}", DISTRO_VERSION_STACK.iter().map(|&(_, _, stack)| format!("'{stack}'")).collect::<Vec<_>>().join(", "))]
UnknownStack(String),
}

impl TargetId {
pub(crate) fn is_arch_aware(&self) -> bool {
ARCH_AWARE_VERSIONS.contains(&self.distro_version.as_str())
}

pub(crate) fn stack_name(&self) -> Result<String, TargetIdError> {
DISTRO_VERSION_STACK
.iter()
Expand Down Expand Up @@ -53,6 +55,13 @@ impl TargetId {
mod test {
use super::*;

#[test]
fn test_arch_aware_versions_are_also_known_as_a_stack() {
for version in ARCH_AWARE_VERSIONS {
assert!(DISTRO_VERSION_STACK.iter().any(|&(_, v, _)| &v == version));
}
}

#[test]
fn test_stack_name() {
assert_eq!(
Expand Down
40 changes: 39 additions & 1 deletion buildpacks/ruby/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ use ureq::Response;
#[test]
#[ignore = "integration test"]
fn test_migrating_metadata() {
// This test is a placeholder for when a change modifies metadata structures.
// Remove the return and update the `buildpack-ruby` reference to the latest version.
#![allow(unreachable_code)]
return;

let builder = "heroku/builder:22";
let app_dir = "tests/fixtures/default_ruby";

Expand Down Expand Up @@ -57,9 +62,28 @@ fn test_default_app_ubuntu20() {

#[test]
#[ignore = "integration test"]
fn test_default_app_latest_distro() {
fn test_default_app_ubuntu22() {
TestRunner::default().build(
BuildConfig::new("heroku/builder:22", "tests/fixtures/default_ruby"),
|context| {
println!("{}", context.pack_stdout);
assert_contains!(context.pack_stdout, "# Heroku Ruby Buildpack");
assert_contains!(
context.pack_stdout,
r#"`BUNDLE_BIN="/layers/heroku_ruby/gems/bin" BUNDLE_CLEAN="1" BUNDLE_DEPLOYMENT="1" BUNDLE_GEMFILE="/workspace/Gemfile" BUNDLE_PATH="/layers/heroku_ruby/gems" BUNDLE_WITHOUT="development:test" bundle install`"#);

assert_contains!(context.pack_stdout, "Installing webrick");
},
);
}

#[test]
#[ignore = "integration test"]
fn test_default_app_latest_distro() {
let config = amd_arm_builder_config("heroku/builder:24", "tests/fixtures/default_ruby");

TestRunner::default().build(
config,
|context| {
println!("{}", context.pack_stdout);
assert_contains!(context.pack_stdout, "# Heroku Ruby Buildpack");
Expand Down Expand Up @@ -257,3 +281,17 @@ fn frac_seconds(seconds: f64) -> Duration {
}

const TEST_PORT: u16 = 1234;

// TODO: Once Pack build supports `--platform` and libcnb-test adjusted accordingly, change this
// to allow configuring the target arch independently of the builder name (eg via env var).
fn amd_arm_builder_config(builder_name: &str, app_dir: &str) -> BuildConfig {
let mut config = BuildConfig::new(builder_name, app_dir);

match builder_name {
"heroku/builder:24" if cfg!(target_arch = "aarch64") => {
config.target_triple("aarch64-unknown-linux-musl")
}
_ => config.target_triple("x86_64-unknown-linux-musl"),
};
config
}

1 comment on commit b863136

@nickhammond
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exciting!!!

Please sign in to comment.