Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 -uo 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 -s "$p" "$link"
echo "Created symlink: $link -> $p"
done
`
14 changes: 10 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 @@ -234,20 +234,26 @@ 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

// 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)
}
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 +276,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>