From 3fee56c8e1bcfea6fa5d0e3382678a376c8b682d Mon Sep 17 00:00:00 2001 From: BG Date: Thu, 5 Nov 2026 10:18:56 +0100 Subject: [PATCH 1/2] [internal][recipe] Add JRuby -> Ruby mapping --- internal/recipe/jruby.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/recipe/jruby.go b/internal/recipe/jruby.go index 44fd81b4..81a2c295 100644 --- a/internal/recipe/jruby.go +++ b/internal/recipe/jruby.go @@ -51,8 +51,10 @@ 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", } // mavenVersion is the pinned Maven version used for the JRuby build. From d8fb432e5648bcad41fe7af8b917202fa437f040 Mon Sep 17 00:00:00 2001 From: BG Date: Thu, 5 Nov 2026 11:08:43 +0100 Subject: [PATCH 2/2] [internal][recipe] Build JRuby 10.x with JDK 21 and Maven 3.9 --- internal/recipe/jruby.go | 93 ++++++++++++++++++++----- internal/recipe/recipe_compiled_test.go | 66 ++++++++++++++++++ internal/stack/stack.go | 4 +- internal/stack/stack_test.go | 13 ++++ stacks/cflinuxfs4.yaml | 4 ++ stacks/cflinuxfs5.yaml | 4 ++ 6 files changed, 166 insertions(+), 18 deletions(-) diff --git a/internal/recipe/jruby.go b/internal/recipe/jruby.go index 81a2c295..8f491c8b 100644 --- a/internal/recipe/jruby.go +++ b/internal/recipe/jruby.go @@ -5,6 +5,8 @@ import ( "fmt" "os" "path/filepath" + "sort" + "strconv" "strings" "github.com/cloudfoundry/binary-builder/internal/archive" @@ -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. // @@ -57,12 +60,38 @@ var jrubyToRubyVersion = map[string]string{ "10.1": "4.0", } -// 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 @@ -83,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) } @@ -108,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) } @@ -205,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). @@ -216,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 } diff --git a/internal/recipe/recipe_compiled_test.go b/internal/recipe/recipe_compiled_test.go index c0acb3c0..e7d3d037 100644 --- a/internal/recipe/recipe_compiled_test.go +++ b/internal/recipe/recipe_compiled_test.go @@ -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", @@ -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} @@ -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 ─────────────────────────────────────────────────────────────── diff --git a/internal/stack/stack.go b/internal/stack/stack.go index d30fa392..abcd74b0 100644 --- a/internal/stack/stack.go +++ b/internal/stack/stack.go @@ -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. diff --git a/internal/stack/stack_test.go b/internal/stack/stack_test.go index 729bbc55..f09740b9 100644 --- a/internal/stack/stack_test.go +++ b/internal/stack/stack_test.go @@ -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) diff --git a/stacks/cflinuxfs4.yaml b/stacks/cflinuxfs4.yaml index e6d3364d..f447fb85 100644 --- a/stacks/cflinuxfs4.yaml +++ b/stacks/cflinuxfs4.yaml @@ -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 diff --git a/stacks/cflinuxfs5.yaml b/stacks/cflinuxfs5.yaml index 7dd21b35..3fc68563 100644 --- a/stacks/cflinuxfs5.yaml +++ b/stacks/cflinuxfs5.yaml @@ -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