From 9a7f22dbcfc8cfdb79acf3682b347297bdb73a62 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Wed, 20 May 2026 12:21:11 +0000 Subject: [PATCH 1/2] refactor: extract BaseJRE to eliminate duplication across standard JRE providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 #1264 --- src/java/jres/base_jre.go | 243 ++++++++++++++++++++++++++ src/java/jres/graalvm.go | 218 +----------------------- src/java/jres/ibm.go | 224 +----------------------- src/java/jres/openjdk.go | 253 +--------------------------- src/java/jres/oracle.go | 211 +---------------------- src/java/jres/sapmachine.go | 218 +----------------------- src/java/jres/standard_jres_test.go | 248 +++++++++++++++++++++++++++ src/java/jres/zulu.go | 218 +----------------------- 8 files changed, 523 insertions(+), 1310 deletions(-) create mode 100644 src/java/jres/base_jre.go create mode 100644 src/java/jres/standard_jres_test.go diff --git a/src/java/jres/base_jre.go b/src/java/jres/base_jre.go new file mode 100644 index 000000000..efea0036d --- /dev/null +++ b/src/java/jres/base_jre.go @@ -0,0 +1,243 @@ +package jres + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/cloudfoundry/java-buildpack/src/java/common" +) + +// BaseJRE provides the shared implementation for all standard JRE providers. +// Variation between JREs is injected via fields — implementers never override +// Supply/Finalize, avoiding the "forgot to call base" footgun. +type BaseJRE struct { + ctx *common.Context + jreDir string + version string + javaHome string + memoryCalc *MemoryCalculator + jvmkill *JVMKillAgent + installedVersion string + + jreName string + jreKey string + dirPrefixes []string + dirExacts []string + installErrNote string + + extraFinalizeOpts func() string +} + +func newBaseJRE(ctx *common.Context, jreName, jreKey string, dirPrefixes, dirExacts []string, installErrNote string) BaseJRE { + return BaseJRE{ + ctx: ctx, + jreDir: filepath.Join(ctx.Stager.DepDir(), "jre"), + jreName: jreName, + jreKey: jreKey, + dirPrefixes: dirPrefixes, + dirExacts: dirExacts, + installErrNote: installErrNote, + } +} + +func (b *BaseJRE) Name() string { + return b.jreName +} + +func (b *BaseJRE) Detect() (bool, error) { + return DetectJREByEnv(b.jreKey), nil +} + +func (b *BaseJRE) Supply() error { + b.ctx.Log.BeginStep("Installing %s", b.jreName) + + dep, err := GetJREVersion(b.ctx, b.jreKey) + if err != nil { + return fmt.Errorf("failed to determine %s version from manifest: %w", b.jreName, err) + } + + b.version = dep.Version + b.ctx.Log.Info("Installing %s (%s)", b.jreName, b.version) + + if err := b.ctx.Installer.InstallDependency(dep, b.jreDir); err != nil { + if b.installErrNote != "" { + return fmt.Errorf("failed to install %s: %w %s", b.jreName, err, b.installErrNote) + } + return fmt.Errorf("failed to install %s: %w", b.jreName, err) + } + + javaHome, err := b.findJavaHome() + if err != nil { + return fmt.Errorf("failed to find JAVA_HOME: %w", err) + } + b.javaHome = javaHome + b.installedVersion = b.version + + if err := b.writeProfileDScript(); err != nil { + b.ctx.Log.Warning("Could not write java.sh profile.d script: %s", err.Error()) + } else { + b.ctx.Log.Debug("Created profile.d script: java.sh") + } + + if err := b.ctx.Stager.WriteEnvFile("JAVA_HOME", javaHome); err != nil { + b.ctx.Log.Warning("Could not write JAVA_HOME env file: %s", err.Error()) + } + + javaBin := filepath.Join(javaHome, "bin", "java") + if err := b.ctx.Stager.AddBinDependencyLink(javaBin, "java"); err != nil { + b.ctx.Log.Warning("Could not add java bin dependency link: %s", err.Error()) + } + + libDir := filepath.Join(javaHome, "lib") + if _, err := os.Stat(libDir); err == nil { + if err := b.ctx.Stager.LinkDirectoryInDepDir(libDir, "lib"); err != nil { + b.ctx.Log.Warning("Could not link JRE lib directory: %s", err.Error()) + } + } + + javaMajorVersion, err := common.DetermineJavaVersion(javaHome) + if err != nil { + b.ctx.Log.Warning("Could not determine Java version: %s", err.Error()) + javaMajorVersion = 17 + } + b.ctx.Log.Info("Detected Java major version: %d", javaMajorVersion) + + b.jvmkill = NewJVMKillAgent(b.ctx, b.jreDir, b.version) + if err := b.jvmkill.Supply(); err != nil { + b.ctx.Log.Warning("Failed to install JVMKill agent: %s (continuing)", err.Error()) + } + + b.memoryCalc = NewMemoryCalculator(b.ctx, b.jreDir, b.version, javaMajorVersion, b.jreKey) + if err := b.memoryCalc.Supply(); err != nil { + b.ctx.Log.Warning("Failed to install Memory Calculator: %s (continuing)", err.Error()) + } + + b.ctx.Log.Info("%s installation complete", b.jreName) + return nil +} + +func (b *BaseJRE) Finalize() error { + b.ctx.Log.BeginStep("Finalizing %s configuration", b.jreName) + + if b.javaHome == "" { + javaHome, err := b.findJavaHome() + if err != nil { + b.ctx.Log.Warning("Failed to find JAVA_HOME: %s", err.Error()) + } else { + b.javaHome = javaHome + } + } + + if b.javaHome != "" { + if err := os.Setenv("JAVA_HOME", b.javaHome); err != nil { + b.ctx.Log.Warning("Failed to set JAVA_HOME environment variable: %s", err.Error()) + } else { + b.ctx.Log.Debug("Set JAVA_HOME=%s", b.javaHome) + } + } + + javaMajorVersion := 17 + if b.javaHome != "" { + if ver, err := common.DetermineJavaVersion(b.javaHome); err == nil { + javaMajorVersion = ver + } + } + + if b.jvmkill == nil { + b.jvmkill = NewJVMKillAgent(b.ctx, b.jreDir, b.version) + } + if err := b.jvmkill.Finalize(); err != nil { + b.ctx.Log.Warning("Failed to finalize JVMKill agent: %s", err.Error()) + } + + baseOpts := []string{ + "-Djava.io.tmpdir=$TMPDIR", + "-XX:ActiveProcessorCount=$(nproc)", + } + if err := WriteJavaOpts(b.ctx, strings.Join(baseOpts, " ")); err != nil { + b.ctx.Log.Warning("Failed to write base JAVA_OPTS: %s", err.Error()) + } + + if b.memoryCalc == nil { + b.memoryCalc = NewMemoryCalculator(b.ctx, b.jreDir, b.version, javaMajorVersion, b.jreKey) + } + if err := b.memoryCalc.Finalize(); err != nil { + b.ctx.Log.Warning("Failed to finalize Memory Calculator: %s", err.Error()) + } + + if b.extraFinalizeOpts != nil { + if extraOpts := b.extraFinalizeOpts(); extraOpts != "" { + if err := WriteJavaOpts(b.ctx, extraOpts); err != nil { + b.ctx.Log.Warning("Failed to write %s JAVA_OPTS: %s", b.jreName, err.Error()) + } + } + } + + b.ctx.Log.Info("%s finalization complete", b.jreName) + return nil +} + +func (b *BaseJRE) JavaHome() string { + return b.javaHome +} + +func (b *BaseJRE) Version() string { + return b.installedVersion +} + +func (b *BaseJRE) MemoryCalculatorCommand() string { + if b.memoryCalc == nil { + return "" + } + return b.memoryCalc.GetCalculatorCommand() +} + +func (b *BaseJRE) findJavaHome() (string, error) { + entries, err := os.ReadDir(b.jreDir) + if err != nil { + return "", fmt.Errorf("failed to read JRE directory: %w", err) + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + name := entry.Name() + matches := false + for _, prefix := range b.dirPrefixes { + if strings.HasPrefix(name, prefix) { + matches = true + break + } + } + if !matches { + for _, exact := range b.dirExacts { + if name == exact { + matches = true + break + } + } + } + if !matches { + continue + } + + path := filepath.Join(b.jreDir, name) + if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil { + return path, nil + } + } + + if _, err := os.Stat(filepath.Join(b.jreDir, "bin", "java")); err == nil { + return b.jreDir, nil + } + + return "", fmt.Errorf("could not find valid JAVA_HOME in %s", b.jreDir) +} + +func (b *BaseJRE) writeProfileDScript() error { + return WriteJavaHomeProfileD(b.ctx, b.jreDir, b.javaHome) +} diff --git a/src/java/jres/graalvm.go b/src/java/jres/graalvm.go index 86a9a6662..5fdce9ba2 100644 --- a/src/java/jres/graalvm.go +++ b/src/java/jres/graalvm.go @@ -1,219 +1,11 @@ package jres -import ( - "fmt" - "github.com/cloudfoundry/java-buildpack/src/java/common" - "os" - "path/filepath" -) +import "github.com/cloudfoundry/java-buildpack/src/java/common" -// GraalVMJRE implements the JRE interface for GraalVM -type GraalVMJRE struct { - ctx *common.Context - jreDir string - version string - javaHome string - memoryCalc *MemoryCalculator - jvmkill *JVMKillAgent - installedVersion string -} +// GraalVMJRE implements the JRE interface for GraalVM. +type GraalVMJRE struct{ BaseJRE } -// NewGraalVMJRE creates a new GraalVM JRE provider +// NewGraalVMJRE creates a new GraalVM JRE provider. func NewGraalVMJRE(ctx *common.Context) *GraalVMJRE { - jreDir := filepath.Join(ctx.Stager.DepDir(), "jre") - - return &GraalVMJRE{ - ctx: ctx, - jreDir: jreDir, - } -} - -// Name returns the name of this JRE provider -func (g *GraalVMJRE) Name() string { - return "GraalVM" -} - -// Detect returns true if GraalVM JRE should be used -// GraalVM is selected via JBP_CONFIG_GRAAL_VM_JRE environment variable -func (g *GraalVMJRE) Detect() (bool, error) { - return DetectJREByEnv("graalvm"), nil -} - -// Supply installs the GraalVM JRE and its components -func (g *GraalVMJRE) Supply() error { - g.ctx.Log.BeginStep("Installing GraalVM JRE") - - // Determine version - dep, err := GetJREVersion(g.ctx, "graalvm") - if err != nil { - return fmt.Errorf("failed to determine GraalVM version from manifest: %w", err) - } - - g.version = dep.Version - g.ctx.Log.Info("Installing GraalVM (%s)", g.version) - - // Install JRE - if err := g.ctx.Installer.InstallDependency(dep, g.jreDir); err != nil { - return fmt.Errorf("failed to install GraalVM: %w (ensure repository_root is configured)", err) - } - - // Find the actual JAVA_HOME (handle nested directories from tar extraction) - javaHome, err := g.findJavaHome() - if err != nil { - return fmt.Errorf("failed to find JAVA_HOME: %w", err) - } - g.javaHome = javaHome - g.installedVersion = g.version - - // Write profile.d script for runtime environment - if err := g.writeProfileDScript(); err != nil { - g.ctx.Log.Warning("Could not write java.sh profile.d script: %s", err.Error()) - } else { - g.ctx.Log.Debug("Created profile.d script: java.sh") - } - - // Determine Java major version - javaMajorVersion, err := common.DetermineJavaVersion(javaHome) - if err != nil { - g.ctx.Log.Warning("Could not determine Java version: %s", err.Error()) - javaMajorVersion = 17 // default for GraalVM - } - g.ctx.Log.Info("Detected Java major version: %d", javaMajorVersion) - - // Install JVMKill agent - g.jvmkill = NewJVMKillAgent(g.ctx, g.jreDir, g.version) - if err := g.jvmkill.Supply(); err != nil { - g.ctx.Log.Warning("Failed to install JVMKill agent: %s (continuing)", err.Error()) - // Non-fatal - continue without jvmkill - } - - // Install Memory Calculator - g.memoryCalc = NewMemoryCalculator(g.ctx, g.jreDir, g.version, javaMajorVersion, "graalvm") - if err := g.memoryCalc.Supply(); err != nil { - g.ctx.Log.Warning("Failed to install Memory Calculator: %s (continuing)", err.Error()) - // Non-fatal - continue without memory calculator - } - - g.ctx.Log.Info("GraalVM JRE installation complete") - return nil -} - -// Finalize performs final JRE configuration -func (g *GraalVMJRE) Finalize() error { - g.ctx.Log.BeginStep("Finalizing GraalVM JRE configuration") - - // Find the actual JAVA_HOME (needed if finalize is called on a fresh instance) - if g.javaHome == "" { - javaHome, err := g.findJavaHome() - if err != nil { - g.ctx.Log.Warning("Failed to find JAVA_HOME: %s", err.Error()) - } else { - g.javaHome = javaHome - } - } - - // Set JAVA_HOME in environment for frameworks that need it during finalize phase - // (e.g., Luna Security Provider, Container Security Provider) - if g.javaHome != "" { - if err := os.Setenv("JAVA_HOME", g.javaHome); err != nil { - g.ctx.Log.Warning("Failed to set JAVA_HOME environment variable: %s", err.Error()) - } else { - g.ctx.Log.Debug("Set JAVA_HOME=%s", g.javaHome) - } - } - - // Determine Java major version for memory calculator - javaMajorVersion := 17 // default - if g.javaHome != "" { - if ver, err := common.DetermineJavaVersion(g.javaHome); err == nil { - javaMajorVersion = ver - } - } - - // Reconstruct JVMKill agent component if not already set - if g.jvmkill == nil { - g.jvmkill = NewJVMKillAgent(g.ctx, g.jreDir, g.version) - } - - // Finalize JVMKill agent - if err := g.jvmkill.Finalize(); err != nil { - g.ctx.Log.Warning("Failed to finalize JVMKill agent: %s", err.Error()) - // Non-fatal - } - - // Reconstruct Memory Calculator component if not already set - if g.memoryCalc == nil { - g.memoryCalc = NewMemoryCalculator(g.ctx, g.jreDir, g.version, javaMajorVersion, "graalvm") - } - - // Finalize Memory Calculator - if err := g.memoryCalc.Finalize(); err != nil { - g.ctx.Log.Warning("Failed to finalize Memory Calculator: %s", err.Error()) - // Non-fatal - } - - g.ctx.Log.Info("GraalVM JRE finalization complete") - return nil -} - -// JavaHome returns the path to JAVA_HOME -func (g *GraalVMJRE) JavaHome() string { - return g.javaHome -} - -// Version returns the installed JRE version -func (g *GraalVMJRE) Version() string { - return g.installedVersion -} - -// MemoryCalculatorCommand returns the shell command snippet to run memory calculator at runtime -func (g *GraalVMJRE) MemoryCalculatorCommand() string { - if g.memoryCalc == nil { - return "" - } - return g.memoryCalc.GetCalculatorCommand() -} - -// findJavaHome locates the actual JAVA_HOME directory after extraction -// GraalVM tarballs usually extract to graalvm-* or jdk-* subdirectories -func (g *GraalVMJRE) findJavaHome() (string, error) { - entries, err := os.ReadDir(g.jreDir) - if err != nil { - return "", fmt.Errorf("failed to read JRE directory: %w", err) - } - - // Look for graalvm-*, jdk-*, or jre-* subdirectory - for _, entry := range entries { - if entry.IsDir() { - name := entry.Name() - // Check for GraalVM directory patterns - if len(name) > 7 && name[:7] == "graalvm" { - path := filepath.Join(g.jreDir, name) - // Verify it has a bin directory with java - if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil { - return path, nil - } - } - // Also check for standard jdk/jre patterns - if len(name) > 3 && (name[:3] == "jdk" || name[:3] == "jre") { - path := filepath.Join(g.jreDir, name) - if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil { - return path, nil - } - } - } - } - - // If no subdirectory found, check if jreDir itself is valid - if _, err := os.Stat(filepath.Join(g.jreDir, "bin", "java")); err == nil { - return g.jreDir, nil - } - - return "", fmt.Errorf("could not find valid JAVA_HOME in %s", g.jreDir) -} - -// writeProfileDScript creates a profile.d script that exports JAVA_HOME, JRE_HOME, and PATH at runtime -// Delegates to the shared helper function in jre.go -func (g *GraalVMJRE) writeProfileDScript() error { - return WriteJavaHomeProfileD(g.ctx, g.jreDir, g.javaHome) + return &GraalVMJRE{newBaseJRE(ctx, "GraalVM", "graalvm", []string{"graalvm"}, nil, "(ensure repository_root is configured)")} } diff --git a/src/java/jres/ibm.go b/src/java/jres/ibm.go index a6fa1662e..33f87a106 100644 --- a/src/java/jres/ibm.go +++ b/src/java/jres/ibm.go @@ -1,223 +1,13 @@ package jres -import ( - "fmt" - "github.com/cloudfoundry/java-buildpack/src/java/common" - "os" - "path/filepath" -) +import "github.com/cloudfoundry/java-buildpack/src/java/common" -// IBMJRE implements the JRE interface for IBM JRE -// IBM JRE requires a user-provided repository via JBP_CONFIG_IBM_JRE environment variable -// IBM JRE adds specific JVM options: -Xtune:virtualized -Xshareclasses:none -type IBMJRE struct { - ctx *common.Context - jreDir string - version string - javaHome string - memoryCalc *MemoryCalculator - jvmkill *JVMKillAgent - installedVersion string -} +// IBMJRE implements the JRE interface for IBM JRE. +type IBMJRE struct{ BaseJRE } -// NewIBMJRE creates a new IBM JRE provider +// NewIBMJRE creates a new IBM JRE provider. func NewIBMJRE(ctx *common.Context) *IBMJRE { - jreDir := filepath.Join(ctx.Stager.DepDir(), "jre") - - return &IBMJRE{ - ctx: ctx, - jreDir: jreDir, - } -} - -// Name returns the name of this JRE provider -func (i *IBMJRE) Name() string { - return "IBM JRE" -} - -// Detect returns true if IBM JRE should be used -// IBM JRE requires explicit configuration via JBP_CONFIG_IBM_JRE environment variable -func (i *IBMJRE) Detect() (bool, error) { - return DetectJREByEnv("ibm"), nil -} - -// Supply installs the IBM JRE and its components -func (i *IBMJRE) Supply() error { - i.ctx.Log.BeginStep("Installing IBM JRE") - - // Determine version - dep, err := GetJREVersion(i.ctx, "ibm") - if err != nil { - return fmt.Errorf("failed to determine IBM JRE version from manifest: %w", err) - } - - i.version = dep.Version - i.ctx.Log.Info("Installing IBM JRE (%s)", i.version) - - // Install JRE - if err := i.ctx.Installer.InstallDependency(dep, i.jreDir); err != nil { - return fmt.Errorf("failed to install IBM JRE: %w", err) - } - - // Find the actual JAVA_HOME (handle nested directories from tar extraction) - javaHome, err := i.findJavaHome() - if err != nil { - return fmt.Errorf("failed to find JAVA_HOME: %w", err) - } - i.javaHome = javaHome - i.installedVersion = i.version - - // Write profile.d script for runtime environment - if err := i.writeProfileDScript(); err != nil { - i.ctx.Log.Warning("Could not write java.sh profile.d script: %s", err.Error()) - } else { - i.ctx.Log.Debug("Created profile.d script: java.sh") - } - - // Determine Java major version - javaMajorVersion, err := common.DetermineJavaVersion(javaHome) - if err != nil { - i.ctx.Log.Warning("Could not determine Java version: %s", err.Error()) - javaMajorVersion = 8 // IBM JRE default - } - i.ctx.Log.Info("Detected Java major version: %d", javaMajorVersion) - - // Install JVMKill agent (using IBM-specific repository if configured) - i.jvmkill = NewJVMKillAgent(i.ctx, i.jreDir, i.version) - if err := i.jvmkill.Supply(); err != nil { - i.ctx.Log.Warning("Failed to install JVMKill agent: %s (continuing)", err.Error()) - // Non-fatal - continue without jvmkill - } - - // Install Memory Calculator - i.memoryCalc = NewMemoryCalculator(i.ctx, i.jreDir, i.version, javaMajorVersion, "ibm") - if err := i.memoryCalc.Supply(); err != nil { - i.ctx.Log.Warning("Failed to install Memory Calculator: %s (continuing)", err.Error()) - // Non-fatal - continue without memory calculator - } - - i.ctx.Log.Info("IBM JRE installation complete") - return nil -} - -// Finalize performs final JRE configuration -// Adds IBM-specific JVM options: -Xtune:virtualized -Xshareclasses:none -func (i *IBMJRE) Finalize() error { - i.ctx.Log.BeginStep("Finalizing IBM JRE configuration") - - // Find the actual JAVA_HOME (needed if finalize is called on a fresh instance) - if i.javaHome == "" { - javaHome, err := i.findJavaHome() - if err != nil { - i.ctx.Log.Warning("Failed to find JAVA_HOME: %s", err.Error()) - } else { - i.javaHome = javaHome - } - } - - // Set JAVA_HOME in environment for frameworks that need it during finalize phase - // (e.g., Luna Security Provider, Container Security Provider) - if i.javaHome != "" { - if err := os.Setenv("JAVA_HOME", i.javaHome); err != nil { - i.ctx.Log.Warning("Failed to set JAVA_HOME environment variable: %s", err.Error()) - } else { - i.ctx.Log.Debug("Set JAVA_HOME=%s", i.javaHome) - } - } - - // Determine Java major version for memory calculator - javaMajorVersion := 8 // IBM JRE default - if i.javaHome != "" { - if ver, err := common.DetermineJavaVersion(i.javaHome); err == nil { - javaMajorVersion = ver - } - } - - // Reconstruct JVMKill agent component if not already set - if i.jvmkill == nil { - i.jvmkill = NewJVMKillAgent(i.ctx, i.jreDir, i.version) - } - - // Finalize JVMKill agent - if err := i.jvmkill.Finalize(); err != nil { - i.ctx.Log.Warning("Failed to finalize JVMKill agent: %s", err.Error()) - // Non-fatal - } - - // Reconstruct Memory Calculator component if not already set - if i.memoryCalc == nil { - i.memoryCalc = NewMemoryCalculator(i.ctx, i.jreDir, i.version, javaMajorVersion, "ibm") - } - - // Finalize Memory Calculator - if err := i.memoryCalc.Finalize(); err != nil { - i.ctx.Log.Warning("Failed to finalize Memory Calculator: %s", err.Error()) - // Non-fatal - } - - // Add IBM-specific JVM options - // -Xtune:virtualized - Optimizes for virtualized environments - // -Xshareclasses:none - Disables class data sharing (not supported in containers) - ibmOpts := "-Xtune:virtualized -Xshareclasses:none" - if err := WriteJavaOpts(i.ctx, ibmOpts); err != nil { - i.ctx.Log.Warning("Failed to write IBM JVM options: %s", err.Error()) - // Non-fatal - } - - i.ctx.Log.Info("IBM JRE finalization complete") - return nil -} - -// JavaHome returns the path to JAVA_HOME -func (i *IBMJRE) JavaHome() string { - return i.javaHome -} - -// Version returns the installed JRE version -func (i *IBMJRE) Version() string { - return i.installedVersion -} - -// MemoryCalculatorCommand returns the shell command snippet to run memory calculator at runtime -func (i *IBMJRE) MemoryCalculatorCommand() string { - if i.memoryCalc == nil { - return "" - } - return i.memoryCalc.GetCalculatorCommand() -} - -// findJavaHome locates the actual JAVA_HOME directory after extraction -// IBM JRE tarballs usually extract to ibm-java-* or jre subdirectories -func (i *IBMJRE) findJavaHome() (string, error) { - entries, err := os.ReadDir(i.jreDir) - if err != nil { - return "", fmt.Errorf("failed to read JRE directory: %w", err) - } - - // Look for ibm-java-* or jre subdirectory - for _, entry := range entries { - if entry.IsDir() { - name := entry.Name() - // Check for common IBM JRE directory patterns - if (len(name) > 8 && name[:8] == "ibm-java") || name == "jre" { - path := filepath.Join(i.jreDir, name) - // Verify it has a bin directory with java - if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil { - return path, nil - } - } - } - } - - // If no subdirectory found, check if jreDir itself is valid - if _, err := os.Stat(filepath.Join(i.jreDir, "bin", "java")); err == nil { - return i.jreDir, nil - } - - return "", fmt.Errorf("could not find valid JAVA_HOME in %s", i.jreDir) -} - -// writeProfileDScript creates the profile.d script for setting JAVA_HOME, JRE_HOME, and PATH at runtime -func (i *IBMJRE) writeProfileDScript() error { - return WriteJavaHomeProfileD(i.ctx, i.jreDir, i.javaHome) + b := newBaseJRE(ctx, "IBM JRE", "ibm", []string{"ibm-java"}, []string{"jre"}, "") + b.extraFinalizeOpts = func() string { return "-Xtune:virtualized -Xshareclasses:none" } + return &IBMJRE{b} } diff --git a/src/java/jres/openjdk.go b/src/java/jres/openjdk.go index 5299d7261..b610876d5 100644 --- a/src/java/jres/openjdk.go +++ b/src/java/jres/openjdk.go @@ -1,254 +1,11 @@ package jres -import ( - "fmt" - "github.com/cloudfoundry/java-buildpack/src/java/common" - "os" - "path/filepath" - "strings" -) +import "github.com/cloudfoundry/java-buildpack/src/java/common" -// OpenJDKJRE implements the JRE interface for OpenJDK -type OpenJDKJRE struct { - ctx *common.Context - jreDir string - version string - javaHome string - memoryCalc *MemoryCalculator - jvmkill *JVMKillAgent - installedVersion string -} +// OpenJDKJRE implements the JRE interface for OpenJDK. +type OpenJDKJRE struct{ BaseJRE } -// NewOpenJDKJRE creates a new OpenJDK JRE provider +// NewOpenJDKJRE creates a new OpenJDK JRE provider. func NewOpenJDKJRE(ctx *common.Context) *OpenJDKJRE { - jreDir := filepath.Join(ctx.Stager.DepDir(), "jre") - - return &OpenJDKJRE{ - ctx: ctx, - jreDir: jreDir, - } -} - -// Name returns the name of this JRE provider -func (o *OpenJDKJRE) Name() string { - return "OpenJDK" -} - -// Detect returns true if OpenJDK should be used -// OpenJDK is selected via JBP_CONFIG_OPEN_JDK_JRE environment variable -func (o *OpenJDKJRE) Detect() (bool, error) { - return DetectJREByEnv("openjdk"), nil -} - -// Supply installs the OpenJDK JRE and its components -func (o *OpenJDKJRE) Supply() error { - o.ctx.Log.BeginStep("Installing OpenJDK JRE") - - // Determine version - dep, err := GetJREVersion(o.ctx, "openjdk") - if err != nil { - return fmt.Errorf("failed to determine OpenJDK version from manifest: %w", err) - } - - o.version = dep.Version - o.ctx.Log.Info("Installing OpenJDK (%s)", o.version) - - // Install JRE - if err := o.ctx.Installer.InstallDependency(dep, o.jreDir); err != nil { - return fmt.Errorf("failed to install OpenJDK: %w", err) - } - - // Find the actual JAVA_HOME (handle nested directories from tar extraction) - javaHome, err := o.findJavaHome() - if err != nil { - return fmt.Errorf("failed to find JAVA_HOME: %w", err) - } - o.javaHome = javaHome - o.installedVersion = o.version - - // Create profile.d script to export JAVA_HOME at runtime - // This is needed for containers like DistZip that use startup scripts expecting $JAVA_HOME - if err := o.writeProfileDScript(); err != nil { - o.ctx.Log.Warning("Could not write java.sh profile.d script: %s", err.Error()) - } else { - o.ctx.Log.Debug("Created profile.d script: java.sh") - } - - // Write JAVA_HOME to the deps env dir so subsequent buildpacks can use it. - // This is the multi-buildpack pattern used by go-buildpack (WriteEnvFile for GOROOT) - // and dotnet-core-buildpack. - if err := o.ctx.Stager.WriteEnvFile("JAVA_HOME", javaHome); err != nil { - o.ctx.Log.Warning("Could not write JAVA_HOME env file: %s", err.Error()) - } - - // Symlink the java binary into the shared bin directory so it is on PATH - // for subsequent buildpacks — mirrors go-buildpack's AddBinDependencyLink for "go". - javaBin := filepath.Join(javaHome, "bin", "java") - if err := o.ctx.Stager.AddBinDependencyLink(javaBin, "java"); err != nil { - o.ctx.Log.Warning("Could not add java bin dependency link: %s", err.Error()) - } - - // Link the JRE lib directory into the deps dir so native libraries (.so files) - // are included on LD_LIBRARY_PATH — mirrors dotnet-core-buildpack's LinkDirectoryInDepDir. - libDir := filepath.Join(javaHome, "lib") - if _, err := os.Stat(libDir); err == nil { - if err := o.ctx.Stager.LinkDirectoryInDepDir(libDir, "lib"); err != nil { - o.ctx.Log.Warning("Could not link JRE lib directory: %s", err.Error()) - } - } - - // Determine Java major version - javaMajorVersion, err := common.DetermineJavaVersion(javaHome) - if err != nil { - o.ctx.Log.Warning("Could not determine Java version: %s", err.Error()) - javaMajorVersion = 17 // default - } - o.ctx.Log.Info("Detected Java major version: %d", javaMajorVersion) - - // Install JVMKill agent - o.jvmkill = NewJVMKillAgent(o.ctx, o.jreDir, o.version) - if err := o.jvmkill.Supply(); err != nil { - o.ctx.Log.Warning("Failed to install JVMKill agent: %s (continuing)", err.Error()) - // Non-fatal - continue without jvmkill - } - - // Install Memory Calculator - o.memoryCalc = NewMemoryCalculator(o.ctx, o.jreDir, o.version, javaMajorVersion, "openjdk") - if err := o.memoryCalc.Supply(); err != nil { - o.ctx.Log.Warning("Failed to install Memory Calculator: %s (continuing)", err.Error()) - // Non-fatal - continue without memory calculator - } - - o.ctx.Log.Debug("OpenJDK JRE installation complete") - return nil -} - -// Finalize performs final JRE configuration -func (o *OpenJDKJRE) Finalize() error { - o.ctx.Log.Debug("Finalizing OpenJDK JRE configuration") - - // Find the actual JAVA_HOME (needed if finalize is called on a fresh instance) - if o.javaHome == "" { - javaHome, err := o.findJavaHome() - if err != nil { - o.ctx.Log.Warning("Failed to find JAVA_HOME: %s", err.Error()) - } else { - o.javaHome = javaHome - } - } - - // Set JAVA_HOME in environment for frameworks that need it during finalize phase - // (e.g., Luna Security Provider, Container Security Provider) - if o.javaHome != "" { - if err := os.Setenv("JAVA_HOME", o.javaHome); err != nil { - o.ctx.Log.Warning("Failed to set JAVA_HOME environment variable: %s", err.Error()) - } else { - o.ctx.Log.Debug("Set JAVA_HOME=%s", o.javaHome) - } - } - - // Determine Java major version for memory calculator - javaMajorVersion := 17 // default - if o.javaHome != "" { - if ver, err := common.DetermineJavaVersion(o.javaHome); err == nil { - javaMajorVersion = ver - } - } - - // Reconstruct JVMKill agent component if not already set - // This handles the case where finalize is called on a fresh instance - if o.jvmkill == nil { - o.jvmkill = NewJVMKillAgent(o.ctx, o.jreDir, o.version) - } - - // Finalize JVMKill agent - if err := o.jvmkill.Finalize(); err != nil { - o.ctx.Log.Warning("Failed to finalize JVMKill agent: %s", err.Error()) - // Non-fatal - } - - // Add base JAVA_OPTS for compatibility with Ruby buildpack - // These are standard JVM options that should be set for all OpenJDK-like JREs - baseOpts := []string{ - "-Djava.io.tmpdir=$TMPDIR", // Temp directory - "-XX:ActiveProcessorCount=$(nproc)", // CPU count - } - - // Note: We do NOT set -Djava.ext.dirs= here because frameworks like Container Security Provider - // need to set their own java.ext.dirs values. The Ruby buildpack delegates this to the - // JavaSecurity framework which collects all extension directories from contributing frameworks. - - if err := WriteJavaOpts(o.ctx, strings.Join(baseOpts, " ")); err != nil { - o.ctx.Log.Warning("Failed to write base JAVA_OPTS: %s", err.Error()) - } - - // Reconstruct Memory Calculator component if not already set - if o.memoryCalc == nil { - o.memoryCalc = NewMemoryCalculator(o.ctx, o.jreDir, o.version, javaMajorVersion, "openjdk") - } - - // Finalize Memory Calculator - if err := o.memoryCalc.Finalize(); err != nil { - o.ctx.Log.Warning("Failed to finalize Memory Calculator: %s", err.Error()) - // Non-fatal - } - - o.ctx.Log.Debug("OpenJDK JRE finalization complete") - return nil -} - -// JavaHome returns the path to JAVA_HOME -func (o *OpenJDKJRE) JavaHome() string { - return o.javaHome -} - -// Version returns the installed JRE version -func (o *OpenJDKJRE) Version() string { - return o.installedVersion -} - -// MemoryCalculatorCommand returns the shell command snippet to run memory calculator at runtime -// This is prepended to the container startup command to calculate optimal JVM memory settings -func (o *OpenJDKJRE) MemoryCalculatorCommand() string { - if o.memoryCalc == nil { - return "" - } - return o.memoryCalc.GetCalculatorCommand() -} - -// findJavaHome locates the actual JAVA_HOME directory after extraction -// OpenJDK tarballs usually extract to jdk-* or jre-* subdirectories -func (o *OpenJDKJRE) findJavaHome() (string, error) { - entries, err := os.ReadDir(o.jreDir) - if err != nil { - return "", fmt.Errorf("failed to read JRE directory: %w", err) - } - - // Look for jdk-* or jre-* subdirectory - for _, entry := range entries { - if entry.IsDir() { - name := entry.Name() - // Check for common OpenJDK directory patterns - if len(name) > 3 && (name[:3] == "jdk" || name[:3] == "jre") { - path := filepath.Join(o.jreDir, name) - // Verify it has a bin directory with java - if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil { - return path, nil - } - } - } - } - - // If no subdirectory found, check if jreDir itself is valid - if _, err := os.Stat(filepath.Join(o.jreDir, "bin", "java")); err == nil { - return o.jreDir, nil - } - - return "", fmt.Errorf("could not find valid JAVA_HOME in %s", o.jreDir) -} - -// writeProfileDScript creates a profile.d script that exports JAVA_HOME, JRE_HOME, and PATH at runtime -// Delegates to the shared helper function in jre.go -func (o *OpenJDKJRE) writeProfileDScript() error { - return WriteJavaHomeProfileD(o.ctx, o.jreDir, o.javaHome) + return &OpenJDKJRE{newBaseJRE(ctx, "OpenJDK", "openjdk", []string{"jdk", "jre"}, nil, "")} } diff --git a/src/java/jres/oracle.go b/src/java/jres/oracle.go index f578f3b1b..9cc8d5f0e 100644 --- a/src/java/jres/oracle.go +++ b/src/java/jres/oracle.go @@ -1,212 +1,11 @@ package jres -import ( - "fmt" - "github.com/cloudfoundry/java-buildpack/src/java/common" - "os" - "path/filepath" -) +import "github.com/cloudfoundry/java-buildpack/src/java/common" -// OracleJRE implements the JRE interface for Oracle JRE -// Oracle JRE requires a user-provided repository via JBP_CONFIG_ORACLE_JRE environment variable -type OracleJRE struct { - ctx *common.Context - jreDir string - version string - javaHome string - memoryCalc *MemoryCalculator - jvmkill *JVMKillAgent - installedVersion string -} +// OracleJRE implements the JRE interface for Oracle JRE. +type OracleJRE struct{ BaseJRE } -// NewOracleJRE creates a new Oracle JRE provider +// NewOracleJRE creates a new Oracle JRE provider. func NewOracleJRE(ctx *common.Context) *OracleJRE { - jreDir := filepath.Join(ctx.Stager.DepDir(), "jre") - - return &OracleJRE{ - ctx: ctx, - jreDir: jreDir, - } -} - -// Name returns the name of this JRE provider -func (o *OracleJRE) Name() string { - return "Oracle JRE" -} - -// Detect returns true if Oracle JRE should be used -// Oracle JRE requires explicit configuration via JBP_CONFIG_ORACLE_JRE environment variable -func (o *OracleJRE) Detect() (bool, error) { - return DetectJREByEnv("oracle"), nil -} - -// Supply installs the Oracle JRE and its components -func (o *OracleJRE) Supply() error { - o.ctx.Log.BeginStep("Installing Oracle JRE") - - // Determine version - dep, err := GetJREVersion(o.ctx, "oracle") - if err != nil { - return fmt.Errorf("failed to determine Oracle JRE version from manifest: %w", err) - } - - o.version = dep.Version - o.ctx.Log.Info("Installing Oracle JRE (%s)", o.version) - - // Install JRE - if err := o.ctx.Installer.InstallDependency(dep, o.jreDir); err != nil { - return fmt.Errorf("failed to install Oracle JRE: %w", err) - } - - // Find the actual JAVA_HOME (handle nested directories from tar extraction) - javaHome, err := o.findJavaHome() - if err != nil { - return fmt.Errorf("failed to find JAVA_HOME: %w", err) - } - o.javaHome = javaHome - o.installedVersion = o.version - - // Write profile.d script for runtime environment - if err := o.writeProfileDScript(); err != nil { - o.ctx.Log.Warning("Could not write java.sh profile.d script: %s", err.Error()) - } else { - o.ctx.Log.Debug("Created profile.d script: java.sh") - } - - // Determine Java major version - javaMajorVersion, err := common.DetermineJavaVersion(javaHome) - if err != nil { - o.ctx.Log.Warning("Could not determine Java version: %s", err.Error()) - javaMajorVersion = 17 // default - } - o.ctx.Log.Info("Detected Java major version: %d", javaMajorVersion) - - // Install JVMKill agent - o.jvmkill = NewJVMKillAgent(o.ctx, o.jreDir, o.version) - if err := o.jvmkill.Supply(); err != nil { - o.ctx.Log.Warning("Failed to install JVMKill agent: %s (continuing)", err.Error()) - // Non-fatal - continue without jvmkill - } - - // Install Memory Calculator - o.memoryCalc = NewMemoryCalculator(o.ctx, o.jreDir, o.version, javaMajorVersion, "oracle") - if err := o.memoryCalc.Supply(); err != nil { - o.ctx.Log.Warning("Failed to install Memory Calculator: %s (continuing)", err.Error()) - // Non-fatal - continue without memory calculator - } - - o.ctx.Log.Info("Oracle JRE installation complete") - return nil -} - -// Finalize performs final JRE configuration -func (o *OracleJRE) Finalize() error { - o.ctx.Log.BeginStep("Finalizing Oracle JRE configuration") - - // Find the actual JAVA_HOME (needed if finalize is called on a fresh instance) - if o.javaHome == "" { - javaHome, err := o.findJavaHome() - if err != nil { - o.ctx.Log.Warning("Failed to find JAVA_HOME: %s", err.Error()) - } else { - o.javaHome = javaHome - } - } - - // Set JAVA_HOME in environment for frameworks that need it during finalize phase - // (e.g., Luna Security Provider, Container Security Provider) - if o.javaHome != "" { - if err := os.Setenv("JAVA_HOME", o.javaHome); err != nil { - o.ctx.Log.Warning("Failed to set JAVA_HOME environment variable: %s", err.Error()) - } else { - o.ctx.Log.Debug("Set JAVA_HOME=%s", o.javaHome) - } - } - - // Determine Java major version for memory calculator - javaMajorVersion := 17 // default - if o.javaHome != "" { - if ver, err := common.DetermineJavaVersion(o.javaHome); err == nil { - javaMajorVersion = ver - } - } - - // Reconstruct JVMKill agent component if not already set - if o.jvmkill == nil { - o.jvmkill = NewJVMKillAgent(o.ctx, o.jreDir, o.version) - } - - // Finalize JVMKill agent - if err := o.jvmkill.Finalize(); err != nil { - o.ctx.Log.Warning("Failed to finalize JVMKill agent: %s", err.Error()) - // Non-fatal - } - - // Reconstruct Memory Calculator component if not already set - if o.memoryCalc == nil { - o.memoryCalc = NewMemoryCalculator(o.ctx, o.jreDir, o.version, javaMajorVersion, "oracle") - } - - // Finalize Memory Calculator - if err := o.memoryCalc.Finalize(); err != nil { - o.ctx.Log.Warning("Failed to finalize Memory Calculator: %s", err.Error()) - // Non-fatal - } - - o.ctx.Log.Info("Oracle JRE finalization complete") - return nil -} - -// JavaHome returns the path to JAVA_HOME -func (o *OracleJRE) JavaHome() string { - return o.javaHome -} - -// Version returns the installed JRE version -func (o *OracleJRE) Version() string { - return o.installedVersion -} - -// MemoryCalculatorCommand returns the shell command snippet to run memory calculator at runtime -func (o *OracleJRE) MemoryCalculatorCommand() string { - if o.memoryCalc == nil { - return "" - } - return o.memoryCalc.GetCalculatorCommand() -} - -// findJavaHome locates the actual JAVA_HOME directory after extraction -// Oracle JRE tarballs usually extract to jdk-* or jre-* subdirectories -func (o *OracleJRE) findJavaHome() (string, error) { - entries, err := os.ReadDir(o.jreDir) - if err != nil { - return "", fmt.Errorf("failed to read JRE directory: %w", err) - } - - // Look for jdk-* or jre-* subdirectory - for _, entry := range entries { - if entry.IsDir() { - name := entry.Name() - // Check for common Oracle JRE directory patterns - if len(name) > 3 && (name[:3] == "jdk" || name[:3] == "jre") { - path := filepath.Join(o.jreDir, name) - // Verify it has a bin directory with java - if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil { - return path, nil - } - } - } - } - - // If no subdirectory found, check if jreDir itself is valid - if _, err := os.Stat(filepath.Join(o.jreDir, "bin", "java")); err == nil { - return o.jreDir, nil - } - - return "", fmt.Errorf("could not find valid JAVA_HOME in %s", o.jreDir) -} - -// writeProfileDScript creates the profile.d script for setting JAVA_HOME, JRE_HOME, and PATH at runtime -func (o *OracleJRE) writeProfileDScript() error { - return WriteJavaHomeProfileD(o.ctx, o.jreDir, o.javaHome) + return &OracleJRE{newBaseJRE(ctx, "Oracle JRE", "oracle", []string{"jdk", "jre"}, nil, "")} } diff --git a/src/java/jres/sapmachine.go b/src/java/jres/sapmachine.go index 1391d150e..2f5a10df3 100644 --- a/src/java/jres/sapmachine.go +++ b/src/java/jres/sapmachine.go @@ -1,219 +1,11 @@ package jres -import ( - "fmt" - "github.com/cloudfoundry/java-buildpack/src/java/common" - "os" - "path/filepath" -) +import "github.com/cloudfoundry/java-buildpack/src/java/common" -// SapMachineJRE implements the JRE interface for SAP Machine OpenJDK -type SapMachineJRE struct { - ctx *common.Context - jreDir string - version string - javaHome string - memoryCalc *MemoryCalculator - jvmkill *JVMKillAgent - installedVersion string -} +// SapMachineJRE implements the JRE interface for SAP Machine OpenJDK. +type SapMachineJRE struct{ BaseJRE } -// NewSapMachineJRE creates a new SAP Machine JRE provider +// NewSapMachineJRE creates a new SAP Machine JRE provider. func NewSapMachineJRE(ctx *common.Context) *SapMachineJRE { - jreDir := filepath.Join(ctx.Stager.DepDir(), "jre") - - return &SapMachineJRE{ - ctx: ctx, - jreDir: jreDir, - } -} - -// Name returns the name of this JRE provider -func (s *SapMachineJRE) Name() string { - return "SapMachine" -} - -// Detect returns true if SAP Machine JRE should be used -// SAP Machine is selected via JBP_CONFIG_SAP_MACHINE_JRE environment variable -func (s *SapMachineJRE) Detect() (bool, error) { - return DetectJREByEnv("sapmachine"), nil -} - -// Supply installs the SAP Machine JRE and its components -func (s *SapMachineJRE) Supply() error { - s.ctx.Log.BeginStep("Installing SAP Machine JRE") - - // Determine version - dep, err := GetJREVersion(s.ctx, "sapmachine") - if err != nil { - return fmt.Errorf("failed to determine SAP Machine version from manifest: %w", err) - } - - s.version = dep.Version - s.ctx.Log.Info("Installing SAP Machine (%s)", s.version) - - // Install JRE - if err := s.ctx.Installer.InstallDependency(dep, s.jreDir); err != nil { - return fmt.Errorf("failed to install SAP Machine: %w", err) - } - - // Find the actual JAVA_HOME (handle nested directories from tar extraction) - javaHome, err := s.findJavaHome() - if err != nil { - return fmt.Errorf("failed to find JAVA_HOME: %w", err) - } - s.javaHome = javaHome - s.installedVersion = s.version - - // Write profile.d script for runtime environment - if err := s.writeProfileDScript(); err != nil { - s.ctx.Log.Warning("Could not write java.sh profile.d script: %s", err.Error()) - } else { - s.ctx.Log.Debug("Created profile.d script: java.sh") - } - - // Determine Java major version - javaMajorVersion, err := common.DetermineJavaVersion(javaHome) - if err != nil { - s.ctx.Log.Warning("Could not determine Java version: %s", err.Error()) - javaMajorVersion = 17 // default for SAP Machine - } - s.ctx.Log.Info("Detected Java major version: %d", javaMajorVersion) - - // Install JVMKill agent - s.jvmkill = NewJVMKillAgent(s.ctx, s.jreDir, s.version) - if err := s.jvmkill.Supply(); err != nil { - s.ctx.Log.Warning("Failed to install JVMKill agent: %s (continuing)", err.Error()) - // Non-fatal - continue without jvmkill - } - - // Install Memory Calculator - s.memoryCalc = NewMemoryCalculator(s.ctx, s.jreDir, s.version, javaMajorVersion, "sapmachine") - if err := s.memoryCalc.Supply(); err != nil { - s.ctx.Log.Warning("Failed to install Memory Calculator: %s (continuing)", err.Error()) - // Non-fatal - continue without memory calculator - } - - s.ctx.Log.Info("SAP Machine JRE installation complete") - return nil -} - -// Finalize performs final JRE configuration -func (s *SapMachineJRE) Finalize() error { - s.ctx.Log.BeginStep("Finalizing SAP Machine JRE configuration") - - // Find the actual JAVA_HOME (needed if finalize is called on a fresh instance) - if s.javaHome == "" { - javaHome, err := s.findJavaHome() - if err != nil { - s.ctx.Log.Warning("Failed to find JAVA_HOME: %s", err.Error()) - } else { - s.javaHome = javaHome - } - } - - // Set JAVA_HOME in environment for frameworks that need it during finalize phase - // (e.g., Luna Security Provider, Container Security Provider) - if s.javaHome != "" { - if err := os.Setenv("JAVA_HOME", s.javaHome); err != nil { - s.ctx.Log.Warning("Failed to set JAVA_HOME environment variable: %s", err.Error()) - } else { - s.ctx.Log.Debug("Set JAVA_HOME=%s", s.javaHome) - } - } - - // Determine Java major version for memory calculator - javaMajorVersion := 17 // default - if s.javaHome != "" { - if ver, err := common.DetermineJavaVersion(s.javaHome); err == nil { - javaMajorVersion = ver - } - } - - // Reconstruct JVMKill agent component if not already set - if s.jvmkill == nil { - s.jvmkill = NewJVMKillAgent(s.ctx, s.jreDir, s.version) - } - - // Finalize JVMKill agent - if err := s.jvmkill.Finalize(); err != nil { - s.ctx.Log.Warning("Failed to finalize JVMKill agent: %s", err.Error()) - // Non-fatal - } - - // Reconstruct Memory Calculator component if not already set - if s.memoryCalc == nil { - s.memoryCalc = NewMemoryCalculator(s.ctx, s.jreDir, s.version, javaMajorVersion, "sapmachine") - } - - // Finalize Memory Calculator - if err := s.memoryCalc.Finalize(); err != nil { - s.ctx.Log.Warning("Failed to finalize Memory Calculator: %s", err.Error()) - // Non-fatal - } - - s.ctx.Log.Info("SAP Machine JRE finalization complete") - return nil -} - -// JavaHome returns the path to JAVA_HOME -func (s *SapMachineJRE) JavaHome() string { - return s.javaHome -} - -// Version returns the installed JRE version -func (s *SapMachineJRE) Version() string { - return s.installedVersion -} - -// MemoryCalculatorCommand returns the shell command snippet to run memory calculator at runtime -func (s *SapMachineJRE) MemoryCalculatorCommand() string { - if s.memoryCalc == nil { - return "" - } - return s.memoryCalc.GetCalculatorCommand() -} - -// findJavaHome locates the actual JAVA_HOME directory after extraction -// SAP Machine tarballs usually extract to sapmachine-* or jdk-* subdirectories -func (s *SapMachineJRE) findJavaHome() (string, error) { - entries, err := os.ReadDir(s.jreDir) - if err != nil { - return "", fmt.Errorf("failed to read JRE directory: %w", err) - } - - // Look for sapmachine-*, jdk-*, or jre-* subdirectory - for _, entry := range entries { - if entry.IsDir() { - name := entry.Name() - // Check for SAP Machine directory patterns - if len(name) > 10 && name[:10] == "sapmachine" { - path := filepath.Join(s.jreDir, name) - // Verify it has a bin directory with java - if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil { - return path, nil - } - } - // Also check for standard jdk/jre patterns - if len(name) > 3 && (name[:3] == "jdk" || name[:3] == "jre") { - path := filepath.Join(s.jreDir, name) - if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil { - return path, nil - } - } - } - } - - // If no subdirectory found, check if jreDir itself is valid - if _, err := os.Stat(filepath.Join(s.jreDir, "bin", "java")); err == nil { - return s.jreDir, nil - } - - return "", fmt.Errorf("could not find valid JAVA_HOME in %s", s.jreDir) -} - -// writeProfileDScript creates a profile.d script that exports JAVA_HOME, JRE_HOME, and PATH at runtime -// Delegates to the shared helper function in jre.go -func (s *SapMachineJRE) writeProfileDScript() error { - return WriteJavaHomeProfileD(s.ctx, s.jreDir, s.javaHome) + return &SapMachineJRE{newBaseJRE(ctx, "SapMachine", "sapmachine", []string{"sapmachine"}, nil, "")} } diff --git a/src/java/jres/standard_jres_test.go b/src/java/jres/standard_jres_test.go new file mode 100644 index 000000000..fbd96da09 --- /dev/null +++ b/src/java/jres/standard_jres_test.go @@ -0,0 +1,248 @@ +package jres_test + +import ( + "os" + "path/filepath" + + "github.com/cloudfoundry/java-buildpack/src/java/common" + "github.com/cloudfoundry/java-buildpack/src/java/jres" + "github.com/cloudfoundry/libbuildpack" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Standard JREs", func() { + testCases := []struct { + description string + displayName string + envVar string + otherEnvVar string + subdir string + version string + newJRE func(*common.Context) jres.JRE + }{ + { + description: "Zulu", + displayName: "Zulu", + envVar: "JBP_CONFIG_ZULU_JRE", + otherEnvVar: "JBP_CONFIG_OPEN_JDK_JRE", + subdir: "zulu-17.0.13", + version: "17.0.13", + newJRE: func(ctx *common.Context) jres.JRE { + return jres.NewZuluJRE(ctx) + }, + }, + { + description: "SapMachine", + displayName: "SapMachine", + envVar: "JBP_CONFIG_SAP_MACHINE_JRE", + otherEnvVar: "JBP_CONFIG_ZULU_JRE", + subdir: "sapmachine-17.0.13", + version: "17.0.13", + newJRE: func(ctx *common.Context) jres.JRE { + return jres.NewSapMachineJRE(ctx) + }, + }, + { + description: "IBM", + displayName: "IBM JRE", + envVar: "JBP_CONFIG_IBM_JRE", + otherEnvVar: "JBP_CONFIG_ORACLE_JRE", + subdir: "ibm-java-8.0", + version: "1.8.0_422", + newJRE: func(ctx *common.Context) jres.JRE { + return jres.NewIBMJRE(ctx) + }, + }, + { + description: "Oracle", + displayName: "Oracle JRE", + envVar: "JBP_CONFIG_ORACLE_JRE", + otherEnvVar: "JBP_CONFIG_GRAAL_VM_JRE", + subdir: "jdk-17.0.13", + version: "17.0.13", + newJRE: func(ctx *common.Context) jres.JRE { + return jres.NewOracleJRE(ctx) + }, + }, + { + description: "GraalVM", + displayName: "GraalVM", + envVar: "JBP_CONFIG_GRAAL_VM_JRE", + otherEnvVar: "JBP_CONFIG_SAP_MACHINE_JRE", + subdir: "graalvm-21.0.1", + version: "21.0.1", + newJRE: func(ctx *common.Context) jres.JRE { + return jres.NewGraalVMJRE(ctx) + }, + }, + } + + for _, tc := range testCases { + tc := tc + + Describe(tc.description, func() { + var ( + ctx *common.Context + jre jres.JRE + cleanup func() + ) + + BeforeEach(func() { + ctx, cleanup = makeTestContext() + jre = tc.newJRE(ctx) + clearJREEnvVars() + DeferCleanup(cleanup) + DeferCleanup(clearJREEnvVars) + DeferCleanup(os.Unsetenv, "JAVA_HOME") + }) + + Describe("Name", func() { + It("returns the expected display name", func() { + Expect(jre.Name()).To(Equal(tc.displayName)) + }) + }) + + Describe("Detect", func() { + It("returns true when the correct environment variable is set", func() { + Expect(os.Setenv(tc.envVar, "{jre: {version: 17.+}}")).To(Succeed()) + + detected, err := jre.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(detected).To(BeTrue()) + }) + + It("returns false when no environment variable is set", func() { + detected, err := jre.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(detected).To(BeFalse()) + }) + + It("returns false when a different JRE environment variable is set", func() { + Expect(os.Setenv(tc.otherEnvVar, "{jre: {version: 17.+}}")).To(Succeed()) + + detected, err := jre.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(detected).To(BeFalse()) + }) + }) + + Describe("Version", func() { + It("returns empty before installation", func() { + Expect(jre.Version()).To(BeEmpty()) + }) + }) + + Describe("JavaHome", func() { + It("returns empty before installation", func() { + Expect(jre.JavaHome()).To(BeEmpty()) + }) + }) + + Describe("Finalize", func() { + It("sets JAVA_HOME using the JRE-specific directory prefix", func() { + expectedJavaHome := createFakeJRE(ctx, tc.subdir, tc.version) + + Expect(jre.Finalize()).To(Succeed()) + Expect(os.Getenv("JAVA_HOME")).To(Equal(expectedJavaHome)) + Expect(jre.JavaHome()).To(Equal(expectedJavaHome)) + }) + }) + }) + } + + Describe("IBM", func() { + var ( + ctx *common.Context + ibmJRE jres.JRE + cleanup func() + ) + + BeforeEach(func() { + ctx, cleanup = makeTestContext() + ibmJRE = jres.NewIBMJRE(ctx) + clearJREEnvVars() + DeferCleanup(cleanup) + DeferCleanup(clearJREEnvVars) + DeferCleanup(os.Unsetenv, "JAVA_HOME") + }) + + Describe("Finalize", func() { + It("sets JAVA_HOME when the extracted directory is exactly jre", func() { + expectedJavaHome := createFakeJRE(ctx, "jre", "1.8.0_422") + + Expect(ibmJRE.Finalize()).To(Succeed()) + Expect(os.Getenv("JAVA_HOME")).To(Equal(expectedJavaHome)) + Expect(ibmJRE.JavaHome()).To(Equal(expectedJavaHome)) + }) + }) + }) +}) + +func makeTestContext() (*common.Context, func()) { + buildDir, err := os.MkdirTemp("", "build") + Expect(err).NotTo(HaveOccurred()) + + depsDir, err := os.MkdirTemp("", "deps") + Expect(err).NotTo(HaveOccurred()) + + cacheDir, err := os.MkdirTemp("", "cache") + Expect(err).NotTo(HaveOccurred()) + + Expect(os.MkdirAll(filepath.Join(depsDir, "0"), 0755)).To(Succeed()) + + logger := libbuildpack.NewLogger(os.Stdout) + manifest := &libbuildpack.Manifest{} + installer := &libbuildpack.Installer{} + stager := libbuildpack.NewStager([]string{buildDir, cacheDir, depsDir, "0"}, logger, manifest) + command := &libbuildpack.Command{} + + ctx := &common.Context{ + Stager: stager, + Manifest: manifest, + Installer: installer, + Log: logger, + Command: command, + } + + cleanup := func() { + os.RemoveAll(buildDir) + os.RemoveAll(depsDir) + os.RemoveAll(cacheDir) + } + + return ctx, cleanup +} + +func createFakeJRE(ctx *common.Context, subdir, version string) string { + jreDir := filepath.Join(ctx.Stager.DepDir(), "jre") + javaHome := jreDir + if subdir != "" { + javaHome = filepath.Join(jreDir, subdir) + } + + Expect(os.MkdirAll(filepath.Join(javaHome, "bin"), 0755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(javaHome, "bin", "java"), []byte("#!/bin/sh\necho 'java version \""+version+"\"'\n"), 0755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(javaHome, "release"), []byte("JAVA_VERSION=\""+version+"\"\n"), 0644)).To(Succeed()) + + return javaHome +} + +func clearJREEnvVars() { + for _, envVar := range []string{ + "JBP_CONFIG_OPENJDK", + "JBP_CONFIG_OPEN_JDK_JRE", + "JBP_CONFIG_ZULU", + "JBP_CONFIG_ZULU_JRE", + "JBP_CONFIG_SAPMACHINE", + "JBP_CONFIG_SAP_MACHINE_JRE", + "JBP_CONFIG_IBM", + "JBP_CONFIG_IBM_JRE", + "JBP_CONFIG_ORACLE", + "JBP_CONFIG_ORACLE_JRE", + "JBP_CONFIG_GRAALVM", + "JBP_CONFIG_GRAAL_VM_JRE", + } { + os.Unsetenv(envVar) + } +} diff --git a/src/java/jres/zulu.go b/src/java/jres/zulu.go index 60ced44c6..144c9d1e1 100644 --- a/src/java/jres/zulu.go +++ b/src/java/jres/zulu.go @@ -1,219 +1,11 @@ package jres -import ( - "fmt" - "github.com/cloudfoundry/java-buildpack/src/java/common" - "os" - "path/filepath" -) +import "github.com/cloudfoundry/java-buildpack/src/java/common" -// ZuluJRE implements the JRE interface for Azul Zulu OpenJDK -type ZuluJRE struct { - ctx *common.Context - jreDir string - version string - javaHome string - memoryCalc *MemoryCalculator - jvmkill *JVMKillAgent - installedVersion string -} +// ZuluJRE implements the JRE interface for Azul Zulu OpenJDK. +type ZuluJRE struct{ BaseJRE } -// NewZuluJRE creates a new Zulu JRE provider +// NewZuluJRE creates a new Zulu JRE provider. func NewZuluJRE(ctx *common.Context) *ZuluJRE { - jreDir := filepath.Join(ctx.Stager.DepDir(), "jre") - - return &ZuluJRE{ - ctx: ctx, - jreDir: jreDir, - } -} - -// Name returns the name of this JRE provider -func (z *ZuluJRE) Name() string { - return "Zulu" -} - -// Detect returns true if Zulu JRE should be used -// Zulu is selected via JBP_CONFIG_ZULU_JRE environment variable -func (z *ZuluJRE) Detect() (bool, error) { - return DetectJREByEnv("zulu"), nil -} - -// Supply installs the Zulu JRE and its components -func (z *ZuluJRE) Supply() error { - z.ctx.Log.BeginStep("Installing Zulu JRE") - - // Determine version - dep, err := GetJREVersion(z.ctx, "zulu") - if err != nil { - return fmt.Errorf("failed to determine Zulu version from manifest: %w", err) - } - - z.version = dep.Version - z.ctx.Log.Info("Installing Zulu (%s)", z.version) - - // Install JRE - if err := z.ctx.Installer.InstallDependency(dep, z.jreDir); err != nil { - return fmt.Errorf("failed to install Zulu: %w", err) - } - - // Find the actual JAVA_HOME (handle nested directories from tar extraction) - javaHome, err := z.findJavaHome() - if err != nil { - return fmt.Errorf("failed to find JAVA_HOME: %w", err) - } - z.javaHome = javaHome - z.installedVersion = z.version - - // Set up JAVA_HOME environment - if err := z.writeProfileDScript(); err != nil { - z.ctx.Log.Warning("Could not write java.sh profile.d script: %s", err.Error()) - } else { - z.ctx.Log.Debug("Created profile.d script: java.sh") - } - - // Determine Java major version - javaMajorVersion, err := common.DetermineJavaVersion(javaHome) - if err != nil { - z.ctx.Log.Warning("Could not determine Java version: %s", err.Error()) - javaMajorVersion = 11 // default for Zulu - } - z.ctx.Log.Info("Detected Java major version: %d", javaMajorVersion) - - // Install JVMKill agent - z.jvmkill = NewJVMKillAgent(z.ctx, z.jreDir, z.version) - if err := z.jvmkill.Supply(); err != nil { - z.ctx.Log.Warning("Failed to install JVMKill agent: %s (continuing)", err.Error()) - // Non-fatal - continue without jvmkill - } - - // Install Memory Calculator - z.memoryCalc = NewMemoryCalculator(z.ctx, z.jreDir, z.version, javaMajorVersion, "zulu") - if err := z.memoryCalc.Supply(); err != nil { - z.ctx.Log.Warning("Failed to install Memory Calculator: %s (continuing)", err.Error()) - // Non-fatal - continue without memory calculator - } - - z.ctx.Log.Info("Zulu JRE installation complete") - return nil -} - -// Finalize performs final JRE configuration -func (z *ZuluJRE) Finalize() error { - z.ctx.Log.BeginStep("Finalizing Zulu JRE configuration") - - // Find the actual JAVA_HOME (needed if finalize is called on a fresh instance) - if z.javaHome == "" { - javaHome, err := z.findJavaHome() - if err != nil { - z.ctx.Log.Warning("Failed to find JAVA_HOME: %s", err.Error()) - } else { - z.javaHome = javaHome - } - } - - // Set JAVA_HOME in environment for frameworks that need it during finalize phase - // (e.g., Luna Security Provider, Container Security Provider) - if z.javaHome != "" { - if err := os.Setenv("JAVA_HOME", z.javaHome); err != nil { - z.ctx.Log.Warning("Failed to set JAVA_HOME environment variable: %s", err.Error()) - } else { - z.ctx.Log.Debug("Set JAVA_HOME=%s", z.javaHome) - } - } - - // Determine Java major version for memory calculator - javaMajorVersion := 11 // default - if z.javaHome != "" { - if ver, err := common.DetermineJavaVersion(z.javaHome); err == nil { - javaMajorVersion = ver - } - } - - // Reconstruct JVMKill agent component if not already set - if z.jvmkill == nil { - z.jvmkill = NewJVMKillAgent(z.ctx, z.jreDir, z.version) - } - - // Finalize JVMKill agent - if err := z.jvmkill.Finalize(); err != nil { - z.ctx.Log.Warning("Failed to finalize JVMKill agent: %s", err.Error()) - // Non-fatal - } - - // Reconstruct Memory Calculator component if not already set - if z.memoryCalc == nil { - z.memoryCalc = NewMemoryCalculator(z.ctx, z.jreDir, z.version, javaMajorVersion, "zulu") - } - - // Finalize Memory Calculator - if err := z.memoryCalc.Finalize(); err != nil { - z.ctx.Log.Warning("Failed to finalize Memory Calculator: %s", err.Error()) - // Non-fatal - } - - z.ctx.Log.Info("Zulu JRE finalization complete") - return nil -} - -// JavaHome returns the path to JAVA_HOME -func (z *ZuluJRE) JavaHome() string { - return z.javaHome -} - -// Version returns the installed JRE version -func (z *ZuluJRE) Version() string { - return z.installedVersion -} - -// MemoryCalculatorCommand returns the shell command snippet to run memory calculator at runtime -func (z *ZuluJRE) MemoryCalculatorCommand() string { - if z.memoryCalc == nil { - return "" - } - return z.memoryCalc.GetCalculatorCommand() -} - -// findJavaHome locates the actual JAVA_HOME directory after extraction -// Zulu tarballs usually extract to zulu-* subdirectories -func (z *ZuluJRE) findJavaHome() (string, error) { - entries, err := os.ReadDir(z.jreDir) - if err != nil { - return "", fmt.Errorf("failed to read JRE directory: %w", err) - } - - // Look for zulu-*, jdk-*, or jre-* subdirectory - for _, entry := range entries { - if entry.IsDir() { - name := entry.Name() - // Check for common Zulu directory patterns - if len(name) > 4 && name[:4] == "zulu" { - path := filepath.Join(z.jreDir, name) - // Verify it has a bin directory with java - if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil { - return path, nil - } - } - // Also check for standard jdk/jre patterns - if len(name) > 3 && (name[:3] == "jdk" || name[:3] == "jre") { - path := filepath.Join(z.jreDir, name) - if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil { - return path, nil - } - } - } - } - - // If no subdirectory found, check if jreDir itself is valid - if _, err := os.Stat(filepath.Join(z.jreDir, "bin", "java")); err == nil { - return z.jreDir, nil - } - - return "", fmt.Errorf("could not find valid JAVA_HOME in %s", z.jreDir) -} - -// writeProfileDScript creates a profile.d script that exports JAVA_HOME, JRE_HOME, and PATH at runtime -// Delegates to the shared helper function in jre.go -func (z *ZuluJRE) writeProfileDScript() error { - return WriteJavaHomeProfileD(z.ctx, z.jreDir, z.javaHome) + return &ZuluJRE{newBaseJRE(ctx, "Zulu", "zulu", []string{"zulu"}, nil, "")} } From 7e627cc11cc21f92e230d7abc9479795c72280cd Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Wed, 20 May 2026 12:41:17 +0000 Subject: [PATCH 2/2] fix: log warning when Java version detection falls back to default 17 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 --- src/java/common/context.go | 4 -- src/java/containers/tomcat.go | 69 +++++++++++++++-------------- src/java/jres/base_jre.go | 4 +- src/java/jres/jre_test.go | 8 ++-- src/java/jres/standard_jres_test.go | 28 +++++++++++- 5 files changed, 69 insertions(+), 44 deletions(-) diff --git a/src/java/common/context.go b/src/java/common/context.go index d9290d2e8..aa19d4ce2 100644 --- a/src/java/common/context.go +++ b/src/java/common/context.go @@ -66,10 +66,6 @@ func DetermineJavaVersion(javaHome string) (int, error) { releaseFile := filepath.Join(javaHome, "release") content, err := os.ReadFile(releaseFile) if err != nil { - // Default to Java 17 if release file is missing - if os.IsNotExist(err) { - return 17, nil - } return 0, fmt.Errorf("failed to read release file: %w", err) } diff --git a/src/java/containers/tomcat.go b/src/java/containers/tomcat.go index fab4713ee..c83213cfc 100644 --- a/src/java/containers/tomcat.go +++ b/src/java/containers/tomcat.go @@ -64,45 +64,46 @@ func (t *TomcatContainer) Supply() error { if javaHome != "" { javaMajorVersion, versionErr := common.DetermineJavaVersion(javaHome) - if versionErr == nil { - tomcatVersion := DetermineTomcatVersion(t.config.Tomcat.Version) - t.context.Log.Debug("Detected Java major version: %d", javaMajorVersion) - - // Select Tomcat version pattern based on Java version - var versionPattern string - if tomcatVersion == "" { - t.context.Log.Info("Tomcat version not specified") - if javaMajorVersion >= 11 { - // Java 11+: Use Tomcat 10.x (Jakarta EE 9+) - versionPattern = "10.x" - t.context.Log.Info("Using Tomcat 10.x for Java %d", javaMajorVersion) - } else { - // Java 8-10: Use Tomcat 9.x (Java EE 8) - versionPattern = "9.x" - t.context.Log.Info("Using Tomcat 9.x for Java %d", javaMajorVersion) - } - } else { - versionPattern = tomcatVersion - t.context.Log.Info("Using Tomcat %s for Java %d", versionPattern, javaMajorVersion) - } + if versionErr != nil { + t.context.Log.Warning("Unable to determine Java version: %s (defaulting to 17)", versionErr.Error()) + javaMajorVersion = 17 + } - if strings.HasPrefix(versionPattern, "10.") && javaMajorVersion < 11 { - return fmt.Errorf("Tomcat 10.x requires Java 11+, but Java %d detected", javaMajorVersion) + tomcatVersion := DetermineTomcatVersion(t.config.Tomcat.Version) + t.context.Log.Debug("Detected Java major version: %d", javaMajorVersion) + + // Select Tomcat version pattern based on Java version + var versionPattern string + if tomcatVersion == "" { + t.context.Log.Info("Tomcat version not specified") + if javaMajorVersion >= 11 { + // Java 11+: Use Tomcat 10.x (Jakarta EE 9+) + versionPattern = "10.x" + t.context.Log.Info("Using Tomcat 10.x for Java %d", javaMajorVersion) + } else { + // Java 8-10: Use Tomcat 9.x (Java EE 8) + versionPattern = "9.x" + t.context.Log.Info("Using Tomcat 9.x for Java %d", javaMajorVersion) } + } else { + versionPattern = tomcatVersion + t.context.Log.Info("Using Tomcat %s for Java %d", versionPattern, javaMajorVersion) + } - // Resolve the version pattern to actual version using libbuildpack - allVersions := t.context.Manifest.AllDependencyVersions("tomcat") - resolvedVersion, err := libbuildpack.FindMatchingVersion(versionPattern, allVersions) - if err != nil { - return fmt.Errorf("tomcat version resolution error for pattern %q: %w", versionPattern, err) - } + if strings.HasPrefix(versionPattern, "10.") && javaMajorVersion < 11 { + return fmt.Errorf("Tomcat 10.x requires Java 11+, but Java %d detected", javaMajorVersion) + } - dep.Name = "tomcat" - dep.Version = resolvedVersion - t.context.Log.Debug("Resolved Tomcat version pattern '%s' to %s", versionPattern, resolvedVersion) - } else { - t.context.Log.Warning("Unable to determine Java version: %s", versionErr.Error()) + // Resolve the version pattern to actual version using libbuildpack + allVersions := t.context.Manifest.AllDependencyVersions("tomcat") + resolvedVersion, err := libbuildpack.FindMatchingVersion(versionPattern, allVersions) + if err != nil { + return fmt.Errorf("tomcat version resolution error for pattern %q: %w", versionPattern, err) } + + dep.Name = "tomcat" + dep.Version = resolvedVersion + t.context.Log.Debug("Resolved Tomcat version pattern '%s' to %s", versionPattern, resolvedVersion) } // Fallback to default version if we couldn't determine Java version diff --git a/src/java/jres/base_jre.go b/src/java/jres/base_jre.go index efea0036d..f1749b136 100644 --- a/src/java/jres/base_jre.go +++ b/src/java/jres/base_jre.go @@ -140,7 +140,9 @@ func (b *BaseJRE) Finalize() error { javaMajorVersion := 17 if b.javaHome != "" { - if ver, err := common.DetermineJavaVersion(b.javaHome); err == nil { + if ver, err := common.DetermineJavaVersion(b.javaHome); err != nil { + b.ctx.Log.Warning("Could not determine Java version: %s (defaulting to %d)", err.Error(), javaMajorVersion) + } else { javaMajorVersion = ver } } diff --git a/src/java/jres/jre_test.go b/src/java/jres/jre_test.go index 784469bfe..01148477f 100644 --- a/src/java/jres/jre_test.go +++ b/src/java/jres/jre_test.go @@ -571,10 +571,10 @@ IMPLEMENTOR="Eclipse Adoptium"` Expect(version).To(Equal(21)) }) - It("defaults to 17 when release file is missing", func() { - version, err := common.DetermineJavaVersion(javaHome) - Expect(err).NotTo(HaveOccurred()) - Expect(version).To(Equal(17)) + It("returns an error when release file is missing", func() { + _, err := common.DetermineJavaVersion(javaHome) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("release file")) }) }) diff --git a/src/java/jres/standard_jres_test.go b/src/java/jres/standard_jres_test.go index fbd96da09..b5e8ddfb5 100644 --- a/src/java/jres/standard_jres_test.go +++ b/src/java/jres/standard_jres_test.go @@ -1,6 +1,7 @@ package jres_test import ( + "bytes" "os" "path/filepath" @@ -179,7 +180,32 @@ var _ = Describe("Standard JREs", func() { }) }) +var _ = Describe("BaseJRE Java version detection fallback", func() { + It("logs a warning when DetermineJavaVersion fails in Finalize and falls back to 17", func() { + var buf bytes.Buffer + ctx, cleanup := makeTestContextWithBuffer(&buf) + defer cleanup() + + // Create a jre dir with a valid java binary but no release file, + // so DetermineJavaVersion returns an error and Finalize falls back to Java 17. + jreDir := filepath.Join(ctx.Stager.DepDir(), "jre") + Expect(os.MkdirAll(filepath.Join(jreDir, "bin"), 0755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(jreDir, "bin", "java"), []byte("#!/bin/sh\n"), 0755)).To(Succeed()) + + jre := jres.NewOpenJDKJRE(ctx) + err := jre.Finalize() + Expect(err).NotTo(HaveOccurred()) + + Expect(buf.String()).To(ContainSubstring("WARNING")) + Expect(buf.String()).To(ContainSubstring("defaulting to 17")) + }) +}) + func makeTestContext() (*common.Context, func()) { + return makeTestContextWithBuffer(os.Stdout) +} + +func makeTestContextWithBuffer(w interface{ Write([]byte) (int, error) }) (*common.Context, func()) { buildDir, err := os.MkdirTemp("", "build") Expect(err).NotTo(HaveOccurred()) @@ -191,7 +217,7 @@ func makeTestContext() (*common.Context, func()) { Expect(os.MkdirAll(filepath.Join(depsDir, "0"), 0755)).To(Succeed()) - logger := libbuildpack.NewLogger(os.Stdout) + logger := libbuildpack.NewLogger(w) manifest := &libbuildpack.Manifest{} installer := &libbuildpack.Installer{} stager := libbuildpack.NewStager([]string{buildDir, cacheDir, depsDir, "0"}, logger, manifest)