Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 80 additions & 19 deletions internal/recipe/jruby.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"

"github.com/cloudfoundry/binary-builder/internal/archive"
Expand All @@ -30,8 +32,9 @@ func jdkSubdir(jdkInstallDir string) (string, error) {
}

// JRubyRecipe builds JRuby by:
// 1. Downloading the JDK from the stack-specific URL.
// 2. Building Maven from source (pinned version).
// 1. Downloading the JDK from the stack-specific URL (JDK 8 for JRuby 9.x,
// JDK 21 for JRuby 10.x).
// 2. Downloading Maven (pinned version, 3.6.3 for 9.x, 3.9.9 for 10.x).
// 3. Compiling JRuby via Maven.
// 4. Stripping incorrect_words.yaml from the resulting jar files.
//
Expand All @@ -51,16 +54,44 @@ func (j *JRubyRecipe) Artifact() ArtifactMeta {
// jrubyToRubyVersion maps a JRuby major.minor prefix to the Ruby compatibility version.
// This mapping is JRuby's own versioning scheme — not stack-specific.
var jrubyToRubyVersion = map[string]string{
"9.3": "2.6",
"9.4": "3.1",
"9.3": "2.6",
"9.4": "3.1",
"10.0": "3.4",
"10.1": "4.0",
Comment thread
benjaminguttmann-avtq marked this conversation as resolved.
Resolved
}

// mavenVersion is the pinned Maven version used for the JRuby build.
// SHA512 matches the official Apache Maven 3.6.3 binary distribution.
const (
mavenVersion = "3.6.3"
mavenSHA512 = "c35a1803a6e70a126e80b2b3ae33eed961f83ed74d18fcd16909b2d44d7dada3203f1ffe726c17ef8dcca2dcaa9fca676987befeadc9b9f759967a8cb77181c0"
)
// jrubyToolchain is the JDK and Maven combination used to build a JRuby version.
// SHA512 values match the official Apache Maven binary distributions.
type jrubyToolchain struct {
jdk stack.BootstrapBinary
jdkTarball string
mavenVersion string
mavenSHA512 string
}

// jrubyToolchainFor returns the toolchain for a JRuby version. JRuby 10.x
// requires Java 21+ and Maven 3.9+; 9.x builds with JDK 8 and Maven 3.6.3.
func jrubyToolchainFor(s *stack.Stack, jrubyVersion string) (jrubyToolchain, error) {
major, _, _ := strings.Cut(jrubyVersion, ".")
n, err := strconv.Atoi(major)
if err != nil {
return jrubyToolchain{}, fmt.Errorf("unexpected jruby version format %q", jrubyVersion)
}
if n >= 10 {
return jrubyToolchain{
jdk: s.Bootstrap.JRubyJDK21,
jdkTarball: "openjdk-21-jdk.tar.gz",
mavenVersion: "3.9.9",
mavenSHA512: "a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5678835887ab404556bfaf78dcfe03ba76fa2508649dca8531c74bca4d5846513522404d48e8c4ac8b",
}, nil
}
return jrubyToolchain{
jdk: s.Bootstrap.JRuby,
jdkTarball: "openjdk-8-jdk.tar.gz",
mavenVersion: "3.6.3",
mavenSHA512: "c35a1803a6e70a126e80b2b3ae33eed961f83ed74d18fcd16909b2d44d7dada3203f1ffe726c17ef8dcca2dcaa9fca676987befeadc9b9f759967a8cb77181c0",
}, nil
}

func (j *JRubyRecipe) Build(ctx context.Context, s *stack.Stack, src *source.Input, run runner.Runner, outData *output.OutData) error {
jrubyVersion := src.Version
Expand All @@ -81,16 +112,21 @@ func (j *JRubyRecipe) Build(ctx context.Context, s *stack.Stack, src *source.Inp
// source_input.version (never overwritten by build_jruby).
outData.ArtifactVersion = fullVersion

tc, err := jrubyToolchainFor(s, jrubyVersion)
if err != nil {
return fmt.Errorf("jruby: %w", err)
}

// Step 2: Download and install JDK from stack-specific URL.
jdkDir := s.Bootstrap.JRuby.InstallDir
jdkTar := fmt.Sprintf("%s/openjdk-8-jdk.tar.gz", jdkDir)
jdkDir := tc.jdk.InstallDir
jdkTar := fmt.Sprintf("%s/%s", jdkDir, tc.jdkTarball)

if err := run.Run("mkdir", "-p", jdkDir); err != nil {
return fmt.Errorf("jruby: creating JDK dir: %w", err)
}

jdkChecksum := source.Checksum{Algorithm: "sha256", Value: s.Bootstrap.JRuby.SHA256}
if err := j.Fetcher.Download(ctx, s.Bootstrap.JRuby.URL, jdkTar, jdkChecksum); err != nil {
jdkChecksum := source.Checksum{Algorithm: "sha256", Value: tc.jdk.SHA256}
if err := j.Fetcher.Download(ctx, tc.jdk.URL, jdkTar, jdkChecksum); err != nil {
return fmt.Errorf("jruby: downloading JDK: %w", err)
}

Expand All @@ -106,15 +142,15 @@ func (j *JRubyRecipe) Build(ctx context.Context, s *stack.Stack, src *source.Inp
}

// Step 3: Download and set up Maven.
mavenURL := fmt.Sprintf("https://archive.apache.org/dist/maven/maven-3/%s/binaries/apache-maven-%s-bin.tar.gz", mavenVersion, mavenVersion)
mavenTar := fmt.Sprintf("/tmp/apache-maven-%s-bin.tar.gz", mavenVersion)
mavenChecksum := source.Checksum{Algorithm: "sha512", Value: mavenSHA512}
mavenURL := fmt.Sprintf("https://archive.apache.org/dist/maven/maven-3/%s/binaries/apache-maven-%s-bin.tar.gz", tc.mavenVersion, tc.mavenVersion)
mavenTar := fmt.Sprintf("/tmp/apache-maven-%s-bin.tar.gz", tc.mavenVersion)
mavenChecksum := source.Checksum{Algorithm: "sha512", Value: tc.mavenSHA512}

if err := j.Fetcher.Download(ctx, mavenURL, mavenTar, mavenChecksum); err != nil {
return fmt.Errorf("jruby: downloading Maven: %w", err)
}

mavenInstallDir := fmt.Sprintf("/tmp/apache-maven-%s", mavenVersion)
mavenInstallDir := fmt.Sprintf("/tmp/apache-maven-%s", tc.mavenVersion)
if err := run.Run("tar", "xf", mavenTar, "-C", "/tmp"); err != nil {
return fmt.Errorf("jruby: extracting Maven: %w", err)
}
Expand Down Expand Up @@ -203,6 +239,31 @@ func (j *JRubyRecipe) Build(ctx context.Context, s *stack.Stack, src *source.Inp
return nil
}

func supportedJRubyPrefixes() string {
prefixes := make([]string, 0, len(jrubyToRubyVersion))
for p := range jrubyToRubyVersion {
prefixes = append(prefixes, p)
}
sort.Slice(prefixes, func(i, j int) bool {
return versionPrefixLess(prefixes[i], prefixes[j])
})
return strings.Join(prefixes, ", ")
}

// versionPrefixLess orders "major.minor" strings numerically (9.4 < 10.0).
func versionPrefixLess(a, b string) bool {
am, an, _ := strings.Cut(a, ".")
bm, bn, _ := strings.Cut(b, ".")
ai, _ := strconv.Atoi(am)
bi, _ := strconv.Atoi(bm)
if ai != bi {
return ai < bi
}
aj, _ := strconv.Atoi(an)
bj, _ := strconv.Atoi(bn)
return aj < bj
}

// jrubyRubyVersion maps a JRuby version string to the Ruby compatibility version.
func jrubyRubyVersion(jrubyVersion string) (string, error) {
// Extract major.minor prefix (first two parts).
Expand All @@ -214,7 +275,7 @@ func jrubyRubyVersion(jrubyVersion string) (string, error) {

rubyVer, ok := jrubyToRubyVersion[prefix]
if !ok {
return "", fmt.Errorf("unknown JRuby version %q — cannot determine Ruby compatibility version (supported prefixes: 9.3, 9.4)", jrubyVersion)
return "", fmt.Errorf("unknown JRuby version %q — cannot determine Ruby compatibility version (supported prefixes: %s)", jrubyVersion, supportedJRubyPrefixes())
}
return rubyVer, nil
}
66 changes: 66 additions & 0 deletions internal/recipe/recipe_compiled_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ func newCompiledStack(t *testing.T) *stack.Stack {
SHA256: "cafebabe",
InstallDir: t.TempDir() + "/java",
},
JRubyJDK21: stack.BootstrapBinary{
URL: "https://example.com/openjdk21.tar.gz",
SHA256: "feedface",
InstallDir: t.TempDir() + "/java21",
},
Go: stack.BootstrapBinary{
URL: "https://example.com/go-bootstrap.tar.gz",
SHA256: "deadbeef",
Expand Down Expand Up @@ -936,6 +941,66 @@ func TestJRubyRecipeVersion93(t *testing.T) {
"JRuby 9.3.x should produce artifact with ruby-2.6")
}

func TestJRubyRecipeRubyVersionMapping(t *testing.T) {
tests := []struct {
jruby, ruby string
}{
{"9.4.14.0", "3.1"},
{"10.0.7.0", "3.4"},
{"10.1.2.0", "4.0"},
}
for _, tc := range tests {
t.Run(tc.jruby, func(t *testing.T) {
useTempWorkDir(t)
full := tc.jruby + "-ruby-" + tc.ruby
writeFakeArtifact(t, "jruby-"+full+"-linux-x64.tgz")

r := &recipe.JRubyRecipe{Fetcher: newFakeFetcher()}
run := runner.NewFakeRunner()
s := newCompiledStack(t)
require.NoError(t, os.MkdirAll(filepath.Join(s.Bootstrap.JRuby.InstallDir, "jdk8u452"), 0755))
require.NoError(t, os.MkdirAll(filepath.Join(s.Bootstrap.JRubyJDK21.InstallDir, "jdk-21.0.7"), 0755))
src := newInput("jruby", tc.jruby, "https://repo1.maven.org/maven2/org/jruby/jruby-dist/"+tc.jruby+"/jruby-dist-"+tc.jruby+"-src.zip")

out := &output.OutData{}
require.NoError(t, r.Build(context.Background(), s, src, run, out))
assert.Equal(t, full, out.ArtifactVersion)
assert.True(t, hasCallMatching(run.Calls, "sh", "-Djruby.default.ruby.version="+tc.ruby))
})
}
}

func TestJRubyRecipeToolchainPerMajorVersion(t *testing.T) {
tests := []struct {
name, version, wantJDK, wantMaven, wantJavaHome string
useJDK21 bool
}{
{"9.x uses JDK 8 and Maven 3.6.3", "9.4.14.0", "https://example.com/openjdk.tar.gz", "apache-maven-3.6.3", "jdk8u452", false},
{"10.x uses JDK 21 and Maven 3.9", "10.0.7.0", "https://example.com/openjdk21.tar.gz", "apache-maven-3.9.9", "jdk-21.0.7", true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
useTempWorkDir(t)
ruby := map[bool]string{false: "3.1", true: "3.4"}[tc.useJDK21]
writeFakeArtifact(t, "jruby-"+tc.version+"-ruby-"+ruby+"-linux-x64.tgz")

f := newFakeFetcher()
r := &recipe.JRubyRecipe{Fetcher: f}
run := runner.NewFakeRunner()
s := newCompiledStack(t)
require.NoError(t, os.MkdirAll(filepath.Join(s.Bootstrap.JRuby.InstallDir, "jdk8u452"), 0755))
require.NoError(t, os.MkdirAll(filepath.Join(s.Bootstrap.JRubyJDK21.InstallDir, "jdk-21.0.7"), 0755))
src := newInput("jruby", tc.version, "https://repo1.maven.org/maven2/org/jruby/jruby-dist/"+tc.version+"/jruby-dist-"+tc.version+"-src.zip")

require.NoError(t, r.Build(context.Background(), s, src, run, &output.OutData{}))
assert.True(t, hasDownload(f, tc.wantJDK), "should download %s", tc.wantJDK)
assert.True(t, hasDownloadContaining(f, tc.wantMaven), "should download %s", tc.wantMaven)
assert.True(t, hasCallMatching(run.Calls, "sh", "export JAVA_HOME="), "mvn call sets JAVA_HOME")
assert.True(t, hasCallMatching(run.Calls, "sh", tc.wantJavaHome), "JAVA_HOME must point to %s", tc.wantJavaHome)
})
}
}

func TestJRubyRecipeUnknownVersion(t *testing.T) {
f := newFakeFetcher()
r := &recipe.JRubyRecipe{Fetcher: f}
Expand All @@ -946,6 +1011,7 @@ func TestJRubyRecipeUnknownVersion(t *testing.T) {
err := r.Build(context.Background(), s, src, run, &output.OutData{})
require.Error(t, err)
assert.Contains(t, err.Error(), "9.9")
assert.Contains(t, err.Error(), "9.3, 9.4, 10.0, 10.1")
}

// ── HTTPDRecipe ───────────────────────────────────────────────────────────────
Expand Down
4 changes: 3 additions & 1 deletion internal/stack/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ type BootstrapBinary struct {
type BootstrapConfig struct {
Go BootstrapBinary `yaml:"go"`
JRuby BootstrapBinary `yaml:"jruby"`
Ruby BootstrapBinary `yaml:"ruby"`
// JRubyJDK21 is the JDK used to build JRuby 10.x, which requires Java 21+.
JRubyJDK21 BootstrapBinary `yaml:"jruby_jdk21"`
Ruby BootstrapBinary `yaml:"ruby"`
}

// PythonConfig holds Python-specific build settings.
Expand Down
13 changes: 13 additions & 0 deletions internal/stack/stack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,19 @@ func TestJRubyConfigCflinuxfs4(t *testing.T) {
assert.Equal(t, "/opt/java", s.Bootstrap.JRuby.InstallDir)
}

func TestJRubyJDK21Config(t *testing.T) {
for _, name := range []string{"cflinuxfs4", "cflinuxfs5"} {
t.Run(name, func(t *testing.T) {
s, err := stack.Load(stacksDir(t), name)
require.NoError(t, err)

assert.Contains(t, s.Bootstrap.JRubyJDK21.URL, "jdk21")
assert.NotEmpty(t, s.Bootstrap.JRubyJDK21.SHA256)
assert.Equal(t, "/opt/java21", s.Bootstrap.JRubyJDK21.InstallDir)
})
}
}

func TestJRubyConfigCflinuxfs5(t *testing.T) {
s, err := stack.Load(stacksDir(t), "cflinuxfs5")
require.NoError(t, err)
Expand Down
4 changes: 4 additions & 0 deletions stacks/cflinuxfs4.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ bootstrap:
url: https://java-buildpack.cloudfoundry.org/openjdk-jdk/jammy/x86_64/bellsoft-jdk8u452%2B11-linux-amd64.tar.gz
sha256: d87d70d286150e662f12650ed22d91f1663c267b27ab3b0775eed1416ef3bd12
install_dir: /opt/java
jruby_jdk21:
url: https://java-buildpack.cloudfoundry.org/openjdk-jdk/jammy/x86_64/bellsoft-jdk21.0.7%2B9-linux-amd64.tar.gz
sha256: 52104f50eeb5f5299ea83dd25262a2f9847ee57b0240adced538cbebb19a42f2
install_dir: /opt/java21
ruby:
url: https://buildpacks.cloudfoundry.org/dependencies/ruby/ruby_3.3.6_linux_x64_cflinuxfs4_e4311262.tgz
sha256: e4311262803d0e1a01c83001e3e98ad58700b5cc36eff12433f24312b11e3dc9
Expand Down
4 changes: 4 additions & 0 deletions stacks/cflinuxfs5.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ bootstrap:
url: https://java-buildpack.cloudfoundry.org/openjdk-jdk/jammy/x86_64/bellsoft-jdk8u452%2B11-linux-amd64.tar.gz
sha256: d87d70d286150e662f12650ed22d91f1663c267b27ab3b0775eed1416ef3bd12
install_dir: /opt/java
jruby_jdk21:
url: https://java-buildpack.cloudfoundry.org/openjdk-jdk/jammy/x86_64/bellsoft-jdk21.0.7%2B9-linux-amd64.tar.gz
sha256: 52104f50eeb5f5299ea83dd25262a2f9847ee57b0240adced538cbebb19a42f2
install_dir: /opt/java21
ruby:
url: https://buildpacks.cloudfoundry.org/dependencies/ruby/ruby_3.3.6_linux_x64_cflinuxfs4_e4311262.tgz
sha256: e4311262803d0e1a01c83001e3e98ad58700b5cc36eff12433f24312b11e3dc9
Expand Down
Loading