Skip to content

refactor: extract BaseJRE to eliminate duplication across standard JRE providers#1288

Open
stokpop wants to merge 2 commits into
cloudfoundry:mainfrom
stokpop:issue-1264-base-jre-refactor
Open

refactor: extract BaseJRE to eliminate duplication across standard JRE providers#1288
stokpop wants to merge 2 commits into
cloudfoundry:mainfrom
stokpop:issue-1264-base-jre-refactor

Conversation

@stokpop
Copy link
Copy Markdown
Contributor

@stokpop stokpop commented May 20, 2026

Closes #1264

Summary

Reduces ~1300 lines of near-identical code in 6 JRE files to a single BaseJRE struct (243 lines) plus thin per-JRE wrappers (~11 lines each).

Design

BaseJRE uses a template method pattern with injected config/function fields so implementers never need to override Supply/Finalize, eliminating the 'forgot to call base' risk from method-override approaches.

Variation points:

  • dirPrefixes/dirExacts: drive findJavaHome directory matching per JRE
  • extraFinalizeOpts: hook for JRE-specific JAVA_OPTS (used by IBM JRE: -Xtune:virtualized -Xshareclasses:none)
  • installErrNote: extra context in install error messages (GraalVM: ensure repository_root is configured)

ZingJRE is intentionally excluded — it has no memory calculator or jvmkill and has genuinely different Finalize behaviour.

Before / After

Before After
openjdk.go 254 lines 11 lines
zulu.go 219 lines 11 lines
sapmachine.go 219 lines 11 lines
ibm.go 223 lines 13 lines
oracle.go 212 lines 11 lines
graalvm.go 219 lines 11 lines
base_jre.go 243 lines
Total 1346 lines 311 lines

Tests

Added standard_jres_test.go covering all 5 non-OpenJDK standard JREs (OpenJDK already had openjdk_test.go). For each JRE:

  • Name() returns the correct display string
  • Detect() triggers on the correct env var and not on others
  • Version() / JavaHome() are empty before installation
  • findJavaHome (via Finalize()) locates the JRE-specific subdirectory prefix correctly

All existing and new tests pass.

stokpop added 2 commits May 20, 2026 12:21
…E providers

Reduces ~1300 lines of near-identical code in 6 JRE files to a single
BaseJRE struct (243 lines) plus thin per-JRE wrappers (~11 lines each).

BaseJRE uses a template method pattern with injected config/function fields
so implementers never need to override Supply/Finalize, eliminating the
'forgot to call base' risk from method-override approaches.

Variation points:
- dirPrefixes/dirExacts: drive findJavaHome directory matching per JRE
- extraFinalizeOpts: hook for JRE-specific JAVA_OPTS (used by IBM JRE)
- installErrNote: extra context in install error messages (GraalVM)

Zing JRE is intentionally excluded — it has no memory calculator or
jvmkill and has different Finalize behaviour.

Fixes cloudfoundry#1264
DetermineJavaVersion previously returned 17 silently when the JRE release
file was missing. This made it impossible to diagnose broken or incomplete
JRE installations.

Changes:
- DetermineJavaVersion now returns an error for missing/unreadable release files
- BaseJRE.Finalize() logs a WARNING when falling back to Java 17
- Tomcat.Supply() logs a WARNING when falling back to Java 17
- Updated tests to reflect the new error behaviour
@stokpop
Copy link
Copy Markdown
Contributor Author

stokpop commented May 20, 2026

Added a follow-up commit that improves observability when the JRE release file is missing or unreadable:

  • DetermineJavaVersion now returns an error instead of silently returning 17 when the release file is absent
  • BaseJRE.Finalize() and Tomcat.Supply() both log a **WARNING** when falling back to Java 17

This means a broken or incomplete JRE installation will now produce a visible warning in buildpack output rather than silently proceeding with Java 17 assumptions.

@ramonskie
Copy link
Copy Markdown
Contributor

ramonskie commented May 21, 2026

a quick ai review. (with my own context/skilss)
gave me

