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
120 changes: 83 additions & 37 deletions src/java/containers/java_main.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package containers

import (
"github.com/cloudfoundry/java-buildpack/src/java/common"
"archive/zip"
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/cloudfoundry/java-buildpack/src/java/common"
)

// JavaMainContainer handles standalone JAR applications with a main class
Expand Down Expand Up @@ -56,7 +59,9 @@ func (j *JavaMainContainer) Detect() (string, error) {
return "", nil
}

// findMainClass searches for a JAR with a Main-Class manifest entry
// findMainClass searches for a JAR in buildDir whose META-INF/MANIFEST.MF
// contains a Main-Class entry. Returns the main class name and the path to
// the JAR (relative to $HOME) if found, or empty strings if none qualify.
func (j *JavaMainContainer) findMainClass(buildDir string) (string, string) {
entries, err := os.ReadDir(buildDir)
if err != nil {
Expand All @@ -69,30 +74,79 @@ func (j *JavaMainContainer) findMainClass(buildDir string) (string, string) {
}

name := entry.Name()
if strings.HasSuffix(name, ".jar") {
// TODO: In full implementation, extract and read MANIFEST.MF
// For now, assume any JAR could be a main JAR
return "Main", filepath.Join("$HOME", name)
if !strings.HasSuffix(name, ".jar") {
continue
}

jarPath := filepath.Join(buildDir, name)
if mainClass := readMainClassFromJar(jarPath); mainClass != "" {
return mainClass, filepath.Join("$HOME", name)
}
}

return "", ""
}

// readMainClassFromJar opens a JAR (zip) file and reads the Main-Class
// attribute from META-INF/MANIFEST.MF, returning "" if not present or on error.
func readMainClassFromJar(jarPath string) string {
r, err := zip.OpenReader(jarPath)
if err != nil {
return ""
}
defer r.Close()

for _, f := range r.File {
if f.Name != "META-INF/MANIFEST.MF" {
continue
}

rc, err := f.Open()
if err != nil {
return ""
}

data, err := io.ReadAll(rc)
rc.Close()
if err != nil {
return ""
}

return parseMainClass(string(data))
}

return ""
}

// readMainClassFromManifest reads the Main-Class from a manifest file
func (j *JavaMainContainer) readMainClassFromManifest(manifestPath string) string {
data, err := os.ReadFile(manifestPath)
if err != nil {
return ""
}

// Parse MANIFEST.MF file (simple line-by-line parsing)
lines := strings.Split(string(data), "\n")
for _, line := range lines {
return parseMainClass(string(data))
}

// parseMainClass extracts the Main-Class value from MANIFEST.MF content.
// Handles line continuations (lines starting with a space are folded onto the previous line).
func parseMainClass(content string) string {
// Unfold continuation lines (space at start of line means continuation)
content = strings.ReplaceAll(content, "\r\n", "\n")
var unfolded strings.Builder
for _, line := range strings.Split(content, "\n") {
if strings.HasPrefix(line, " ") {
unfolded.WriteString(strings.TrimPrefix(line, " "))
} else {
unfolded.WriteString("\n")
unfolded.WriteString(line)
}
}

for _, line := range strings.Split(unfolded.String(), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "Main-Class:") {
mainClass := strings.TrimSpace(strings.TrimPrefix(line, "Main-Class:"))
return mainClass
return strings.TrimSpace(strings.TrimPrefix(line, "Main-Class:"))
}
}

Expand Down Expand Up @@ -124,9 +178,10 @@ func (j *JavaMainContainer) Finalize() error {
return fmt.Errorf("failed to build classpath: %w", err)
}

// Write CLASSPATH environment variable
if err := j.context.Stager.WriteEnvFile("CLASSPATH", classpath); err != nil {
return fmt.Errorf("failed to write CLASSPATH: %w", err)
profileScript := fmt.Sprintf("export CLASSPATH=\"%s${CLASSPATH:+:$CLASSPATH}\"\n", classpath)

if err := j.context.Stager.WriteProfileD("java_main.sh", profileScript); err != nil {
return fmt.Errorf("failed to write java_main.sh profile.d script: %w", err)
}

// Note: JAVA_OPTS (including JVMKill agent) is configured by the JRE component
Expand All @@ -148,16 +203,16 @@ func (j *JavaMainContainer) buildClasspath() (string, error) {
// Even if it's not a Spring Boot app, we need to include these paths
bootInfClasses := filepath.Join(buildDir, "BOOT-INF", "classes")
if _, err := os.Stat(bootInfClasses); err == nil {
classpathEntries = append(classpathEntries, "BOOT-INF/classes")
classpathEntries = append(classpathEntries, "$HOME/BOOT-INF/classes")
}

bootInfLib := filepath.Join(buildDir, "BOOT-INF", "lib")
if _, err := os.Stat(bootInfLib); err == nil {
classpathEntries = append(classpathEntries, "BOOT-INF/lib/*")
classpathEntries = append(classpathEntries, "$HOME/BOOT-INF/lib/*")
}

// Add all JARs in the build directory
jarFiles, err := filepath.Glob(filepath.Join(buildDir, "*.jar"))
jarFiles, err := filepath.Glob(filepath.Join(buildDir, "$HOME/*.jar"))
if err == nil {
for _, jar := range jarFiles {
classpathEntries = append(classpathEntries, filepath.Base(jar))
Expand All @@ -167,39 +222,30 @@ func (j *JavaMainContainer) buildClasspath() (string, error) {
// Add lib directory if it exists
libDir := filepath.Join(buildDir, "lib")
if _, err := os.Stat(libDir); err == nil {
classpathEntries = append(classpathEntries, "lib/*")
classpathEntries = append(classpathEntries, "$HOME/lib/*")
}

return strings.Join(classpathEntries, ":"), nil
}

// Release returns the Java Main startup command
func (j *JavaMainContainer) Release() (string, error) {
// Determine the main class to run
if j.jarFile != "" {
// JAR has its own Main-Class in the manifest — java -jar handles it
// 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 -jar %s", j.jarFile), nil
}

// Classpath mode: need an explicit main class
mainClass := j.mainClass
if mainClass == "" {
// Try to detect from environment or configuration
mainClass = os.Getenv("JAVA_MAIN_CLASS")
if mainClass == "" {
return "", fmt.Errorf("no main class specified (set JAVA_MAIN_CLASS)")
}
j.context.Log.Debug("Main Class %s found in JAVA_MAIN_CLASS", mainClass)
}

var cmd string
if j.jarFile != "" {
// Run from JAR
// 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", j.jarFile)
} else {
// Build classpath and embed it directly in the command
// (Don't rely on $CLASSPATH environment variable)
classpath, err := j.buildClasspath()
if err != nil {
return "", fmt.Errorf("failed to build classpath: %w", err)
}
// 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 -cp %s %s", classpath, mainClass)
}

return cmd, nil
// 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 ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s", mainClass), nil
}
56 changes: 49 additions & 7 deletions src/java/containers/java_main_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package containers_test

import (
"archive/zip"
"bytes"
"os"
"path/filepath"

Expand All @@ -11,6 +13,23 @@ import (
. "github.com/onsi/gomega"
)

// createJar writes a JAR file at jarPath containing META-INF/MANIFEST.MF with the given content.
func createJar(jarPath, manifestContent string) error {
buf := new(bytes.Buffer)
w := zip.NewWriter(buf)
f, err := w.Create("META-INF/MANIFEST.MF")
if err != nil {
return err
}
if _, err := f.Write([]byte(manifestContent)); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
return os.WriteFile(jarPath, buf.Bytes(), 0644)
}

var _ = Describe("Java Main Container", func() {
var (
ctx *common.Context
Expand Down Expand Up @@ -58,9 +77,12 @@ var _ = Describe("Java Main Container", func() {
})

Describe("Detect", func() {
Context("with JAR file", func() {
Context("with JAR file containing Main-Class manifest", func() {
BeforeEach(func() {
os.WriteFile(filepath.Join(buildDir, "app.jar"), []byte{}, 0644)
Expect(createJar(
filepath.Join(buildDir, "app.jar"),
"Manifest-Version: 1.0\nMain-Class: com.example.Main\n",
)).To(Succeed())
})

It("detects as Java Main", func() {
Expand All @@ -70,6 +92,21 @@ var _ = Describe("Java Main Container", func() {
})
})

Context("with JAR file without Main-Class manifest", func() {
BeforeEach(func() {
Expect(createJar(
filepath.Join(buildDir, "lib.jar"),
"Manifest-Version: 1.0\nCreated-By: test\n",
)).To(Succeed())
})

It("does not detect via JAR alone", func() {
name, err := container.Detect()
Expect(err).NotTo(HaveOccurred())
Expect(name).To(BeEmpty())
})
})

Context("with .class files", func() {
BeforeEach(func() {
os.WriteFile(filepath.Join(buildDir, "Main.class"), []byte{}, 0644)
Expand Down Expand Up @@ -119,11 +156,10 @@ var _ = Describe("Java Main Container", func() {
Describe("Release", func() {
Context("with JAR file", func() {
BeforeEach(func() {
metaInfDir := filepath.Join(buildDir, "META-INF")
os.MkdirAll(metaInfDir, 0755)
manifest := "Manifest-Version: 1.0\nMain-Class: com.example.Main\n"
os.WriteFile(filepath.Join(metaInfDir, "MANIFEST.MF"), []byte(manifest), 0644)
os.WriteFile(filepath.Join(buildDir, "app.jar"), []byte("fake"), 0644)
Expect(createJar(
filepath.Join(buildDir, "app.jar"),
"Manifest-Version: 1.0\nMain-Class: com.example.Main\n",
)).To(Succeed())
container.Detect()
})

Expand All @@ -134,6 +170,12 @@ var _ = Describe("Java Main Container", func() {
Expect(cmd).To(ContainSubstring("-jar"))
Expect(cmd).To(ContainSubstring("app.jar"))
})

It("does not require JAVA_MAIN_CLASS", func() {
cmd, err := container.Release()
Expect(err).NotTo(HaveOccurred())
Expect(cmd).NotTo(ContainSubstring("JAVA_MAIN_CLASS"))
})
})

Context("with JAVA_MAIN_CLASS env variable", func() {
Expand Down