diff --git a/src/java/frameworks/java_opts_writer.go b/src/java/frameworks/java_opts_writer.go index abfd1c3c7..4bfd0a6d1 100644 --- a/src/java/frameworks/java_opts_writer.go +++ b/src/java/frameworks/java_opts_writer.go @@ -73,7 +73,9 @@ func CreateJavaOptsAssemblyScript(ctx *common.Context) error { # Expands runtime variables like $DEPS_DIR, $HOME, $JAVA_OPTS, and all other environment variables # Save original JAVA_OPTS from environment (user-provided) -USER_JAVA_OPTS="$JAVA_OPTS" +# Normalize to single line: YAML block scalars (>) may introduce newlines +# xargs trims leading/trailing whitespace and collapses internal spaces +USER_JAVA_OPTS=$(echo "$JAVA_OPTS" | tr '\n' ' ' | tr -s ' ' | xargs) # Start building new JAVA_OPTS JAVA_OPTS="" @@ -84,19 +86,17 @@ if [ -d "$DEPS_DIR/%s/java_opts" ]; then # Read content and expand runtime variables opts_content=$(cat "$opts_file") - # First, expand special variables that need specific handling - # Expand $DEPS_DIR variable - opts_content=$(echo "$opts_content" | sed "s|\$DEPS_DIR|$DEPS_DIR|g") + # Expand $DEPS_DIR, $HOME, $JAVA_OPTS using bash parameter expansion. + # sed-based substitution breaks when these values contain the sed delimiter (|), + # backslashes, ampersands, or newlines — all valid in JAVA_OPTS and paths. + opts_content="${opts_content//\$DEPS_DIR/$DEPS_DIR}" + opts_content="${opts_content//\$HOME/$HOME}" + opts_content="${opts_content//\$JAVA_OPTS/$USER_JAVA_OPTS}" - # Expand $HOME variable (for app-provided JARs like AspectJ) - opts_content=$(echo "$opts_content" | sed "s|\$HOME|$HOME|g") - - # Expand $JAVA_OPTS to the saved USER_JAVA_OPTS value (not the loop's current JAVA_OPTS) - opts_content=$(echo "$opts_content" | sed "s|\$JAVA_OPTS|$USER_JAVA_OPTS|g") - - # Now expand all remaining environment variables using eval with proper escaping - # This mimics Ruby buildpack behavior where shell naturally expands variables - # Use eval in a subshell to safely expand variables without executing commands + # Expand any remaining environment variables in opts content via eval. + # Note: eval executes commands, but .opts files are written by the buildpack + # at staging time and run within the container context. + # This matches how the Ruby buildpack naturally expanded variables via shell. opts_content=$(eval "echo \"$opts_content\"") if [ -n "$opts_content" ]; then diff --git a/src/java/frameworks/java_opts_writer_test.go b/src/java/frameworks/java_opts_writer_test.go index e4c154ac0..e092ec5df 100644 --- a/src/java/frameworks/java_opts_writer_test.go +++ b/src/java/frameworks/java_opts_writer_test.go @@ -2,14 +2,55 @@ package frameworks_test import ( "os" + "os/exec" + "path/filepath" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + "github.com/cloudfoundry/java-buildpack/src/java/common" + "github.com/cloudfoundry/java-buildpack/src/java/frameworks" + "github.com/cloudfoundry/libbuildpack" ) +func newJavaOptsContext(buildDir, cacheDir, depsDir string) *common.Context { + logger := libbuildpack.NewLogger(GinkgoWriter) + manifest := &libbuildpack.Manifest{} + installer := &libbuildpack.Installer{} + stager := libbuildpack.NewStager([]string{buildDir, cacheDir, depsDir, "0"}, logger, manifest) + return &common.Context{ + Stager: stager, + Manifest: manifest, + Installer: installer, + Log: logger, + Command: &libbuildpack.Command{}, + } +} + var _ = Describe("Java Opts Writer", func() { + var ( + buildDir string + cacheDir string + depsDir string + ctx *common.Context + ) + + BeforeEach(func() { + var err error + buildDir, err = os.MkdirTemp("", "build") + Expect(err).NotTo(HaveOccurred()) + cacheDir, err = os.MkdirTemp("", "cache") + Expect(err).NotTo(HaveOccurred()) + depsDir, err = os.MkdirTemp("", "deps") + Expect(err).NotTo(HaveOccurred()) + ctx = newJavaOptsContext(buildDir, cacheDir, depsDir) + }) + AfterEach(func() { os.Unsetenv("JAVA_OPTS") + os.RemoveAll(buildDir) + os.RemoveAll(cacheDir) + os.RemoveAll(depsDir) }) Describe("Basic options", func() { @@ -20,4 +61,61 @@ var _ = Describe("Java Opts Writer", func() { Expect(os.Getenv("JAVA_OPTS")).To(Equal(javaOpts)) }) }) + + Describe("CreateJavaOptsAssemblyScript", func() { + runScript := func(javaOpts string, optsFileContent string) (string, error) { + err := frameworks.CreateJavaOptsAssemblyScript(ctx) + Expect(err).NotTo(HaveOccurred()) + + optsDir := filepath.Join(depsDir, "0", "java_opts") + Expect(os.MkdirAll(optsDir, 0755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(optsDir, "42_agent.opts"), []byte(optsFileContent), 0644)).To(Succeed()) + + scriptPath := filepath.Join(depsDir, "0", "profile.d", "00_java_opts.sh") + cmd := exec.Command("bash", "-c", "source "+scriptPath+" && echo \"$JAVA_OPTS\"") + cmd.Env = append(os.Environ(), + "JAVA_OPTS="+javaOpts, + "DEPS_DIR="+depsDir, + "HOME=/home/vcap/app", + ) + output, err := cmd.CombinedOutput() + return string(output), err + } + + It("handles multiline JAVA_OPTS from YAML block scalar without sed error", func() { + // Reproduce the manifest pattern: + // JAVA_OPTS: > + // -javaagent:$HOME/BOOT-INF/lib/agent.jar + // -XX:+UseZGC + // YAML '>' folds newlines to spaces, but CF may deliver them as literal newlines + multilineJavaOpts := "-javaagent:$HOME/BOOT-INF/lib/agent.jar\n-XX:+UseZGC\n-XX:+AlwaysPreTouch" + + output, err := runScript(multilineJavaOpts, "-javaagent:somepath.jar $JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring("-XX:+UseZGC")) + }) + + It("handles pipe character in JAVA_OPTS (e.g. javaagent options) without sed error", func() { + // Reproduce the manifest pattern: + // JAVA_OPTS: > + // -javaagent:$HOME/BOOT-INF/lib/jfr-exporter.jar=enableExecutorMBeans|disableMyFeature + pipeJavaOpts := "-javaagent:$HOME/BOOT-INF/lib/jfr-exporter.jar=enableExecutorMBeans|disableMyFeature" + + output, err := runScript(pipeJavaOpts, "-javaagent:somepath.jar $JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring("enableExecutorMBeans|disableMyFeature")) + }) + + It("expands $HOME in opts file content", func() { + output, err := runScript("", "-javaagent:$HOME/BOOT-INF/lib/agent.jar") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring("-javaagent:/home/vcap/app/BOOT-INF/lib/agent.jar")) + }) + + It("expands $DEPS_DIR in opts file content", func() { + output, err := runScript("", "-Djava.security.properties=$DEPS_DIR/0/security.properties") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring("-Djava.security.properties=" + depsDir + "/0/security.properties")) + }) + }) })