From e2d7c540550e167c93227af8e43de2eed6102f01 Mon Sep 17 00:00:00 2001 From: Kiril Keranov <114745615+kiril-keranov@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:05:36 +0200 Subject: [PATCH 01/10] Refactor classpath handling for client certificate mapper --- .../frameworks/client_certificate_mapper.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/java/frameworks/client_certificate_mapper.go b/src/java/frameworks/client_certificate_mapper.go index 879e2819a..f0fae5ed4 100644 --- a/src/java/frameworks/client_certificate_mapper.go +++ b/src/java/frameworks/client_certificate_mapper.go @@ -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 } From f4b7d9821ec92ed832fe7e774bc40c71c55e6299 Mon Sep 17 00:00:00 2001 From: Kiril Keranov Date: Tue, 10 Mar 2026 13:14:06 +0200 Subject: [PATCH 02/10] Fix classpath handling for dependency frameworks --- src/java/frameworks/java_cf_env.go | 17 ++++++++--------- src/java/frameworks/maria_db_jdbc.go | 7 +++++-- src/java/frameworks/postgresql_jdbc.go | 16 +++++++--------- .../frameworks/spring_auto_reconfiguration.go | 15 +++++++-------- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/java/frameworks/java_cf_env.go b/src/java/frameworks/java_cf_env.go index 3924342b9..f1a0294ef 100644 --- a/src/java/frameworks/java_cf_env.go +++ b/src/java/frameworks/java_cf_env.go @@ -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" @@ -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 } diff --git a/src/java/frameworks/maria_db_jdbc.go b/src/java/frameworks/maria_db_jdbc.go index 3f8010e74..da85c65f1 100644 --- a/src/java/frameworks/maria_db_jdbc.go +++ b/src/java/frameworks/maria_db_jdbc.go @@ -88,8 +88,11 @@ 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 } diff --git a/src/java/frameworks/postgresql_jdbc.go b/src/java/frameworks/postgresql_jdbc.go index f8f99605b..8d9310a4a 100644 --- a/src/java/frameworks/postgresql_jdbc.go +++ b/src/java/frameworks/postgresql_jdbc.go @@ -3,7 +3,6 @@ package frameworks import ( "fmt" "github.com/cloudfoundry/java-buildpack/src/java/common" - "os" "path/filepath" "strings" @@ -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 } diff --git a/src/java/frameworks/spring_auto_reconfiguration.go b/src/java/frameworks/spring_auto_reconfiguration.go index 897ab8a1f..8d4d66885 100644 --- a/src/java/frameworks/spring_auto_reconfiguration.go +++ b/src/java/frameworks/spring_auto_reconfiguration.go @@ -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 } From 13c265868092821420bb6ffb710a08590a7c6748 Mon Sep 17 00:00:00 2001 From: Kiril Keranov Date: Tue, 10 Mar 2026 13:25:26 +0200 Subject: [PATCH 03/10] Adjust symlink dep creation for tomcat;refactor finalize --- src/java/containers/tomcat.go | 23 +++++++++++++++++++++++ src/java/finalize/finalize.go | 12 ++---------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/java/containers/tomcat.go b/src/java/containers/tomcat.go index be53ad148..84b8bd37a 100644 --- a/src/java/containers/tomcat.go +++ b/src/java/containers/tomcat.go @@ -604,6 +604,12 @@ func (t *TomcatContainer) Finalize() error { webInf := filepath.Join(buildDir, "WEB-INF") if _, err := os.Stat(webInf); err == nil { + // the fix 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 were sourced previous to it. + if err := t.context.Stager.WriteProfileD("zzz_classpath_symlinks.sh", symlinkScript); 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) @@ -644,3 +650,20 @@ func (t *TomcatContainer) Release() (string, error) { return cmd, nil } + +var symlinkScript = fmt.Sprintf(`#!/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 +`, filepath.Join("WEB-INF", "lib")) diff --git a/src/java/finalize/finalize.go b/src/java/finalize/finalize.go index 665b41344..55802df07 100644 --- a/src/java/finalize/finalize.go +++ b/src/java/finalize/finalize.go @@ -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 } @@ -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() From cf94d9ee9d6f774859ae632519bec3efc0a77ede Mon Sep 17 00:00:00 2001 From: Kiril Keranov Date: Tue, 10 Mar 2026 16:16:16 +0200 Subject: [PATCH 04/10] Adjust symlink dep creation --- src/java/containers/container.go | 17 +++++++++++++++++ src/java/containers/spring_boot.go | 5 ++++- src/java/containers/tomcat.go | 19 +------------------ 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/java/containers/container.go b/src/java/containers/container.go index 6a5b0a1c2..8feed5dd5 100644 --- a/src/java/containers/container.go +++ b/src/java/containers/container.go @@ -107,3 +107,20 @@ func (r *Registry) RegisterStandardContainers() { r.Register(NewDistZipContainer(r.context)) r.Register(NewJavaMainContainer(r.context)) } + +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 +` diff --git a/src/java/containers/spring_boot.go b/src/java/containers/spring_boot.go index cabc4d4ef..bc9091dcb 100644 --- a/src/java/containers/spring_boot.go +++ b/src/java/containers/spring_boot.go @@ -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" @@ -234,6 +234,9 @@ 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 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 diff --git a/src/java/containers/tomcat.go b/src/java/containers/tomcat.go index 84b8bd37a..92fa69d4d 100644 --- a/src/java/containers/tomcat.go +++ b/src/java/containers/tomcat.go @@ -606,7 +606,7 @@ func (t *TomcatContainer) Finalize() error { if _, err := os.Stat(webInf); err == nil { // the fix 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 were sourced previous to it. - if err := t.context.Stager.WriteProfileD("zzz_classpath_symlinks.sh", symlinkScript); err != nil { + 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) } @@ -650,20 +650,3 @@ func (t *TomcatContainer) Release() (string, error) { return cmd, nil } - -var symlinkScript = fmt.Sprintf(`#!/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 -`, filepath.Join("WEB-INF", "lib")) From 61c927949f5ea53e78564873850451d3a6b55950 Mon Sep 17 00:00:00 2001 From: Kiril Keranov Date: Tue, 10 Mar 2026 16:40:27 +0200 Subject: [PATCH 05/10] Adjust comments --- src/java/containers/spring_boot.go | 3 +++ src/java/containers/tomcat.go | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/java/containers/spring_boot.go b/src/java/containers/spring_boot.go index bc9091dcb..303992e1d 100644 --- a/src/java/containers/spring_boot.go +++ b/src/java/containers/spring_boot.go @@ -234,6 +234,9 @@ 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) } diff --git a/src/java/containers/tomcat.go b/src/java/containers/tomcat.go index 92fa69d4d..f264133ba 100644 --- a/src/java/containers/tomcat.go +++ b/src/java/containers/tomcat.go @@ -604,8 +604,8 @@ func (t *TomcatContainer) Finalize() error { webInf := filepath.Join(buildDir, "WEB-INF") if _, err := os.Stat(webInf); err == nil { - // the fix 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 were sourced previous to it. + // 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) } From 7926d76e0d06fdaf914a29b123d8e63ac2cefd4b Mon Sep 17 00:00:00 2001 From: Kiril Keranov Date: Thu, 12 Mar 2026 15:00:30 +0200 Subject: [PATCH 06/10] Adjust classpath for csp;allow linking in tomcat --- src/java/containers/spring_boot.go | 4 ++-- src/java/containers/tomcat.go | 2 +- src/java/frameworks/container_security_provider.go | 8 ++++++-- src/java/resources/files/tomcat/conf/context.xml | 1 + 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/java/containers/spring_boot.go b/src/java/containers/spring_boot.go index 303992e1d..9c23c340e 100644 --- a/src/java/containers/spring_boot.go +++ b/src/java/containers/spring_boot.go @@ -245,7 +245,7 @@ func (s *SpringBootContainer) Release() (string, error) { // 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 @@ -253,7 +253,7 @@ func (s *SpringBootContainer) Release() (string, error) { 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") diff --git a/src/java/containers/tomcat.go b/src/java/containers/tomcat.go index f264133ba..6b28c2ed1 100644 --- a/src/java/containers/tomcat.go +++ b/src/java/containers/tomcat.go @@ -258,7 +258,7 @@ func (t *TomcatContainer) createSetenvScript(tomcatDir, loggingSupportJar string jarPath := "$CATALINA_HOME/bin/" + loggingSupportJar 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 { diff --git a/src/java/frameworks/container_security_provider.go b/src/java/frameworks/container_security_provider.go index 149391c8b..4773390e6 100644 --- a/src/java/frameworks/container_security_provider.go +++ b/src/java/frameworks/container_security_provider.go @@ -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) diff --git a/src/java/resources/files/tomcat/conf/context.xml b/src/java/resources/files/tomcat/conf/context.xml index 3fd6a275d..f4c2d1783 100644 --- a/src/java/resources/files/tomcat/conf/context.xml +++ b/src/java/resources/files/tomcat/conf/context.xml @@ -17,4 +17,5 @@ --> + From 7c6606fd0a7f52e3a614ea667f7d0a6229ac5bf6 Mon Sep 17 00:00:00 2001 From: Kiril Keranov Date: Thu, 12 Mar 2026 15:24:17 +0200 Subject: [PATCH 07/10] Add comments --- src/java/containers/container.go | 3 +++ src/java/containers/tomcat.go | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/java/containers/container.go b/src/java/containers/container.go index 8feed5dd5..86f6f0b85 100644 --- a/src/java/containers/container.go +++ b/src/java/containers/container.go @@ -108,6 +108,9 @@ func (r *Registry) RegisterStandardContainers() { 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" diff --git a/src/java/containers/tomcat.go b/src/java/containers/tomcat.go index 6b28c2ed1..9e5168993 100644 --- a/src/java/containers/tomcat.go +++ b/src/java/containers/tomcat.go @@ -256,7 +256,10 @@ 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 CLASSPATH="%s${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER}" `, jarPath) From cf9a2e56835d106de8c484e933dea1f5e88b5ac3 Mon Sep 17 00:00:00 2001 From: Kiril Keranov Date: Thu, 12 Mar 2026 15:45:26 +0200 Subject: [PATCH 08/10] Adjustments+debug log --- src/java/containers/spring_boot.go | 2 +- src/java/frameworks/maria_db_jdbc.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/java/containers/spring_boot.go b/src/java/containers/spring_boot.go index 9c23c340e..a4e4fe45c 100644 --- a/src/java/containers/spring_boot.go +++ b/src/java/containers/spring_boot.go @@ -276,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 } diff --git a/src/java/frameworks/maria_db_jdbc.go b/src/java/frameworks/maria_db_jdbc.go index da85c65f1..3867494f4 100644 --- a/src/java/frameworks/maria_db_jdbc.go +++ b/src/java/frameworks/maria_db_jdbc.go @@ -97,7 +97,7 @@ func (f *MariaDBJDBCFramework) Finalize() error { 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 } From 2a8534f22f18943b16622e95866dcd8f80624fe0 Mon Sep 17 00:00:00 2001 From: Kiril Keranov Date: Thu, 12 Mar 2026 16:29:03 +0200 Subject: [PATCH 09/10] Adjust symlink script --- src/java/containers/container.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/java/containers/container.go b/src/java/containers/container.go index 86f6f0b85..d525a9916 100644 --- a/src/java/containers/container.go +++ b/src/java/containers/container.go @@ -112,7 +112,7 @@ func (r *Registry) RegisterStandardContainers() { // 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 +set -euo pipefail TARGET_DIR="$PWD/%s" CLASSPATH=${CLASSPATH:-} mkdir -p "$TARGET_DIR" @@ -123,7 +123,7 @@ for p in "${PATHS[@]}"; do [[ -z "$p" ]] && continue name=$(basename "$p") link="$TARGET_DIR/$name" - ln -s "$p" "$link" + ln -sf "$p" "$link" echo "Created symlink: $link -> $p" done ` From fe851f859d17802aa809c1776a6f3862b84ba7cb Mon Sep 17 00:00:00 2001 From: Kiril Keranov Date: Thu, 12 Mar 2026 16:34:51 +0200 Subject: [PATCH 10/10] Move to finalize instead of release for springboot --- src/java/containers/spring_boot.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/java/containers/spring_boot.go b/src/java/containers/spring_boot.go index a4e4fe45c..9d4df698d 100644 --- a/src/java/containers/spring_boot.go +++ b/src/java/containers/spring_boot.go @@ -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) @@ -235,11 +245,6 @@ func (s *SpringBootContainer) Release() (string, error) { 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