From f42c75ec94586fe5851ab7404ca6b2be46738361 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Fri, 26 Jun 2026 14:59:41 +0000 Subject: [PATCH 1/4] docs: rewrite IMPLEMENTING_JRES.md for BaseJRE pattern PR #1288 refactored all standard JREs to embed BaseJRE (14 lines each), but the guide still showed the old 100+ line manual implementation with explicit Supply()/Finalize() boilerplate. Rewrite to document the actual pattern: newBaseJRE() + optional extraFinalizeOpts. Removes sections on manual memory calculator and JVMKill integration (now internal to BaseJRE). Adds variation-point table and examples for all three patterns (standard, exact-dir, error-hint). --- docs/IMPLEMENTING_JRES.md | 1649 +++---------------------------------- 1 file changed, 132 insertions(+), 1517 deletions(-) diff --git a/docs/IMPLEMENTING_JRES.md b/docs/IMPLEMENTING_JRES.md index ba035868d..2aa9c60e5 100644 --- a/docs/IMPLEMENTING_JRES.md +++ b/docs/IMPLEMENTING_JRES.md @@ -1,1635 +1,250 @@ # Implementing JREs -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. +This guide explains how to implement new JRE providers for the Cloud Foundry Java Buildpack. ## Table of Contents - [Overview](#overview) - [Available JRE Providers](#available-jre-providers) - [JRE Interface](#jre-interface) +- [BaseJRE — Shared Implementation](#basejre--shared-implementation) - [Implementation Steps](#implementation-steps) -- [Complete Examples](#complete-examples) - - [Example 1: OpenJDK (Standard JRE)](#example-1-openjdk-standard-jre) - - [Example 2: Zulu (Alternative Distribution)](#example-2-zulu-alternative-distribution) - - [Example 3: IBM JRE (Custom Configuration)](#example-3-ibm-jre-custom-configuration) -- [Common Patterns](#common-patterns) -- [Memory Calculator Integration](#memory-calculator-integration) -- [JVMKill Agent](#jvmkill-agent) +- [Examples](#examples) - [Testing JREs](#testing-jres) -- [Best Practices](#best-practices) - [Troubleshooting](#troubleshooting) ## Overview -A JRE provider is a component that: +A JRE provider: -1. **Detects** when it should be used (via environment variables or configuration) -2. **Supplies** the Java runtime by downloading and extracting it -3. **Installs components** like the memory calculator and JVMKill agent -4. **Finalizes** configuration by setting up JAVA_HOME and JVM options -5. **Provides information** about the installed Java version and location +1. **Detects** when it should be used (env vars / config) +2. **Supplies** the Java runtime (download + extract) +3. **Finalizes** configuration (JAVA_HOME, JVM options) -The buildpack supports multiple JRE providers, allowing operators to choose between different Java distributions (OpenJDK, Zulu, GraalVM, IBM, etc.) based on their requirements. +`Supply()`, `Finalize()`, memory calculator, and JVMKill agent are all handled by `BaseJRE`. Implementing a new JRE is typically **14 lines**. ## Available JRE Providers -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` | +| Provider | Key | Default | `dirPrefixes` / `dirExacts` | +|----------|-----|---------|------------------------------| +| **OpenJDK** | `openjdk` | Yes (fallback) | `["jdk", "jre"]` | +| **Zulu** | `zulu` | No | `["zulu"]` | +| **GraalVM** | `graalvm` | No | `["graalvm"]` | +| **IBM JRE** | `ibm` | No | prefixes: `["ibm-java"]`, exacts: `["jre"]` | +| **Oracle JRE** | `oracle` | No | `["jdk", "jre"]` | +| **SapMachine** | `sapmachine` | No | `["sapmachine"]` | +| **Azul Platform Prime** | `zing` | No | (custom — does not use `BaseJRE`) | ## JRE Interface -All JRE providers must implement the `jres.JRE` interface defined in `src/java/jres/jre.go`: +All providers implement `jres.JRE` (`src/java/jres/jre.go`): ```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 + Name() string + Detect() (bool, error) + Supply() error Finalize() error - - // JavaHome returns the path to JAVA_HOME JavaHome() string - - // Version returns the installed JRE version - Version() string -} -``` - -### JRE Context - -JRE providers receive a `Context` struct with shared dependencies: - -```go -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 -} -``` - -## Implementation Steps - -Follow these steps to implement a new JRE provider: - -### Step 1: Create the JRE Struct - -Create a new file `src/java/jres/.go` with a struct that will implement the `JRE` interface: - -```go -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 -} -``` - -### Step 2: Implement the Constructor - -Create a constructor function that initializes your JRE provider: - -```go -func NewMyJRE(ctx *Context) *MyJRE { - jreDir := filepath.Join(ctx.Stager.DepDir(), "jre") - - return &MyJRE{ - ctx: ctx, - jreDir: jreDir, - } -} -``` - -### Step 3: Implement Name() - -Return a human-readable name for your JRE: - -```go -func (m *MyJRE) Name() string { - return "My JRE" -} -``` - -### Step 4: Implement Detect() - -Implement detection logic to determine if this JRE should be used: - -```go -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 -} -``` - -### Step 5: Implement Supply() - -Install the JRE and its components: - -```go -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 -} -``` - -### Step 6: Implement Finalize() - -Perform final configuration (JVM options, environment setup): - -```go -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 + Version() string } ``` -### Step 7: Implement Helper Methods +Providers receive a `*common.Context` with shared dependencies (stager, manifest, installer, logger). -Implement remaining interface methods and helper functions: +## BaseJRE — Shared Implementation -```go -// JavaHome returns the path to JAVA_HOME -func (m *MyJRE) JavaHome() string { - return m.javaHome -} +`BaseJRE` (`src/java/jres/base_jre.go`) provides the full `Supply()`/`Finalize()` implementation. Concrete JREs embed it and inject variation via constructor fields — **never override `Supply` or `Finalize`**. -// Version returns the installed JRE version -func (m *MyJRE) Version() string { - return m.installedVersion -} +### Variation points -// 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) -} -``` +| Field | Type | Purpose | +|-------|------|---------| +| `jreName` | `string` | Display name in logs (e.g. `"OpenJDK"`) | +| `jreKey` | `string` | Manifest key and env detection (e.g. `"openjdk"`) | +| `dirPrefixes` | `[]string` | Directory name prefixes for `findJavaHome()` (e.g. `["jdk", "jre"]`) | +| `dirExacts` | `[]string` | Exact directory names for `findJavaHome()` (e.g. `["jre"]` for IBM) | +| `installErrNote` | `string` | Extra context appended to install error (e.g. GraalVM repo hint) | +| `extraFinalizeOpts` | `func() string` | JRE-specific JVM opts written during `Finalize()` (e.g. `-XX:ActiveProcessorCount=$(nproc)`) | -### Step 8: Register the JRE +### What BaseJRE handles automatically -Register your JRE provider in `src/java/supply/supply.go`: - -```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 JRE -``` - -## Complete Examples - -### Example 1: OpenJDK (Standard JRE) - -OpenJDK is the default JRE provider. It always detects successfully and serves as the fallback. - -**File:** `src/java/jres/openjdk.go` - -```go -package jres +- Download and extraction via `ctx.Installer.InstallDependency` +- `findJavaHome()` — scans `dirPrefixes` / `dirExacts` then falls back to `jreDir` itself +- `profile.d/java.sh` — exports `JAVA_HOME` at runtime +- `JAVA_HOME` env file and `bin/java` dependency link +- JVMKill agent (`Supply` + `Finalize`) +- Memory calculator (`Supply` + `Finalize`) +- Base JVM opts (`-Djava.io.tmpdir=$TMPDIR`) +- `extraFinalizeOpts` (if set) -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 returns `true` -- **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`: -```bash -cf set-env myapp BP_JAVA_VERSION 21 -``` - -### Example 2: Zulu (Alternative Distribution) - -Zulu is an alternative OpenJDK distribution from Azul Systems. It requires explicit configuration. - -**File:** `src/java/jres/zulu.go` - -```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 to `jdk-*` -- **Same components:** Uses standard JVMKill and Memory Calculator - -**Configuration:** - -Users enable Zulu via environment variable: -```bash -cf set-env myapp JBP_CONFIG_COMPONENTS '{jres: ["JavaBuildpack::Jre::ZuluJRE"]}' -cf set-env myapp BP_JAVA_VERSION 11 -``` - -### Example 3: IBM JRE (Custom Configuration) - -IBM JRE requires custom repository configuration and adds vendor-specific JVM options. +## Implementation Steps -**File:** `src/java/jres/ibm.go` +### Step 1: Create `src/java/jres/.go` ```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 -} +import "github.com/cloudfoundry/java-buildpack/src/java/common" -func (i *IBMJRE) Version() string { - return i.installedVersion -} +// MyJRE implements the JRE interface for My JRE. +type MyJRE struct{ BaseJRE } -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) +// NewMyJRE creates a new My JRE provider. +func NewMyJRE(ctx *common.Context) *MyJRE { + b := newBaseJRE(ctx, "My JRE", "my-jre", []string{"myjre"}, nil, "") + b.extraFinalizeOpts = func() string { return "-XX:ActiveProcessorCount=$(nproc)" } + return &MyJRE{b} } ``` -**Key Points:** -- **Custom JVM options:** Adds `-Xtune:virtualized` and `-Xshareclasses:none` in `Finalize()` -- **Vendor-specific naming:** Looks for `ibm-java-*` directory patterns -- **Repository configuration:** Requires users to configure repository via `JBP_CONFIG_IBM_JRE` +Set `extraFinalizeOpts` only if you need JRE-specific JVM flags. Omit it (or set to `nil`) if none are needed. -**Configuration:** +### Step 2: Add dependency to `manifest.yml` -IBM JRE requires custom repository configuration in `config/ibm_jre.yml`: ```yaml ---- -repository_root: "https://public.dhe.ibm.com/ibmdl/export/pub/systems/cloud/runtimes/java/" -version: 8.0.+ -``` - -Or via environment variable: -```bash -cf set-env myapp JBP_CONFIG_IBM_JRE '{version: 8.0.8.26, repository_root: "https://..."}' -``` - -## Common Patterns - -### Version Selection - -Use the `GetJREVersion()` helper to resolve versions: - -```go -// GetJREVersion checks environment variables and manifest -dep, err := GetJREVersion(ctx, "openjdk") -``` - -Version sources (in priority order): -1. `BP_JAVA_VERSION` environment variable (e.g., `BP_JAVA_VERSION=17`) -2. `JBP_CONFIG_` environment variable -3. Manifest default version - -**Examples:** -```bash -# 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.+}}' +- name: my-jre + version: 1.2.3 + uri: https://example.com/my-jre-1.2.3.tar.gz + sha256: + cf_stacks: + - cflinuxfs4 + - cflinuxfs5 ``` -### Finding JAVA_HOME - -JRE tarballs often extract to subdirectories. Use this pattern: +Also add to `default_versions` if it should be the default for a stack: -```go -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) -} +```yaml +- name: my-jre + version: 1.x ``` -### Profile.d Script - -Always create a profile.d script to export JAVA_HOME at runtime: +### Step 3: Register in `src/java/supply/supply.go` ```go -// Use the helper function -if err := WriteJavaHomeProfileD(ctx, jreDir, javaHome); err != nil { - ctx.Log.Warning("Could not write profile.d script: %s", err.Error()) -} +jreRegistry.Register(jres.NewMyJRE(ctx)) ``` -This creates `.profile.d/java.sh`: -```bash -export JAVA_HOME=$DEPS_DIR//jre/jdk-17.0.13 -export JRE_HOME=$DEPS_DIR//jre/jdk-17.0.13 -export PATH=$JAVA_HOME/bin:$PATH -``` +That is all that is required. -Where `` is the buildpack index (0 for standalone usage, or the position in multi-buildpack chain). +## Examples -### Adding JVM Options +### Standard JRE (OpenJDK) -Use `WriteJavaOpts()` to add JVM options: +Tarball extracts to a `jdk-*` or `jre-*` subdirectory. `dirPrefixes` covers both. ```go -// 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`: -```bash -export JAVA_OPTS="${JAVA_OPTS:--XX:+UseG1GC -XX:MaxGCPauseMillis=200}" -``` - -### Determining Java Version +// src/java/jres/openjdk.go +type OpenJDKJRE struct{ BaseJRE } -Determine the major Java version for memory calculator: - -```go -javaMajorVersion, err := DetermineJavaVersion(javaHome) -if err != nil { - ctx.Log.Warning("Could not determine Java version: %s", err.Error()) - javaMajorVersion = 17 // default +func NewOpenJDKJRE(ctx *common.Context) *OpenJDKJRE { + b := newBaseJRE(ctx, "OpenJDK", "openjdk", []string{"jdk", "jre"}, nil, "") + b.extraFinalizeOpts = func() string { return "-XX:ActiveProcessorCount=$(nproc)" } + return &OpenJDKJRE{b} } ``` -This reads the `release` file in JAVA_HOME: -``` -JAVA_VERSION="17.0.13" -``` - -## Memory Calculator Integration - -The Memory Calculator computes optimal JVM memory settings based on container memory limits. - -### Installing Memory Calculator +### JRE with exact directory name (IBM JRE) -Install during `Supply()`: +IBM JRE tarball may extract to a directory named `jre` exactly, or to `ibm-java-*`. Both patterns covered: ```go -// Create memory calculator component -memoryCalc := NewMemoryCalculator(ctx, jreDir, jreVersion, javaMajorVersion) +// src/java/jres/ibm.go +type IBMJRE struct{ BaseJRE } -// 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 +func NewIBMJRE(ctx *common.Context) *IBMJRE { + b := newBaseJRE(ctx, "IBM JRE", "ibm", []string{"ibm-java"}, []string{"jre"}, "") + b.extraFinalizeOpts = func() string { return "-Xtune:virtualized -Xshareclasses:none" } + return &IBMJRE{b} } ``` -### Finalizing Memory Calculator - -Configure during `Finalize()`: - -```go -// 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: -```bash -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" -``` - -### Memory Calculator Output - -At runtime, the calculator generates JVM options: -``` --Xmx512M -Xms512M -XX:MaxMetaspaceSize=128M -Xss1M -XX:ReservedCodeCacheSize=32M -``` - -### Customizing Memory Calculator - -Users can customize via environment variables: -```bash -cf set-env myapp MEMORY_CALCULATOR_STACK_THREADS 300 -cf set-env myapp MEMORY_CALCULATOR_HEADROOM 10 -``` - -## JVMKill Agent +### JRE with install error hint (GraalVM) -JVMKill is an agent that forcibly terminates the JVM when it cannot allocate memory or throws OutOfMemoryError. - -### Installing JVMKill - -Install during `Supply()`: +GraalVM is not in the default manifest and requires `repository_root` config. The hint appears in the error if installation fails: ```go -// Create JVMKill agent component -jvmkill := NewJVMKillAgent(ctx, jreDir, jreVersion) +// src/java/jres/graalvm.go +type GraalVMJRE struct{ BaseJRE } -// 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 +func NewGraalVMJRE(ctx *common.Context) *GraalVMJRE { + b := newBaseJRE(ctx, "GraalVM", "graalvm", []string{"graalvm"}, nil, + "(ensure repository_root is configured)") + b.extraFinalizeOpts = func() string { return "-XX:ActiveProcessorCount=$(nproc)" } + return &GraalVMJRE{b} } ``` -### Finalizing JVMKill +### JRE without extra JVM opts -Add to JAVA_OPTS during `Finalize()`: +If no JRE-specific opts are needed, omit `extraFinalizeOpts`: ```go -// 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()) +func NewMinimalJRE(ctx *common.Context) *MinimalJRE { + b := newBaseJRE(ctx, "Minimal JRE", "minimal", []string{"jre"}, nil, "") + // extraFinalizeOpts left nil — BaseJRE writes only -Djava.io.tmpdir=$TMPDIR + return &MinimalJRE{b} } ``` -This adds to JAVA_OPTS: -``` --agentpath:/home/vcap/deps/0/jre/bin/jvmkill-1.16.0.so=printHeapHistogram=1 -``` - -### Heap Dump Support - -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: -```bash -cf bind-service myapp my-volume-service -c '{"mount":"/volumes/heap-dumps","tags":["heap-dump"]}' -``` - ## Testing JREs -### Unit Testing with Ginkgo +Tests live in `src/java/jres/`. Use `standard_jres_test.go` as a reference — it tests all `BaseJRE`-based providers with shared helpers. -Test your JRE implementation using Ginkgo and Gomega: - -**File:** `src/java/jres/myjre_test.go` +### Unit test pattern ```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 + ctx *common.Context + myJRE *jres.MyJRE + // ... temp dirs ) - + 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, - } - + // set up ctx with temp dirs and mock manifest 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()) - }) - }) - }) -}) -``` - -### Running Tests - -Run JRE tests: -```bash -# 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/jres -``` - -### Integration Testing - -Create integration tests to verify JRE installation: - -**File:** `src/integration/myjre_test.go` - -```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")) - }) + It("detects when JBP_CONFIG_MY_JRE is set", func() { + os.Setenv("JBP_CONFIG_MY_JRE", "{}") + defer os.Unsetenv("JBP_CONFIG_MY_JRE") + detected, err := myJRE.Detect() + Expect(err).NotTo(HaveOccurred()) + Expect(detected).To(BeTrue()) }) }) ``` -## Best Practices +### Detection -### 1. Use Shared Utility Functions +`BaseJRE.Detect()` calls `DetectJREByEnv(jreKey)` which returns `true` when: +- `JBP_CONFIG__JRE` is set, or +- `JBP_CONFIG_COMPONENTS` names this JRE, or +- `BP_JAVA_` is set -Leverage existing helper functions in `jre.go`: -- `GetJREVersion()` - Version resolution -- `DetermineJavaVersion()` - Parse Java version -- `WriteJavaHomeProfileD()` - Create profile.d script -- `WriteJavaOpts()` - Add JVM options -- `DetectJREByEnv()` - Check environment variables +Replace `` with the uppercased `jreKey` value. -### 2. Handle Errors Gracefully +### Running tests -Component installation failures should be non-fatal: -```go -// 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 -} -``` - -### 3. Support Version Flexibility - -Accept version patterns: ```bash -BP_JAVA_VERSION=17 # Exact major version -BP_JAVA_VERSION=17.* # Any 17.x version -BP_JAVA_VERSION=17.0.+ # Any 17.0.x patch -``` +# JRE unit tests only +.bin/ginkgo -r -mod vendor src/java/jres/ -### 4. Log Comprehensively - -Use structured logging: -```go -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 warnings -``` - -### 5. Verify Installation - -Always verify JAVA_HOME after extraction: -```go -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) -} -``` - -### 6. Support Vendor-Specific Features - -Add vendor-specific JVM options in `Finalize()`: -```go -// 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) -``` - -### 7. Document Configuration - -Add configuration documentation for your JRE in `docs/jre-.md`: -- Environment variable options -- Repository configuration -- Version availability -- Vendor-specific features - -### 8. Test Multiple Versions - -Test with multiple Java versions: -```go -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"), -) +# Full unit suite +bash scripts/unit.sh ``` ## Troubleshooting -### JRE Not Detected - -**Problem:** JRE not being selected during staging - -**Solution:** -1. Check detection logic: - ```bash - # Enable debug logging - cf set-env myapp BP_LOG_LEVEL DEBUG - cf restage myapp - ``` - -2. Verify environment variables: - ```bash - cf env myapp | grep JBP_CONFIG_COMPONENTS - ``` +**`findJavaHome` fails** — the tarball extracted to a directory that matches neither `dirPrefixes` nor `dirExacts`. Check the extraction layout: -3. Check detection order in registry (first match wins) - -### JAVA_HOME Not Found - -**Problem:** `findJavaHome()` fails after extraction - -**Solution:** -1. Check tarball structure: - ```bash - tar -tzf openjdk-17.0.13.tar.gz | head - ``` - -2. Update directory pattern matching: - ```go - // Add more patterns - if strings.HasPrefix(name, "custom-prefix-") { - // ... - } - ``` - -3. Log extracted directory structure: - ```go - ctx.Log.Debug("JRE directory contents: %v", entries) - ``` +```bash +tar tf my-jre.tar.gz | head -5 +``` -### Memory Calculator Fails +Add the top-level directory prefix/name to `dirPrefixes`/`dirExacts` accordingly. -**Problem:** Memory calculator not generating options +**Install fails with "No matching dependency"** — check the `jreKey` in `newBaseJRE` matches the `name:` field in `manifest.yml` exactly. -**Solution:** -1. Verify calculator installed: - ```bash - ls $DEPS_DIR//jre/bin/java-buildpack-memory-calculator-* - ``` - (where is the buildpack index) - -2. Check class counting: - ```go - ctx.Log.Debug("Counted %d classes", classCount) - ``` - -3. Test calculator manually: - ```bash - java-buildpack-memory-calculator -totMemory=1G -loadedClasses=10000 -poolType=metaspace -stackThreads=250 - ``` - -### JVMKill Not Loading - -**Problem:** JVMKill agent not being loaded - -**Solution:** -1. Verify .so file exists: - ```bash - ls -la /home/vcap/deps/0/jre/bin/jvmkill-*.so - ``` - -2. Check JAVA_OPTS at runtime: - ```bash - cf ssh myapp - echo $JAVA_OPTS - ``` - -3. Verify agentpath: - ```bash - # Should see: -agentpath:/home/vcap/deps/0/jre/bin/jvmkill-1.16.0.so=... - ``` - -### Profile.d Script Not Executing - -**Problem:** JAVA_HOME not set at runtime - -**Solution:** -1. Verify profile.d script exists: - ```bash - cf ssh myapp - cat /home/vcap/app/.profile.d/java.sh - ``` - -2. Check script permissions: - ```bash - ls -la /home/vcap/app/.profile.d/ - ``` - -3. Test script manually: - ```bash - source /home/vcap/app/.profile.d/java.sh - echo $JAVA_HOME - ``` - -### Version Resolution Issues - -**Problem:** Wrong Java version being installed - -**Solution:** -1. Check manifest versions: - ```bash - grep -A 10 '"openjdk"' manifest.yml - ``` - -2. Test version resolution: - ```go - dep, err := GetJREVersion(ctx, "openjdk") - ctx.Log.Info("Resolved version: %s", dep.Version) - ``` - -3. Override explicitly: - ```bash - cf set-env myapp BP_JAVA_VERSION 17.0.13 - ``` - ---- - -## Summary - -Implementing a JRE provider involves: - -1. **Create struct** implementing `jres.JRE` interface -2. **Implement detection** logic (environment variables) -3. **Download and extract** JRE tarball in `Supply()` -4. **Find JAVA_HOME** handling nested directories -5. **Install components** (Memory Calculator, JVMKill) -6. **Configure runtime** with profile.d scripts -7. **Add JVM options** vendor-specific or optimizations -8. **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](../ARCHITECTURE.md) - Overall buildpack design -- [Development Guide](DEVELOPING.md) - Building and testing -- [Testing Guide](TESTING.md) - Test framework details -- [Implementing Frameworks](IMPLEMENTING_FRAMEWORKS.md) - Framework integration -- [Implementing Containers](IMPLEMENTING_CONTAINERS.md) - Container types +**`extraFinalizeOpts` not applied** — ensure the function is set on `b` (the `BaseJRE` value) before wrapping it: `b.extraFinalizeOpts = func() string { ... }`. Setting it on the returned struct after `return &MyJRE{b}` has no effect. From 0fddbeaea426fcb84e84288768dd6bd46e30c8c9 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Fri, 26 Jun 2026 15:08:58 +0000 Subject: [PATCH 2/4] docs: restore helper functions, runtime component docs, and troubleshooting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebalances the IMPLEMENTING_JRES.md rewrite — adds back lean sections for: - Helper function reference table (GetJREVersion, WriteJavaOpts, etc.) - profile.d script output example - Memory calculator: runtime output and user customization env vars - JVMKill: purpose, heap dump / volume service binding - Troubleshooting: JAVA_HOME, memory calculator, version resolution --- docs/IMPLEMENTING_JRES.md | 85 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/docs/IMPLEMENTING_JRES.md b/docs/IMPLEMENTING_JRES.md index 2aa9c60e5..0ffeaa443 100644 --- a/docs/IMPLEMENTING_JRES.md +++ b/docs/IMPLEMENTING_JRES.md @@ -10,6 +10,8 @@ This guide explains how to implement new JRE providers for the Cloud Foundry Jav - [BaseJRE — Shared Implementation](#basejre--shared-implementation) - [Implementation Steps](#implementation-steps) - [Examples](#examples) +- [Helper Functions](#helper-functions) +- [Runtime Components](#runtime-components) - [Testing JREs](#testing-jres) - [Troubleshooting](#troubleshooting) @@ -235,9 +237,71 @@ Replace `` with the uppercased `jreKey` value. bash scripts/unit.sh ``` +## Helper Functions + +These are available in `src/java/jres/` and called by `BaseJRE` internally. You may need them if you implement a JRE that does not use `BaseJRE` (e.g. Zing). + +| Function | Purpose | +|----------|---------| +| `GetJREVersion(ctx, jreKey)` | Resolves version from `BP_JAVA_VERSION`, `JBP_CONFIG__JRE`, then manifest default | +| `DetectJREByEnv(jreKey)` | Returns `true` if `JBP_CONFIG__JRE` or `JBP_CONFIG_COMPONENTS` selects this JRE | +| `WriteJavaHomeProfileD(ctx, jreDir, javaHome)` | Writes `profile.d/java.sh` exporting `JAVA_HOME`, `JRE_HOME`, and `PATH` | +| `WriteJavaOpts(ctx, opts)` | Appends opts to the centralized `.opts` file consumed by `profile.d/00_java_opts.sh` | +| `common.DetermineJavaVersion(javaHome)` | Reads `$JAVA_HOME/release` → returns Java major version as int | + +### Version resolution priority + +1. `BP_JAVA_VERSION` (e.g. `17`, `17.*`, `17.0.13`) +2. `JBP_CONFIG__JRE` with `version:` field +3. Manifest `default_versions` entry for this JRE key + +### profile.d output + +`WriteJavaHomeProfileD` produces `$DEPS_DIR//.profile.d/java.sh`: + +```bash +export JAVA_HOME=$DEPS_DIR/0/jre/jdk-17.0.13 +export JRE_HOME=$DEPS_DIR/0/jre/jdk-17.0.13 +export PATH=$JAVA_HOME/bin:$PATH +``` + +## Runtime Components + +`BaseJRE` installs and finalizes these automatically. Documented here for operational reference. + +### Memory Calculator + +Downloads `java-buildpack-memory-calculator` binary. At application start it computes JVM heap/metaspace/stack settings from `$MEMORY_LIMIT`: + +``` +-Xmx512M -Xms512M -XX:MaxMetaspaceSize=128M -Xss1M -XX:ReservedCodeCacheSize=32M +``` + +User customization via env: + +```bash +cf set-env myapp MEMORY_CALCULATOR_STACK_THREADS 300 +cf set-env myapp MEMORY_CALCULATOR_HEADROOM 10 # % headroom to leave +``` + +### JVMKill Agent + +Native agent (`.so`) that kills the JVM on `OutOfMemoryError` or memory allocation failure, causing CF to restart the container cleanly instead of hanging. + +Added to `JAVA_OPTS` as: +``` +-agentpath:/home/vcap/deps/0/jre/bin/jvmkill-1.16.0.so=printHeapHistogram=1 +``` + +**Heap dump support:** if a volume service tagged `heap-dump` is bound, JVMKill writes heap dumps to the mounted volume: + +```bash +cf bind-service myapp my-nfs -c '{"mount":"/volumes/heap-dumps","tags":["heap-dump"]}' +``` + ## Troubleshooting -**`findJavaHome` fails** — the tarball extracted to a directory that matches neither `dirPrefixes` nor `dirExacts`. Check the extraction layout: +**`findJavaHome` fails** — tarball extracted to directory matching neither `dirPrefixes` nor `dirExacts`. Check layout: ```bash tar tf my-jre.tar.gz | head -5 @@ -245,6 +309,21 @@ tar tf my-jre.tar.gz | head -5 Add the top-level directory prefix/name to `dirPrefixes`/`dirExacts` accordingly. -**Install fails with "No matching dependency"** — check the `jreKey` in `newBaseJRE` matches the `name:` field in `manifest.yml` exactly. +**Install fails with "No matching dependency"** — `jreKey` in `newBaseJRE` must match the `name:` field in `manifest.yml` exactly. + +**`extraFinalizeOpts` not applied** — set the function on `b` (the `BaseJRE` value) before wrapping: `b.extraFinalizeOpts = func() string { ... }`. Setting it after `return &MyJRE{b}` has no effect. + +**JAVA_HOME not set at runtime** — verify `profile.d/java.sh` exists: + +```bash +cf ssh myapp -- cat /home/vcap/deps/0/.profile.d/java.sh +``` + +**Memory calculator not running** — verify binary exists and `MEMORY_LIMIT` is set: + +```bash +cf ssh myapp -- ls /home/vcap/deps/0/jre/bin/java-buildpack-memory-calculator-* +cf ssh myapp -- echo $MEMORY_LIMIT +``` -**`extraFinalizeOpts` not applied** — ensure the function is set on `b` (the `BaseJRE` value) before wrapping it: `b.extraFinalizeOpts = func() string { ... }`. Setting it on the returned struct after `return &MyJRE{b}` has no effect. +**Wrong Java version selected** — check resolution order: `BP_JAVA_VERSION` → `JBP_CONFIG__JRE` → manifest default. Enable debug: `cf set-env myapp BP_LOG_LEVEL DEBUG`. From 88bcf93bc373de4432613f72403adf2d6afdabe1 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Fri, 26 Jun 2026 15:10:40 +0000 Subject: [PATCH 3/4] docs: add summary and cross-references to IMPLEMENTING_JRES.md --- docs/IMPLEMENTING_JRES.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/IMPLEMENTING_JRES.md b/docs/IMPLEMENTING_JRES.md index 0ffeaa443..1306ca0d4 100644 --- a/docs/IMPLEMENTING_JRES.md +++ b/docs/IMPLEMENTING_JRES.md @@ -327,3 +327,22 @@ cf ssh myapp -- echo $MEMORY_LIMIT ``` **Wrong Java version selected** — check resolution order: `BP_JAVA_VERSION` → `JBP_CONFIG__JRE` → manifest default. Enable debug: `cf set-env myapp BP_LOG_LEVEL DEBUG`. + +## Summary + +Adding a standard JRE (one that embeds `BaseJRE`) requires three things: + +1. **Create `src/java/jres/.go`** — embed `BaseJRE`, call `newBaseJRE()`, set `extraFinalizeOpts` if needed +2. **Add dependency to `manifest.yml`** — `name`, version, URI, SHA256, stacks; add to `default_versions` if applicable +3. **Register in `src/java/supply/supply.go`** — one `jreRegistry.Register(jres.NewMyJRE(ctx))` call + +`BaseJRE` handles everything else: download, extraction, `findJavaHome`, `profile.d` script, JVMKill, Memory Calculator, base JVM opts. + +For JREs that cannot use `BaseJRE` (e.g. Zing — no memory calculator, custom detection), implement the full `jres.JRE` interface manually and use the helper functions listed above. + +## See Also + +- [DEVELOPING.md](DEVELOPING.md) — building and running the buildpack locally +- [TESTING.md](TESTING.md) — unit and integration test framework +- [IMPLEMENTING_FRAMEWORKS.md](IMPLEMENTING_FRAMEWORKS.md) — adding framework support +- [IMPLEMENTING_CONTAINERS.md](IMPLEMENTING_CONTAINERS.md) — adding container types From 358bb87084d8bf3fbb455d4a4d4a0b5ad660b194 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Mon, 29 Jun 2026 08:31:50 +0000 Subject: [PATCH 4/4] docs: update JRE examples to Java 21, expand memory calculator config - Use Java 21 in version examples and profile.d output - Prefer JBP_CONFIG__JRE for memory calculator config (covers stack_threads, class_count, headroom) - Note MEMORY_CALCULATOR_* env vars take precedence but don't cover class_count - Add stack_threads/class_count/headroom descriptions --- docs/IMPLEMENTING_JRES.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/IMPLEMENTING_JRES.md b/docs/IMPLEMENTING_JRES.md index 1306ca0d4..c3caf4e07 100644 --- a/docs/IMPLEMENTING_JRES.md +++ b/docs/IMPLEMENTING_JRES.md @@ -251,7 +251,7 @@ These are available in `src/java/jres/` and called by `BaseJRE` internally. You ### Version resolution priority -1. `BP_JAVA_VERSION` (e.g. `17`, `17.*`, `17.0.13`) +1. `BP_JAVA_VERSION` (e.g. `21`, `21.*`, `21.0.5`) 2. `JBP_CONFIG__JRE` with `version:` field 3. Manifest `default_versions` entry for this JRE key @@ -260,8 +260,8 @@ These are available in `src/java/jres/` and called by `BaseJRE` internally. You `WriteJavaHomeProfileD` produces `$DEPS_DIR//.profile.d/java.sh`: ```bash -export JAVA_HOME=$DEPS_DIR/0/jre/jdk-17.0.13 -export JRE_HOME=$DEPS_DIR/0/jre/jdk-17.0.13 +export JAVA_HOME=$DEPS_DIR/0/jre/jdk-21.0.5 +export JRE_HOME=$DEPS_DIR/0/jre/jdk-21.0.5 export PATH=$JAVA_HOME/bin:$PATH ``` @@ -277,11 +277,23 @@ Downloads `java-buildpack-memory-calculator` binary. At application start it com -Xmx512M -Xms512M -XX:MaxMetaspaceSize=128M -Xss1M -XX:ReservedCodeCacheSize=32M ``` -User customization via env: +User customization — prefer `JBP_CONFIG__JRE` for structured config (covers all three knobs): + +```bash +cf set-env myapp JBP_CONFIG_OPEN_JDK_JRE \ + '{memory_calculator: {stack_threads: 300, class_count: 500, headroom: 10}}' +``` + +`stack_threads` — number of user threads (default: 200); affects `-Xss` heap budget. +`class_count` — estimated loaded classes (default: auto-detected); affects `-XX:MaxMetaspaceSize`. +`headroom` — percent of total memory to leave unallocated (default: 0). + +`MEMORY_CALCULATOR_*` env vars are a simpler alternative, but only cover two of the three knobs. They take precedence over `JBP_CONFIG_*` when both are set: ```bash cf set-env myapp MEMORY_CALCULATOR_STACK_THREADS 300 -cf set-env myapp MEMORY_CALCULATOR_HEADROOM 10 # % headroom to leave +cf set-env myapp MEMORY_CALCULATOR_HEADROOM 10 +# class_count not available as a MEMORY_CALCULATOR_* env var — use JBP_CONFIG_* for that ``` ### JVMKill Agent