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
18 changes: 8 additions & 10 deletions src/java/frameworks/java_opts_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ 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
USER_JAVA_OPTS=$(echo "$JAVA_OPTS" | tr '\n' ' ' | tr -s ' ')

# Start building new JAVA_OPTS
JAVA_OPTS=""
Expand All @@ -84,15 +85,12 @@ 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 $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")
# 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}"

# Now expand all remaining environment variables using eval with proper escaping
# This mimics Ruby buildpack behavior where shell naturally expands variables
Expand Down
98 changes: 98 additions & 0 deletions src/java/frameworks/java_opts_writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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"))
})
})
})