This guide explains how to implement new JRE (Java Runtime Environment) providers for the Cloud Foundry Java Buildpack. JRE providers are responsible for detecting, installing, and configuring the Java runtime that will execute your application.
- Overview
- Available JRE Providers
- JRE Interface
- Implementation Steps
- Complete Examples
- Common Patterns
- Memory Calculator Integration
- JVMKill Agent
- Testing JREs
- Best Practices
- Troubleshooting
A JRE provider is a component that:
- Detects when it should be used (via environment variables or configuration)
- Supplies the Java runtime by downloading and extracting it
- Installs components like the memory calculator and JVMKill agent
- Finalizes configuration by setting up JAVA_HOME and JVM options
- Provides information about the installed Java version and location
The buildpack supports multiple JRE providers, allowing operators to choose between different Java distributions (OpenJDK, Zulu, GraalVM, IBM, etc.) based on their requirements.
The buildpack includes these JRE providers:
| Provider | Package Name | Default | Detection Method |
|---|---|---|---|
| OpenJDK | openjdk |
Yes | Always detected (fallback) |
| Zulu | zulu |
No | JBP_CONFIG_COMPONENTS or JBP_CONFIG_ZULU_JRE |
| GraalVM | graalvm |
No | JBP_CONFIG_COMPONENTS or JBP_CONFIG_GRAAL_VM_JRE |
| IBM JRE | ibm |
No | JBP_CONFIG_COMPONENTS or JBP_CONFIG_IBM_JRE |
| Oracle JRE | oracle |
No | JBP_CONFIG_COMPONENTS or JBP_CONFIG_ORACLE_JRE |
| SapMachine | sapmachine |
No | JBP_CONFIG_COMPONENTS or JBP_CONFIG_SAP_MACHINE_JRE |
| Azul Platform Prime | zing |
No | JBP_CONFIG_COMPONENTS or JBP_CONFIG_ZING_JRE |
All JRE providers must implement the jres.JRE interface defined in src/java/jres/jre.go:
type JRE interface {
// Name returns the name of this JRE provider (e.g., "OpenJDK", "Zulu")
Name() string
// Detect returns true if this JRE should be used
Detect() (bool, error)
// Supply installs the JRE and its components (memory calculator, jvmkill)
Supply() error
// Finalize performs any final JRE configuration
Finalize() error
// JavaHome returns the path to JAVA_HOME
JavaHome() string
// Version returns the installed JRE version
Version() string
}JRE providers receive a Context struct with shared dependencies:
type Context struct {
Stager *libbuildpack.Stager // Build/staging information
Manifest *libbuildpack.Manifest // Dependency versions
Installer *libbuildpack.Installer // Downloads dependencies
Log *libbuildpack.Logger // Logging
Command *libbuildpack.Command // Execute commands
}Follow these steps to implement a new JRE provider:
Create a new file src/java/jres/<jre_name>.go with a struct that will implement the JRE interface:
package jres
import (
"fmt"
"os"
"path/filepath"
"github.com/cloudfoundry/libbuildpack"
)
type MyJRE struct {
ctx *Context
jreDir string // Installation directory
version string // Requested version
javaHome string // Actual JAVA_HOME path
memoryCalc *MemoryCalculator
jvmkill *JVMKillAgent
installedVersion string
}Create a constructor function that initializes your JRE provider:
func NewMyJRE(ctx *Context) *MyJRE {
jreDir := filepath.Join(ctx.Stager.DepDir(), "jre")
return &MyJRE{
ctx: ctx,
jreDir: jreDir,
}
}Return a human-readable name for your JRE:
func (m *MyJRE) Name() string {
return "My JRE"
}Implement detection logic to determine if this JRE should be used:
func (m *MyJRE) Detect() (bool, error) {
// Check for explicit configuration
configuredJRE := os.Getenv("JBP_CONFIG_COMPONENTS")
if configuredJRE != "" && containsString(configuredJRE, "MyJRE") {
return true, nil
}
// Check legacy environment variable
if DetectJREByEnv("my_jre") {
return true, nil
}
return false, nil
}Install the JRE and its components:
func (m *MyJRE) Supply() error {
m.ctx.Log.BeginStep("Installing My JRE")
// 1. Determine version
dep, err := GetJREVersion(m.ctx, "my-jre")
if err != nil {
m.ctx.Log.Warning("Unable to determine My JRE version: %s", err.Error())
return err
}
m.version = dep.Version
m.ctx.Log.Info("Installing My JRE %s", m.version)
// 2. Install JRE
if err := m.ctx.Installer.InstallDependency(dep, m.jreDir); err != nil {
return fmt.Errorf("failed to install My JRE: %w", err)
}
// 3. Find JAVA_HOME
javaHome, err := m.findJavaHome()
if err != nil {
return fmt.Errorf("failed to find JAVA_HOME: %w", err)
}
m.javaHome = javaHome
m.installedVersion = m.version
// 4. Write profile.d script for runtime
if err := WriteJavaHomeProfileD(m.ctx, m.jreDir, m.javaHome); err != nil {
m.ctx.Log.Warning("Could not write profile.d script: %s", err.Error())
}
// 5. Determine Java major version
javaMajorVersion, err := DetermineJavaVersion(javaHome)
if err != nil {
m.ctx.Log.Warning("Could not determine Java version: %s", err.Error())
javaMajorVersion = 17 // default
}
m.ctx.Log.Info("Detected Java major version: %d", javaMajorVersion)
// 6. Install JVMKill agent
m.jvmkill = NewJVMKillAgent(m.ctx, m.jreDir, m.version)
if err := m.jvmkill.Supply(); err != nil {
m.ctx.Log.Warning("Failed to install JVMKill: %s", err.Error())
}
// 7. Install Memory Calculator
m.memoryCalc = NewMemoryCalculator(m.ctx, m.jreDir, m.version, javaMajorVersion)
if err := m.memoryCalc.Supply(); err != nil {
m.ctx.Log.Warning("Failed to install Memory Calculator: %s", err.Error())
}
m.ctx.Log.Info("My JRE installation complete")
return nil
}Perform final configuration (JVM options, environment setup):
func (m *MyJRE) Finalize() error {
m.ctx.Log.BeginStep("Finalizing My JRE configuration")
// Ensure JAVA_HOME is set
if m.javaHome == "" {
javaHome, err := m.findJavaHome()
if err != nil {
m.ctx.Log.Warning("Failed to find JAVA_HOME: %s", err.Error())
} else {
m.javaHome = javaHome
}
}
// Determine Java major version
javaMajorVersion := 17
if m.javaHome != "" {
if ver, err := DetermineJavaVersion(m.javaHome); err == nil {
javaMajorVersion = ver
}
}
// Finalize JVMKill agent
if m.jvmkill == nil {
m.jvmkill = NewJVMKillAgent(m.ctx, m.jreDir, m.version)
}
if err := m.jvmkill.Finalize(); err != nil {
m.ctx.Log.Warning("Failed to finalize JVMKill: %s", err.Error())
}
// Finalize Memory Calculator
if m.memoryCalc == nil {
m.memoryCalc = NewMemoryCalculator(m.ctx, m.jreDir, m.version, javaMajorVersion)
}
if err := m.memoryCalc.Finalize(); err != nil {
m.ctx.Log.Warning("Failed to finalize Memory Calculator: %s", err.Error())
}
// Add any JRE-specific JVM options
// Example: opts := "-XX:+UseG1GC"
// WriteJavaOpts(m.ctx, opts)
m.ctx.Log.Info("My JRE finalization complete")
return nil
}Implement remaining interface methods and helper functions:
// JavaHome returns the path to JAVA_HOME
func (m *MyJRE) JavaHome() string {
return m.javaHome
}
// Version returns the installed JRE version
func (m *MyJRE) Version() string {
return m.installedVersion
}
// findJavaHome locates JAVA_HOME after extraction
func (m *MyJRE) findJavaHome() (string, error) {
entries, err := os.ReadDir(m.jreDir)
if err != nil {
return "", fmt.Errorf("failed to read JRE directory: %w", err)
}
// Look for jdk-* or jre-* subdirectories
for _, entry := range entries {
if entry.IsDir() {
name := entry.Name()
if len(name) > 3 && (name[:3] == "jdk" || name[:3] == "jre") {
path := filepath.Join(m.jreDir, name)
// Verify it has bin/java
if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil {
return path, nil
}
}
}
}
// Check if jreDir itself is valid
if _, err := os.Stat(filepath.Join(m.jreDir, "bin", "java")); err == nil {
return m.jreDir, nil
}
return "", fmt.Errorf("could not find valid JAVA_HOME in %s", m.jreDir)
}Register your JRE provider in src/java/supply/supply.go:
// In the Supply function, register your JRE
jreRegistry := jres.NewRegistry(jreCtx)
jreRegistry.Register(jres.NewOpenJDKJRE(jreCtx))
jreRegistry.Register(jres.NewZuluJRE(jreCtx))
jreRegistry.Register(jres.NewMyJRE(jreCtx)) // Add your JREOpenJDK is the default JRE provider. It always detects successfully and serves as the fallback.
File: src/java/jres/openjdk.go
package jres
import (
"fmt"
"os"
"path/filepath"
"github.com/cloudfoundry/libbuildpack"
)
type OpenJDKJRE struct {
ctx *Context
jreDir string
version string
javaHome string
memoryCalc *MemoryCalculator
jvmkill *JVMKillAgent
installedVersion string
}
func NewOpenJDKJRE(ctx *Context) *OpenJDKJRE {
jreDir := filepath.Join(ctx.Stager.DepDir(), "jre")
return &OpenJDKJRE{
ctx: ctx,
jreDir: jreDir,
}
}
func (o *OpenJDKJRE) Name() string {
return "OpenJDK"
}
// Detect always returns true (default JRE)
func (o *OpenJDKJRE) Detect() (bool, error) {
return true, nil
}
func (o *OpenJDKJRE) Supply() error {
o.ctx.Log.BeginStep("Installing OpenJDK JRE")
// Determine version from manifest
dep, err := GetJREVersion(o.ctx, "openjdk")
if err != nil {
o.ctx.Log.Warning("Unable to determine OpenJDK version from manifest, using default")
dep = libbuildpack.Dependency{
Name: "openjdk",
Version: "17.0.13",
}
}
o.version = dep.Version
o.ctx.Log.Info("Installing OpenJDK %s", o.version)
// Install JRE tarball
if err := o.ctx.Installer.InstallDependency(dep, o.jreDir); err != nil {
return fmt.Errorf("failed to install OpenJDK: %w", err)
}
// Find JAVA_HOME (OpenJDK extracts to jdk-* subdirectory)
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
if err := WriteJavaHomeProfileD(o.ctx, o.jreDir, o.javaHome); err != nil {
o.ctx.Log.Warning("Could not write profile.d script: %s", err.Error())
}
// Determine Java major version
javaMajorVersion, err := DetermineJavaVersion(javaHome)
if err != nil {
o.ctx.Log.Warning("Could not determine Java version: %s", err.Error())
javaMajorVersion = 17
}
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())
}
// Install Memory Calculator
o.memoryCalc = NewMemoryCalculator(o.ctx, o.jreDir, o.version, javaMajorVersion)
if err := o.memoryCalc.Supply(); err != nil {
o.ctx.Log.Warning("Failed to install Memory Calculator: %s (continuing)", err.Error())
}
o.ctx.Log.Info("OpenJDK JRE installation complete")
return nil
}
func (o *OpenJDKJRE) Finalize() error {
o.ctx.Log.BeginStep("Finalizing OpenJDK JRE configuration")
// Find JAVA_HOME if not set
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 for frameworks during finalize
if o.javaHome != "" {
if err := os.Setenv("JAVA_HOME", o.javaHome); err != nil {
o.ctx.Log.Warning("Failed to set JAVA_HOME: %s", err.Error())
}
}
// Determine Java version
javaMajorVersion := 17
if o.javaHome != "" {
if ver, err := DetermineJavaVersion(o.javaHome); err == nil {
javaMajorVersion = ver
}
}
// Finalize JVMKill agent
if o.jvmkill == nil {
o.jvmkill = NewJVMKillAgent(o.ctx, o.jreDir, o.version)
}
if err := o.jvmkill.Finalize(); err != nil {
o.ctx.Log.Warning("Failed to finalize JVMKill agent: %s", err.Error())
}
// Finalize Memory Calculator
if o.memoryCalc == nil {
o.memoryCalc = NewMemoryCalculator(o.ctx, o.jreDir, o.version, javaMajorVersion)
}
if err := o.memoryCalc.Finalize(); err != nil {
o.ctx.Log.Warning("Failed to finalize Memory Calculator: %s", err.Error())
}
o.ctx.Log.Info("OpenJDK JRE finalization complete")
return nil
}
func (o *OpenJDKJRE) JavaHome() string {
return o.javaHome
}
func (o *OpenJDKJRE) Version() string {
return o.installedVersion
}
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()
if len(name) > 3 && (name[:3] == "jdk" || name[:3] == "jre") {
path := filepath.Join(o.jreDir, name)
if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil {
return path, nil
}
}
}
}
// 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)
}Key Points:
- Always detects: OpenJDK is the default, so
Detect()always returnstrue - Standard installation: Downloads tarball, extracts to
deps/0/jre - Nested directory handling: OpenJDK tarballs extract to
jdk-17.0.13/subdirectory - Component installation: Installs JVMKill and Memory Calculator
- Profile.d script: Exports JAVA_HOME at runtime for containers
Configuration:
Users can specify Java version via BP_JAVA_VERSION:
cf set-env myapp BP_JAVA_VERSION 21Zulu is an alternative OpenJDK distribution from Azul Systems. It requires explicit configuration.
File: src/java/jres/zulu.go
package jres
import (
"fmt"
"os"
"path/filepath"
"github.com/cloudfoundry/libbuildpack"
)
type ZuluJRE struct {
ctx *Context
jreDir string
version string
javaHome string
memoryCalc *MemoryCalculator
jvmkill *JVMKillAgent
installedVersion string
}
func NewZuluJRE(ctx *Context) *ZuluJRE {
jreDir := filepath.Join(ctx.Stager.DepDir(), "jre")
return &ZuluJRE{
ctx: ctx,
jreDir: jreDir,
}
}
func (z *ZuluJRE) Name() string {
return "Zulu"
}
// Detect checks for explicit Zulu configuration
func (z *ZuluJRE) Detect() (bool, error) {
// Check JBP_CONFIG_COMPONENTS for Zulu
configuredJRE := os.Getenv("JBP_CONFIG_COMPONENTS")
if configuredJRE != "" && (containsString(configuredJRE, "ZuluJRE") || containsString(configuredJRE, "Zulu")) {
return true, nil
}
// Check legacy environment variable
if DetectJREByEnv("zulu_jre") {
return true, nil
}
return false, nil
}
func (z *ZuluJRE) Supply() error {
z.ctx.Log.BeginStep("Installing Zulu JRE")
// Determine version
dep, err := GetJREVersion(z.ctx, "zulu")
if err != nil {
z.ctx.Log.Warning("Unable to determine Zulu version from manifest, using default")
dep = libbuildpack.Dependency{
Name: "zulu",
Version: "11.0.25",
}
}
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 JAVA_HOME (Zulu extracts to zulu-* subdirectory)
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 := WriteJavaHomeProfileD(z.ctx, z.jreDir, z.javaHome); err != nil {
z.ctx.Log.Warning("Could not write profile.d script: %s", err.Error())
}
// Determine Java major version
javaMajorVersion, err := 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())
}
// Install Memory Calculator
z.memoryCalc = NewMemoryCalculator(z.ctx, z.jreDir, z.version, javaMajorVersion)
if err := z.memoryCalc.Supply(); err != nil {
z.ctx.Log.Warning("Failed to install Memory Calculator: %s (continuing)", err.Error())
}
z.ctx.Log.Info("Zulu JRE installation complete")
return nil
}
func (z *ZuluJRE) Finalize() error {
z.ctx.Log.BeginStep("Finalizing Zulu JRE configuration")
// Find JAVA_HOME if not set
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
}
}
// Determine Java major version
javaMajorVersion := 11
if z.javaHome != "" {
if ver, err := DetermineJavaVersion(z.javaHome); err == nil {
javaMajorVersion = ver
}
}
// Finalize JVMKill agent
if z.jvmkill == nil {
z.jvmkill = NewJVMKillAgent(z.ctx, z.jreDir, z.version)
}
if err := z.jvmkill.Finalize(); err != nil {
z.ctx.Log.Warning("Failed to finalize JVMKill agent: %s", err.Error())
}
// Finalize Memory Calculator
if z.memoryCalc == nil {
z.memoryCalc = NewMemoryCalculator(z.ctx, z.jreDir, z.version, javaMajorVersion)
}
if err := z.memoryCalc.Finalize(); err != nil {
z.ctx.Log.Warning("Failed to finalize Memory Calculator: %s", err.Error())
}
z.ctx.Log.Info("Zulu JRE finalization complete")
return nil
}
func (z *ZuluJRE) JavaHome() string {
return z.javaHome
}
func (z *ZuluJRE) Version() string {
return z.installedVersion
}
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 Zulu-specific patterns first
if len(name) > 4 && name[:4] == "zulu" {
path := filepath.Join(z.jreDir, name)
if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil {
return path, nil
}
}
// Also check standard 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
}
}
}
}
// 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)
}Key Points:
- Explicit detection: Only detects when configured via
JBP_CONFIG_COMPONENTS - Alternative naming: Looks for
zulu-*directory patterns in addition tojdk-* - Same components: Uses standard JVMKill and Memory Calculator
Configuration:
Users enable Zulu via environment variable:
cf set-env myapp JBP_CONFIG_COMPONENTS '{jres: ["JavaBuildpack::Jre::ZuluJRE"]}'
cf set-env myapp BP_JAVA_VERSION 11IBM JRE requires custom repository configuration and adds vendor-specific JVM options.
File: src/java/jres/ibm.go
package jres
import (
"fmt"
"os"
"path/filepath"
"github.com/cloudfoundry/libbuildpack"
)
type IBMJRE struct {
ctx *Context
jreDir string
version string
javaHome string
memoryCalc *MemoryCalculator
jvmkill *JVMKillAgent
installedVersion string
}
func NewIBMJRE(ctx *Context) *IBMJRE {
jreDir := filepath.Join(ctx.Stager.DepDir(), "jre")
return &IBMJRE{
ctx: ctx,
jreDir: jreDir,
}
}
func (i *IBMJRE) Name() string {
return "IBM JRE"
}
func (i *IBMJRE) Detect() (bool, error) {
// Check for explicit configuration
configuredJRE := os.Getenv("JBP_CONFIG_COMPONENTS")
if configuredJRE != "" && (containsString(configuredJRE, "IbmJRE") || containsString(configuredJRE, "IBM")) {
return true, nil
}
// Check legacy config
if DetectJREByEnv("ibm_jre") {
return true, nil
}
return false, nil
}
func (i *IBMJRE) Supply() error {
i.ctx.Log.BeginStep("Installing IBM JRE")
// IBM JRE requires repository_root configuration
dep, err := GetJREVersion(i.ctx, "ibm")
if err != nil {
i.ctx.Log.Warning("Unable to determine IBM JRE version from manifest, using default")
dep = libbuildpack.Dependency{
Name: "ibm",
Version: "8.0.8.26",
}
}
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 JAVA_HOME (IBM extracts to ibm-java-* subdirectory)
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
if err := WriteJavaHomeProfileD(i.ctx, i.jreDir, i.javaHome); err != nil {
i.ctx.Log.Warning("Could not write profile.d script: %s", err.Error())
}
// Determine Java major version
javaMajorVersion, err := 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
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())
}
// Install Memory Calculator
i.memoryCalc = NewMemoryCalculator(i.ctx, i.jreDir, i.version, javaMajorVersion)
if err := i.memoryCalc.Supply(); err != nil {
i.ctx.Log.Warning("Failed to install Memory Calculator: %s (continuing)", err.Error())
}
i.ctx.Log.Info("IBM JRE installation complete")
return nil
}
// Finalize adds IBM-specific JVM options
func (i *IBMJRE) Finalize() error {
i.ctx.Log.BeginStep("Finalizing IBM JRE configuration")
// Find JAVA_HOME if not set
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
}
}
// Determine Java major version
javaMajorVersion := 8
if i.javaHome != "" {
if ver, err := DetermineJavaVersion(i.javaHome); err == nil {
javaMajorVersion = ver
}
}
// Finalize JVMKill agent
if i.jvmkill == nil {
i.jvmkill = NewJVMKillAgent(i.ctx, i.jreDir, i.version)
}
if err := i.jvmkill.Finalize(); err != nil {
i.ctx.Log.Warning("Failed to finalize JVMKill agent: %s", err.Error())
}
// Finalize Memory Calculator
if i.memoryCalc == nil {
i.memoryCalc = NewMemoryCalculator(i.ctx, i.jreDir, i.version, javaMajorVersion)
}
if err := i.memoryCalc.Finalize(); err != nil {
i.ctx.Log.Warning("Failed to finalize Memory Calculator: %s", err.Error())
}
// 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())
} else {
i.ctx.Log.Info("Added IBM-specific JVM options: %s", ibmOpts)
}
i.ctx.Log.Info("IBM JRE finalization complete")
return nil
}
func (i *IBMJRE) JavaHome() string {
return i.javaHome
}
func (i *IBMJRE) Version() string {
return i.installedVersion
}
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()
// IBM JRE specific patterns
if (len(name) > 8 && name[:8] == "ibm-java") || name == "jre" {
path := filepath.Join(i.jreDir, name)
if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil {
return path, nil
}
}
}
}
// 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)
}Key Points:
- Custom JVM options: Adds
-Xtune:virtualizedand-Xshareclasses:noneinFinalize() - Vendor-specific naming: Looks for
ibm-java-*directory patterns - Repository configuration: Requires users to configure repository via
JBP_CONFIG_IBM_JRE
Configuration:
IBM JRE requires custom repository configuration in config/ibm_jre.yml:
---
repository_root: "https://public.dhe.ibm.com/ibmdl/export/pub/systems/cloud/runtimes/java/"
version: 8.0.+Or via environment variable:
cf set-env myapp JBP_CONFIG_IBM_JRE '{version: 8.0.8.26, repository_root: "https://..."}'Use the GetJREVersion() helper to resolve versions:
// GetJREVersion checks environment variables and manifest
dep, err := GetJREVersion(ctx, "openjdk")Version sources (in priority order):
BP_JAVA_VERSIONenvironment variable (e.g.,BP_JAVA_VERSION=17)JBP_CONFIG_<JRE_NAME>environment variable- Manifest default version
Examples:
# Simple version
cf set-env myapp BP_JAVA_VERSION 21
# Version pattern (wildcard)
cf set-env myapp BP_JAVA_VERSION "17.*"
# Legacy config
cf set-env myapp JBP_CONFIG_OPEN_JDK_JRE '{jre: {version: 11.+}}'JRE tarballs often extract to subdirectories. Use this pattern:
func (j *MyJRE) findJavaHome() (string, error) {
entries, err := os.ReadDir(j.jreDir)
if err != nil {
return "", fmt.Errorf("failed to read JRE directory: %w", err)
}
// Look for vendor-specific patterns first
for _, entry := range entries {
if entry.IsDir() {
name := entry.Name()
// Example: "myjre-21.0.1" or "jdk-21.0.1"
if strings.HasPrefix(name, "myjre-") || strings.HasPrefix(name, "jdk-") {
path := filepath.Join(j.jreDir, name)
// Verify it's a valid JRE
if _, err := os.Stat(filepath.Join(path, "bin", "java")); err == nil {
return path, nil
}
}
}
}
// Fallback: check if jreDir itself is valid
if _, err := os.Stat(filepath.Join(j.jreDir, "bin", "java")); err == nil {
return j.jreDir, nil
}
return "", fmt.Errorf("could not find valid JAVA_HOME in %s", j.jreDir)
}Always create a profile.d script to export JAVA_HOME at runtime:
// Use the helper function
if err := WriteJavaHomeProfileD(ctx, jreDir, javaHome); err != nil {
ctx.Log.Warning("Could not write profile.d script: %s", err.Error())
}This creates .profile.d/java.sh:
export JAVA_HOME=$DEPS_DIR/<idx>/jre/jdk-17.0.13
export JRE_HOME=$DEPS_DIR/<idx>/jre/jdk-17.0.13
export PATH=$JAVA_HOME/bin:$PATHWhere <idx> is the buildpack index (0 for standalone usage, or the position in multi-buildpack chain).
Use WriteJavaOpts() to add JVM options:
// Add custom JVM options
opts := "-XX:+UseG1GC -XX:MaxGCPauseMillis=200"
if err := WriteJavaOpts(ctx, opts); err != nil {
ctx.Log.Warning("Failed to write JVM options: %s", err.Error())
}This appends to .profile.d/java_opts.sh:
export JAVA_OPTS="${JAVA_OPTS:--XX:+UseG1GC -XX:MaxGCPauseMillis=200}"Determine the major Java version for memory calculator:
javaMajorVersion, err := DetermineJavaVersion(javaHome)
if err != nil {
ctx.Log.Warning("Could not determine Java version: %s", err.Error())
javaMajorVersion = 17 // default
}This reads the release file in JAVA_HOME:
JAVA_VERSION="17.0.13"
The Memory Calculator computes optimal JVM memory settings based on container memory limits.
Install during Supply():
// Create memory calculator component
memoryCalc := NewMemoryCalculator(ctx, jreDir, jreVersion, javaMajorVersion)
// Install the calculator binary
if err := memoryCalc.Supply(); err != nil {
ctx.Log.Warning("Failed to install Memory Calculator: %s", err.Error())
// Non-fatal - continue without memory calculator
}Configure during Finalize():
// Finalize memory calculator
if err := memoryCalc.Finalize(); err != nil {
ctx.Log.Warning("Failed to finalize Memory Calculator: %s", err.Error())
}This creates a script that containers can invoke at runtime:
CALCULATED_MEMORY=$(java-buildpack-memory-calculator-3.13.0 \
-totMemory=$MEMORY_LIMIT \
-loadedClasses=12345 \
-poolType=metaspace \
-stackThreads=250)
export JAVA_OPTS="$JAVA_OPTS $CALCULATED_MEMORY"At runtime, the calculator generates JVM options:
-Xmx512M -Xms512M -XX:MaxMetaspaceSize=128M -Xss1M -XX:ReservedCodeCacheSize=32M
Users can customize via environment variables:
cf set-env myapp MEMORY_CALCULATOR_STACK_THREADS 300
cf set-env myapp MEMORY_CALCULATOR_HEADROOM 10JVMKill is an agent that forcibly terminates the JVM when it cannot allocate memory or throws OutOfMemoryError.
Install during Supply():
// Create JVMKill agent component
jvmkill := NewJVMKillAgent(ctx, jreDir, jreVersion)
// Install the agent .so file
if err := jvmkill.Supply(); err != nil {
ctx.Log.Warning("Failed to install JVMKill agent: %s", err.Error())
// Non-fatal - continue without jvmkill
}Add to JAVA_OPTS during Finalize():
// Finalize JVMKill agent (adds -agentpath to JAVA_OPTS)
if err := jvmkill.Finalize(); err != nil {
ctx.Log.Warning("Failed to finalize JVMKill agent: %s", err.Error())
}This adds to JAVA_OPTS:
-agentpath:/home/vcap/deps/0/jre/bin/jvmkill-1.16.0.so=printHeapHistogram=1
If a volume service with heap-dump tag is bound, JVMKill writes heap dumps:
-agentpath:/home/vcap/deps/0/jre/bin/jvmkill-1.16.0.so=printHeapHistogram=1,heapDumpPath=/volumes/heap-dumps/app.hprof
Bind volume service:
cf bind-service myapp my-volume-service -c '{"mount":"/volumes/heap-dumps","tags":["heap-dump"]}'Test your JRE implementation using Ginkgo and Gomega:
File: src/java/jres/myjre_test.go
package jres_test
import (
"os"
"path/filepath"
"github.com/cloudfoundry/java-buildpack/src/java/jres"
"github.com/cloudfoundry/libbuildpack"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("MyJRE", func() {
var (
ctx *jres.Context
myJRE jres.JRE
buildDir string
depsDir string
cacheDir string
)
BeforeEach(func() {
var err error
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())
// Create deps directory structure
err = os.MkdirAll(filepath.Join(depsDir, "0"), 0755)
Expect(err).NotTo(HaveOccurred())
// Set up context
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 = &jres.Context{
Stager: stager,
Manifest: manifest,
Installer: installer,
Log: logger,
Command: command,
}
myJRE = jres.NewMyJRE(ctx)
})
AfterEach(func() {
os.RemoveAll(buildDir)
os.RemoveAll(depsDir)
os.RemoveAll(cacheDir)
})
Describe("Name", func() {
It("returns the JRE name", func() {
Expect(myJRE.Name()).To(Equal("My JRE"))
})
})
Describe("Detect", func() {
Context("when JBP_CONFIG_COMPONENTS specifies MyJRE", func() {
BeforeEach(func() {
os.Setenv("JBP_CONFIG_COMPONENTS", "{jres: ['MyJRE']}")
})
AfterEach(func() {
os.Unsetenv("JBP_CONFIG_COMPONENTS")
})
It("detects successfully", func() {
detected, err := myJRE.Detect()
Expect(err).NotTo(HaveOccurred())
Expect(detected).To(BeTrue())
})
})
Context("when not configured", func() {
It("does not detect", func() {
detected, err := myJRE.Detect()
Expect(err).NotTo(HaveOccurred())
Expect(detected).To(BeFalse())
})
})
})
Describe("JavaHome", func() {
Context("before installation", func() {
It("returns empty string", func() {
Expect(myJRE.JavaHome()).To(BeEmpty())
})
})
Context("after simulated installation", func() {
BeforeEach(func() {
// Simulate JRE installation
jreDir := filepath.Join(depsDir, "0", "jre", "myjre-17.0.1")
err := os.MkdirAll(filepath.Join(jreDir, "bin"), 0755)
Expect(err).NotTo(HaveOccurred())
// Create fake java executable
javaPath := filepath.Join(jreDir, "bin", "java")
err = os.WriteFile(javaPath, []byte("#!/bin/sh\necho 'java version \"17.0.1\"'\n"), 0755)
Expect(err).NotTo(HaveOccurred())
})
It("finds JAVA_HOME after finalize", func() {
err := myJRE.Finalize()
// May return error if components missing, but should not panic
_ = err
// JavaHome should be set if findJavaHome succeeded
javaHome := myJRE.JavaHome()
if javaHome != "" {
Expect(javaHome).To(ContainSubstring("myjre-17.0.1"))
}
})
})
})
Describe("Version", func() {
Context("before installation", func() {
It("returns empty string", func() {
Expect(myJRE.Version()).To(BeEmpty())
})
})
})
})Run JRE tests:
# Run all JRE tests
./scripts/unit.sh
# Run specific JRE test
go test -v ./src/java/jres -run TestMyJRE
# Run with Ginkgo
ginkgo -v ./src/java/jresCreate integration tests to verify JRE installation:
File: src/integration/myjre_test.go
package integration_test
import (
"path/filepath"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/cloudfoundry/switchblade"
)
var _ = Describe("MyJRE Integration", func() {
var (
fixture string
)
BeforeEach(func() {
fixture = "simple_java_app"
})
Context("when MyJRE is configured", func() {
It("successfully builds and runs", func() {
deployment, _, err := switchblade.Deploy(
switchblade.Buildpack(bpDir),
switchblade.FixturePath(filepath.Join(fixturesDir, fixture)),
switchblade.Env(map[string]string{
"JBP_CONFIG_COMPONENTS": "{jres: ['MyJRE']}",
"BP_JAVA_VERSION": "17",
}),
)
Expect(err).NotTo(HaveOccurred())
defer deployment.Delete()
// Verify app is running
Expect(deployment.Status()).To(Equal(switchblade.StatusRunning))
// Verify logs contain MyJRE
logs, err := deployment.Logs()
Expect(err).NotTo(HaveOccurred())
Expect(logs).To(ContainSubstring("Installing My JRE"))
})
})
})Leverage existing helper functions in jre.go:
GetJREVersion()- Version resolutionDetermineJavaVersion()- Parse Java versionWriteJavaHomeProfileD()- Create profile.d scriptWriteJavaOpts()- Add JVM optionsDetectJREByEnv()- Check environment variables
Component installation failures should be non-fatal:
// Install JVMKill (non-fatal if it fails)
if err := jvmkill.Supply(); err != nil {
ctx.Log.Warning("Failed to install JVMKill: %s (continuing)", err.Error())
// Continue without JVMKill
}Accept version patterns:
BP_JAVA_VERSION=17 # Exact major version
BP_JAVA_VERSION=17.* # Any 17.x version
BP_JAVA_VERSION=17.0.+ # Any 17.0.x patchUse structured logging:
ctx.Log.BeginStep("Installing My JRE") // Major phase
ctx.Log.Info("Installing My JRE %s", version) // User-visible info
ctx.Log.Debug("Extracted to: %s", javaHome) // Debug details
ctx.Log.Warning("Could not verify: %s", err.Error()) // Non-fatal warningsAlways verify JAVA_HOME after extraction:
javaExecutable := filepath.Join(javaHome, "bin", "java")
if _, err := os.Stat(javaExecutable); err != nil {
return fmt.Errorf("invalid JAVA_HOME: bin/java not found at %s", javaHome)
}Add vendor-specific JVM options in Finalize():
// GraalVM: Enable native image agent
opts := "-agentlib:native-image-agent=config-output-dir=/tmp/config"
// IBM JRE: Optimize for virtualization
opts := "-Xtune:virtualized -Xshareclasses:none"
// Zulu: Enable Flight Recorder
opts := "-XX:StartFlightRecording=duration=60s,filename=/tmp/recording.jfr"
WriteJavaOpts(ctx, opts)Add configuration documentation for your JRE in docs/jre-<name>.md:
- Environment variable options
- Repository configuration
- Version availability
- Vendor-specific features
Test with multiple Java versions:
DescribeTable("supports multiple versions",
func(version string) {
os.Setenv("BP_JAVA_VERSION", version)
defer os.Unsetenv("BP_JAVA_VERSION")
detected, err := jre.Detect()
Expect(err).NotTo(HaveOccurred())
Expect(detected).To(BeTrue())
},
Entry("Java 8", "8"),
Entry("Java 11", "11"),
Entry("Java 17", "17"),
Entry("Java 21", "21"),
)Problem: JRE not being selected during staging
Solution:
-
Check detection logic:
# Enable debug logging cf set-env myapp BP_LOG_LEVEL DEBUG cf restage myapp -
Verify environment variables:
cf env myapp | grep JBP_CONFIG_COMPONENTS -
Check detection order in registry (first match wins)
Problem: findJavaHome() fails after extraction
Solution:
-
Check tarball structure:
tar -tzf openjdk-17.0.13.tar.gz | head -
Update directory pattern matching:
// Add more patterns if strings.HasPrefix(name, "custom-prefix-") { // ... }
-
Log extracted directory structure:
ctx.Log.Debug("JRE directory contents: %v", entries)
Problem: Memory calculator not generating options
Solution:
-
Verify calculator installed:
ls $DEPS_DIR/<idx>/jre/bin/java-buildpack-memory-calculator-*
(where is the buildpack index)
-
Check class counting:
ctx.Log.Debug("Counted %d classes", classCount)
-
Test calculator manually:
java-buildpack-memory-calculator -totMemory=1G -loadedClasses=10000 -poolType=metaspace -stackThreads=250
Problem: JVMKill agent not being loaded
Solution:
-
Verify .so file exists:
ls -la /home/vcap/deps/0/jre/bin/jvmkill-*.so -
Check JAVA_OPTS at runtime:
cf ssh myapp echo $JAVA_OPTS
-
Verify agentpath:
# Should see: -agentpath:/home/vcap/deps/0/jre/bin/jvmkill-1.16.0.so=...
Problem: JAVA_HOME not set at runtime
Solution:
-
Verify profile.d script exists:
cf ssh myapp cat /home/vcap/app/.profile.d/java.sh
-
Check script permissions:
ls -la /home/vcap/app/.profile.d/
-
Test script manually:
source /home/vcap/app/.profile.d/java.sh echo $JAVA_HOME
Problem: Wrong Java version being installed
Solution:
-
Check manifest versions:
grep -A 10 '"openjdk"' manifest.yml -
Test version resolution:
dep, err := GetJREVersion(ctx, "openjdk") ctx.Log.Info("Resolved version: %s", dep.Version)
-
Override explicitly:
cf set-env myapp BP_JAVA_VERSION 17.0.13
Implementing a JRE provider involves:
- Create struct implementing
jres.JREinterface - Implement detection logic (environment variables)
- Download and extract JRE tarball in
Supply() - Find JAVA_HOME handling nested directories
- Install components (Memory Calculator, JVMKill)
- Configure runtime with profile.d scripts
- Add JVM options vendor-specific or optimizations
- Test thoroughly with unit and integration tests
The buildpack provides extensive utilities to simplify JRE implementation. Follow the patterns from existing JRE providers (OpenJDK, Zulu, IBM) and leverage shared components (Memory Calculator, JVMKill) for consistent functionality across all JREs.
For more information:
- Architecture Guide - Overall buildpack design
- Development Guide - Building and testing
- Testing Guide - Test framework details
- Implementing Frameworks - Framework integration
- Implementing Containers - Container types