🔴 Behavioral Change: tomcat.go — version selection now runs unconditionally when JAVA_HOME is set, even if version detection fails
File: src/java/containers/tomcat.go
Before: If DetermineJavaVersion returned an error (e.g., missing/unreadable release file), the entire Tomcat version selection block was skipped. The code fell through to the manifest default version.
After: On error, javaMajorVersion is silently defaulted to 17, and Tomcat version selection proceeds. The fallback to manifest default (dep.Version == "") is now only reached when JAVA_HOME is not set at all.
Concrete failure scenario: An app using IBM JRE (typically Java 8) where the release file is absent or unreadable. Previously: manifest default Tomcat version used. Now: Tomcat 10.x selected (Java 17 default → javaMajorVersion >= 11 → versionPattern = "10.x"). Tomcat 10.x is Jakarta EE 9+ and is incompatible with Java EE 8 apps. The app will fail at runtime, not at staging.
The javaMajorVersion < 11 guard on line 89 (Tomcat 10.x requires Java 11+) is also bypassed in this scenario because the defaulted version is 17.

🔴 Bug: BaseJRE.Supply() now calls WriteEnvFile, AddBinDependencyLink, and LinkDirectoryInDepDir for ALL JREs — previously only OpenJDK did this
Files: src/java/jres/base_jre.go (new), vs old ibm.go, zulu.go, sapmachine.go, oracle.go, graalvm.go
The old per-JRE implementations of Supply() for IBM, Zulu, SapMachine, Oracle, and GraalVM did not call:

  • ctx.Stager.WriteEnvFile("JAVA_HOME", javaHome) — writes JAVA_HOME to the deps env dir for subsequent buildpacks
  • ctx.Stager.AddBinDependencyLink(javaBin, "java") — symlinks java binary into the shared bin/ dir (puts it on PATH for subsequent buildpacks)
  • ctx.Stager.LinkDirectoryInDepDir(libDir, "lib") — links the JRE lib/ dir into deps (adds native libs to LD_LIBRARY_PATH)
    Only openjdk.go had these three calls. BaseJRE.Supply() now applies them to all six JREs.
    Impact: This is a behavioral expansion, not just a refactor. For multi-buildpack staging scenarios, Zulu/IBM/Oracle/SapMachine/GraalVM JREs will now expose JAVA_HOME, java on PATH, and native libs to subsequent buildpacks — they did not before. This is likely the correct behavior (it was probably an oversight in the original per-JRE implementations), but it is an undocumented behavioral change that could affect existing multi-buildpack setups if a subsequent buildpack was relying on JAVA_HOME not being set.

🟡 DetermineJavaVersion no longer handles missing release file silently
Previously returned (17, nil) for missing files; now returns (0, error). All callers in this PR handle it, but the safe default is now scattered — future callers could get 0 instead of a safe default if they forget to handle the error.

🟡 Finalize() may use empty b.version
If Finalize() is called without a prior Supply() (multi-buildpack staging model), b.version will be "". Not a regression from the old code, but the consolidation makes it more visible.

🟡 BaseJRE.Finalize() — WriteJavaOpts with base opts now applies to ALL JREs
File: src/java/jres/base_jre.go
The old openjdk.go Finalize() wrote base JAVA_OPTS (-Djava.io.tmpdir=$TMPDIR -XX:ActiveProcessorCount=$(nproc)). The old IBM, Zulu, SapMachine, Oracle, and GraalVM Finalize() implementations did not write these base opts.
BaseJRE.Finalize() now writes these opts for all JREs. This is likely correct behavior, but it is an undocumented behavioral expansion. For IBM JRE specifically, -XX:ActiveProcessorCount is a HotSpot flag and may not be recognized by IBM J9 JVM, potentially causing a startup warning or error depending on the IBM JRE version.

i' am okay with the things below.
🟢 IBM JRE default Java version changed
Old IBM JRE defaulted to Java 8 on version detection failure; BaseJRE now defaults to 17 for all JREs. Low severity since IBM JRE is rarely used, but it's a silent behavioral change.

i will still do a full review later on

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor duplicate JRE implementations into shared BaseJRE struct

2 participants