Skip to content
20 changes: 20 additions & 0 deletions src/java/containers/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,23 @@ func (r *Registry) RegisterStandardContainers() {
r.Register(NewDistZipContainer(r.context))
r.Register(NewJavaMainContainer(r.context))
}

// This script is used to process the CLASSPATH assembled from various framework scripts sourced from profile.d
// to further create symlinks to the corresponding framework dependencies in WEB-INF/lib, BOOT-INF/lib and where ever
// needed thus they are available for application classloading
var symlinkScript = `#!/bin/bash
set -euo pipefail
TARGET_DIR="$PWD/%s"
CLASSPATH=${CLASSPATH:-}
mkdir -p "$TARGET_DIR"
# Split CLASSPATH on :
IFS=':' read -ra PATHS <<< "$CLASSPATH"
for p in "${PATHS[@]}"; do
# Skip empty entries
[[ -z "$p" ]] && continue
name=$(basename "$p")
link="$TARGET_DIR/$name"
ln -sf "$p" "$link"
echo "Created symlink: $link -> $p"
done
`
19 changes: 15 additions & 4 deletions src/java/containers/spring_boot.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package containers

import (
"github.com/cloudfoundry/java-buildpack/src/java/common"
"fmt"
"github.com/cloudfoundry/java-buildpack/src/java/common"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -218,6 +218,16 @@ func (s *SpringBootContainer) Finalize() error {
finalOpts = strings.Join(additionalOpts, " ")
}

buildDir := s.context.Stager.BuildDir()
bootInf := filepath.Join(buildDir, "BOOT-INF")
if _, err := os.Stat(bootInf); err == nil {
// the script name is prefixed with 'zzz' as it is important to be the last script sourced from profile.d
// so that the previous scripts assembling the CLASSPATH variable(left from frameworks) are sourced previous to it.
if err := s.context.Stager.WriteProfileD("zzz_classpath_symlinks.sh", fmt.Sprintf(symlinkScript, filepath.Join("BOOT-INF", "lib"))); err != nil {
return fmt.Errorf("failed to write zzz_classpath_symlinks.sh: %w", err)
}
}

// Write combined JAVA_OPTS
if err := s.context.Stager.WriteEnvFile("JAVA_OPTS", finalOpts); err != nil {
return fmt.Errorf("failed to write JAVA_OPTS: %w", err)
Expand All @@ -234,20 +244,21 @@ func (s *SpringBootContainer) Release() (string, error) {
bootInf := filepath.Join(buildDir, "BOOT-INF")
if _, err := os.Stat(bootInf); err == nil {
// Verify this is actually a Spring Boot application

if s.isSpringBootExplodedJar(buildDir) {
// True Spring Boot exploded JAR - use JarLauncher
// Determine the correct JarLauncher class name based on Spring Boot version
jarLauncherClass := s.getJarLauncherClass(buildDir)
// Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity)
return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp . %s", jarLauncherClass), nil
return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp $PWD/.${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s", jarLauncherClass), nil
}

// Exploded JAR but NOT Spring Boot - use Main-Class from MANIFEST.MF
mainClass := s.readMainClassFromManifest(buildDir)
if mainClass != "" {
// Use classpath from BOOT-INF/classes and BOOT-INF/lib
// Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity)
return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp $HOME:$HOME/BOOT-INF/classes:$HOME/BOOT-INF/lib/* %s", mainClass), nil
return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp $HOME${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER}:$HOME/BOOT-INF/classes:$HOME/BOOT-INF/lib/* %s", mainClass), nil
}

return "", fmt.Errorf("exploded JAR found but no Main-Class in MANIFEST.MF")
Expand All @@ -270,7 +281,7 @@ func (s *SpringBootContainer) Release() (string, error) {
}

// Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity)
cmd := fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -jar %s", jarFile)
cmd := fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS ${CONTAINER_SECURITY_PROVIDER:+-Dloader.path=$CONTAINER_SECURITY_PROVIDER} -jar %s", jarFile)
return cmd, nil
}

Expand Down
13 changes: 11 additions & 2 deletions src/java/containers/tomcat.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,12 @@ func (t *TomcatContainer) createSetenvScript(tomcatDir, loggingSupportJar string
setenvPath := filepath.Join(binDir, "setenv.sh")

jarPath := "$CATALINA_HOME/bin/" + loggingSupportJar

// Note that Tomcat builds its own CLASSPATH env before starting. It ensures that any user defined CLASSPATH variables
// are not used on startup, as can be seen in the catalina.sh script. That is why even we have something already
// sourced in CLASSPATH env from profile.d scripts it is disregarded on Tomcat startup and fresh CLASSPATH env is
// built here in the setenv.sh script.
setenvContent := fmt.Sprintf(`#!/bin/sh
JAVA_OPTS="$JAVA_OPTS -Xbootclasspath/a:%s"
CLASSPATH="%s${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER}"
`, jarPath)

if err := os.WriteFile(setenvPath, []byte(setenvContent), 0755); err != nil {
Expand Down Expand Up @@ -604,6 +607,12 @@ func (t *TomcatContainer) Finalize() error {

webInf := filepath.Join(buildDir, "WEB-INF")
if _, err := os.Stat(webInf); err == nil {
// the script name is prefixed with 'zzz' as it is important to be the last script sourced from profile.d
// so that the previous scripts assembling the CLASSPATH variable(left from frameworks) are sourced previous to it.
if err := t.context.Stager.WriteProfileD("zzz_classpath_symlinks.sh", fmt.Sprintf(symlinkScript, filepath.Join("WEB-INF", "lib"))); err != nil {
return fmt.Errorf("failed to write zzz_classpath_symlinks.sh: %w", err)
}

contextXMLDir := filepath.Dir(contextXMLPath)
if err := os.MkdirAll(contextXMLDir, 0755); err != nil {
return fmt.Errorf("failed to create context directory: %w", err)
Expand Down
12 changes: 2 additions & 10 deletions src/java/finalize/finalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func Run(f *Finalizer) error {
}

// Finalize frameworks (APM agents, etc.)
if err := f.finalizeFrameworks(); err != nil {
if err := f.finalizeFrameworks(ctx); err != nil {
f.Log.Error("Failed to finalize frameworks: %s", err.Error())
return err
}
Expand Down Expand Up @@ -158,17 +158,9 @@ func (f *Finalizer) finalizeJRE() error {
}

// finalizeFrameworks finalizes framework components (APM agents, etc.)
func (f *Finalizer) finalizeFrameworks() error {
func (f *Finalizer) finalizeFrameworks(ctx *common.Context) error {
f.Log.BeginStep("Finalizing frameworks")

ctx := &common.Context{
Stager: f.Stager,
Manifest: f.Manifest,
Installer: f.Installer,
Log: f.Log,
Command: f.Command,
}

registry := frameworks.NewRegistry(ctx)
registry.RegisterStandardFrameworks()

Expand Down
18 changes: 9 additions & 9 deletions src/java/frameworks/client_certificate_mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,17 @@ func (c *ClientCertificateMapperFramework) Finalize() error {
return nil
}

// Add to classpath via CLASSPATH environment variable
classpath := os.Getenv("CLASSPATH")
if classpath != "" {
classpath += ":"
}
classpath += matches[0]
depsIdx := c.context.Stager.DepsIdx()
runtimePath := fmt.Sprintf("$DEPS_DIR/%s/client_certificate_mapper/%s", depsIdx, filepath.Base(matches[0]))

profileScript := fmt.Sprintf("export CLASSPATH=\"%s${CLASSPATH:+:$CLASSPATH}\"\n", runtimePath)

if err := c.context.Stager.WriteEnvFile("CLASSPATH", classpath); err != nil {
return fmt.Errorf("failed to set CLASSPATH for Client Certificate Mapper: %w", err)
if err := c.context.Stager.WriteProfileD("client_certificate_mapper.sh", profileScript); err != nil {
return fmt.Errorf("failed to write client_certificate_mapper.sh profile.d script: %w", err)
}


c.context.Log.Debug("Client Certificate Mapper JAR will be added to classpath at runtime: %s", runtimePath)

return nil
}

Expand Down
8 changes: 6 additions & 2 deletions src/java/frameworks/container_security_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,13 @@ func (c *ContainerSecurityProviderFramework) Finalize() error {
// Build JAVA_OPTS with runtime paths using $DEPS_DIR
var javaOpts string
if javaVersion >= 9 {
// Java 9+: Add to bootstrap classpath via -Xbootclasspath/a
runtimeJarPath := fmt.Sprintf("$DEPS_DIR/%s/container_security_provider/%s", depsIdx, jarFilename)
javaOpts = fmt.Sprintf("-Xbootclasspath/a:%s", runtimeJarPath)

profileScript := fmt.Sprintf("export CONTAINER_SECURITY_PROVIDER=\"%s\"\n", runtimeJarPath)

if err := c.context.Stager.WriteProfileD("container_security_provider.sh", profileScript); err != nil {
return fmt.Errorf("failed to write container_security_provider.sh profile.d script: %w", err)
}
} else {
// Java 8: Use extension directory
runtimeProviderDir := fmt.Sprintf("$DEPS_DIR/%s/container_security_provider", depsIdx)
Expand Down
17 changes: 8 additions & 9 deletions src/java/frameworks/java_cf_env.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package frameworks

import (
"github.com/cloudfoundry/java-buildpack/src/java/common"
"fmt"
"github.com/cloudfoundry/java-buildpack/src/java/common"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -81,17 +81,16 @@ func (j *JavaCfEnvFramework) Finalize() error {
return nil
}

// Add to classpath via CLASSPATH environment variable
classpath := os.Getenv("CLASSPATH")
if classpath != "" {
classpath += ":"
}
classpath += matches[0]
depsIdx := j.context.Stager.DepsIdx()
runtimePath := fmt.Sprintf("$DEPS_DIR/%s/java_cf_env/%s", depsIdx, filepath.Base(matches[0]))

if err := j.context.Stager.WriteEnvFile("CLASSPATH", classpath); err != nil {
return fmt.Errorf("failed to set CLASSPATH for Java CF Env: %w", err)
profileScript := fmt.Sprintf("export CLASSPATH=\"%s${CLASSPATH:+:$CLASSPATH}\"\n", runtimePath)
if err := j.context.Stager.WriteProfileD("java_cf_env.sh", profileScript); err != nil {
return fmt.Errorf("failed to write java_cf_env.sh profile.d script: %w", err)
}

j.context.Log.Debug("Java CF Env JAR will be added to classpath at runtime: %s", runtimePath)

return nil
}

Expand Down
9 changes: 6 additions & 3 deletions src/java/frameworks/maria_db_jdbc.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,16 @@ func (f *MariaDBJDBCFramework) Finalize() error {

f.context.Log.BeginStep("Configuring MariaDB JDBC driver")

// Add to CLASSPATH environment variable
if err := f.context.Stager.WriteEnvFile("CLASSPATH", f.jarPath); err != nil {
depsIdx := f.context.Stager.DepsIdx()
runtimePath := fmt.Sprintf("$DEPS_DIR/%s/mariadb_jdbc/%s", depsIdx, filepath.Base(f.jarPath))

profileScript := fmt.Sprintf("export CLASSPATH=\"%s${CLASSPATH:+:$CLASSPATH}\"\n", runtimePath)
if err := f.context.Stager.WriteProfileD("mariadb_jdbc.sh", profileScript); err != nil {
f.context.Log.Warning("Failed to add MariaDB JDBC to CLASSPATH: %s", err)
return nil // Non-blocking
}

f.context.Log.Info("MariaDB JDBC driver added to CLASSPATH")
f.context.Log.Debug("Maria JDBC will be added to classpath at runtime: %s", runtimePath)
return nil
}

Expand Down
16 changes: 7 additions & 9 deletions src/java/frameworks/postgresql_jdbc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package frameworks
import (
"fmt"
"github.com/cloudfoundry/java-buildpack/src/java/common"
"os"
"path/filepath"
"strings"

Expand Down Expand Up @@ -73,17 +72,16 @@ func (p *PostgresqlJdbcFramework) Finalize() error {
return nil
}

// Add to classpath via CLASSPATH environment variable
classpath := os.Getenv("CLASSPATH")
if classpath != "" {
classpath += ":"
}
classpath += matches[0]
depsIdx := p.context.Stager.DepsIdx()
runtimePath := fmt.Sprintf("$DEPS_DIR/%s/postgresql_jdbc/%s", depsIdx, filepath.Base(matches[0]))

if err := p.context.Stager.WriteEnvFile("CLASSPATH", classpath); err != nil {
return fmt.Errorf("failed to set CLASSPATH for PostgreSQL JDBC: %w", err)
profileScript := fmt.Sprintf("export CLASSPATH=\"%s${CLASSPATH:+:$CLASSPATH}\"\n", runtimePath)
if err := p.context.Stager.WriteProfileD("postgresql_jdbc.sh", profileScript); err != nil {
return fmt.Errorf("failed to write postgresql_jdbc.sh profile.d script: %w", err)
}

p.context.Log.Debug("PostgreSQL JDBC JAR will be added to classpath at runtime: %s", runtimePath)

return nil
}

Expand Down
15 changes: 7 additions & 8 deletions src/java/frameworks/spring_auto_reconfiguration.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,16 @@ func (s *SpringAutoReconfigurationFramework) Finalize() error {
return nil
}

// Add to classpath via CLASSPATH environment variable
classpath := os.Getenv("CLASSPATH")
if classpath != "" {
classpath += ":"
}
classpath += matches[0]
depsIdx := s.context.Stager.DepsIdx()
runtimePath := fmt.Sprintf("$DEPS_DIR/%s/spring_auto_reconfiguration/%s", depsIdx, filepath.Base(matches[0]))

if err := s.context.Stager.WriteEnvFile("CLASSPATH", classpath); err != nil {
return fmt.Errorf("failed to set CLASSPATH for Spring Auto-reconfiguration: %w", err)
profileScript := fmt.Sprintf("export CLASSPATH=\"%s${CLASSPATH:+:$CLASSPATH}\"\n", runtimePath)
if err := s.context.Stager.WriteProfileD("spring_auto_reconfiguration.sh", profileScript); err != nil {
return fmt.Errorf("failed to write spring_auto_reconfiguration.sh profile.d script: %w", err)
}

s.context.Log.Debug("Spring Auto-reconfiguration JAR will be added to classpath at runtime: %s", runtimePath)

return nil
}

Expand Down
1 change: 1 addition & 0 deletions src/java/resources/files/tomcat/conf/context.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
-->

<Context>
<Resources allowLinking='true'/>
</Context>