Skip to content

Commit ed3ddf1

Browse files
Fix issues in Java Main container (#1234)
* Fix java main to read main class from JAR * Adjust classpath,add tests * Adjust profile script name * Fix path * Revert "Fix path" This reverts commit 103b123. * Prefix entries * Fix prefix entries * Fix always missing jarFile in release phase * Debug log * Corrections
1 parent 9ce86e2 commit ed3ddf1

2 files changed

Lines changed: 132 additions & 44 deletions

File tree

src/java/containers/java_main.go

Lines changed: 83 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package containers
22

33
import (
4-
"github.com/cloudfoundry/java-buildpack/src/java/common"
4+
"archive/zip"
55
"fmt"
6+
"io"
67
"os"
78
"path/filepath"
89
"strings"
10+
11+
"github.com/cloudfoundry/java-buildpack/src/java/common"
912
)
1013

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

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

7176
name := entry.Name()
72-
if strings.HasSuffix(name, ".jar") {
73-
// TODO: In full implementation, extract and read MANIFEST.MF
74-
// For now, assume any JAR could be a main JAR
75-
return "Main", filepath.Join("$HOME", name)
77+
if !strings.HasSuffix(name, ".jar") {
78+
continue
79+
}
80+
81+
jarPath := filepath.Join(buildDir, name)
82+
if mainClass := readMainClassFromJar(jarPath); mainClass != "" {
83+
return mainClass, filepath.Join("$HOME", name)
7684
}
7785
}
7886

7987
return "", ""
8088
}
8189

90+
// readMainClassFromJar opens a JAR (zip) file and reads the Main-Class
91+
// attribute from META-INF/MANIFEST.MF, returning "" if not present or on error.
92+
func readMainClassFromJar(jarPath string) string {
93+
r, err := zip.OpenReader(jarPath)
94+
if err != nil {
95+
return ""
96+
}
97+
defer r.Close()
98+
99+
for _, f := range r.File {
100+
if f.Name != "META-INF/MANIFEST.MF" {
101+
continue
102+
}
103+
104+
rc, err := f.Open()
105+
if err != nil {
106+
return ""
107+
}
108+
109+
data, err := io.ReadAll(rc)
110+
rc.Close()
111+
if err != nil {
112+
return ""
113+
}
114+
115+
return parseMainClass(string(data))
116+
}
117+
118+
return ""
119+
}
120+
82121
// readMainClassFromManifest reads the Main-Class from a manifest file
83122
func (j *JavaMainContainer) readMainClassFromManifest(manifestPath string) string {
84123
data, err := os.ReadFile(manifestPath)
85124
if err != nil {
86125
return ""
87126
}
88127

89-
// Parse MANIFEST.MF file (simple line-by-line parsing)
90-
lines := strings.Split(string(data), "\n")
91-
for _, line := range lines {
128+
return parseMainClass(string(data))
129+
}
130+
131+
// parseMainClass extracts the Main-Class value from MANIFEST.MF content.
132+
// Handles line continuations (lines starting with a space are folded onto the previous line).
133+
func parseMainClass(content string) string {
134+
// Unfold continuation lines (space at start of line means continuation)
135+
content = strings.ReplaceAll(content, "\r\n", "\n")
136+
var unfolded strings.Builder
137+
for _, line := range strings.Split(content, "\n") {
138+
if strings.HasPrefix(line, " ") {
139+
unfolded.WriteString(strings.TrimPrefix(line, " "))
140+
} else {
141+
unfolded.WriteString("\n")
142+
unfolded.WriteString(line)
143+
}
144+
}
145+
146+
for _, line := range strings.Split(unfolded.String(), "\n") {
92147
line = strings.TrimSpace(line)
93148
if strings.HasPrefix(line, "Main-Class:") {
94-
mainClass := strings.TrimSpace(strings.TrimPrefix(line, "Main-Class:"))
95-
return mainClass
149+
return strings.TrimSpace(strings.TrimPrefix(line, "Main-Class:"))
96150
}
97151
}
98152

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

127-
// Write CLASSPATH environment variable
128-
if err := j.context.Stager.WriteEnvFile("CLASSPATH", classpath); err != nil {
129-
return fmt.Errorf("failed to write CLASSPATH: %w", err)
181+
profileScript := fmt.Sprintf("export CLASSPATH=\"%s${CLASSPATH:+:$CLASSPATH}\"\n", classpath)
182+
183+
if err := j.context.Stager.WriteProfileD("java_main.sh", profileScript); err != nil {
184+
return fmt.Errorf("failed to write java_main.sh profile.d script: %w", err)
130185
}
131186

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

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

159214
// Add all JARs in the build directory
160-
jarFiles, err := filepath.Glob(filepath.Join(buildDir, "*.jar"))
215+
jarFiles, err := filepath.Glob(filepath.Join(buildDir, "$HOME/*.jar"))
161216
if err == nil {
162217
for _, jar := range jarFiles {
163218
classpathEntries = append(classpathEntries, filepath.Base(jar))
@@ -167,39 +222,30 @@ func (j *JavaMainContainer) buildClasspath() (string, error) {
167222
// Add lib directory if it exists
168223
libDir := filepath.Join(buildDir, "lib")
169224
if _, err := os.Stat(libDir); err == nil {
170-
classpathEntries = append(classpathEntries, "lib/*")
225+
classpathEntries = append(classpathEntries, "$HOME/lib/*")
171226
}
172227

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

176231
// Release returns the Java Main startup command
177232
func (j *JavaMainContainer) Release() (string, error) {
178-
// Determine the main class to run
233+
if j.jarFile != "" {
234+
// JAR has its own Main-Class in the manifest — java -jar handles it
235+
// Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity)
236+
return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -jar %s", j.jarFile), nil
237+
}
238+
239+
// Classpath mode: need an explicit main class
179240
mainClass := j.mainClass
180241
if mainClass == "" {
181-
// Try to detect from environment or configuration
182242
mainClass = os.Getenv("JAVA_MAIN_CLASS")
183243
if mainClass == "" {
184244
return "", fmt.Errorf("no main class specified (set JAVA_MAIN_CLASS)")
185245
}
246+
j.context.Log.Debug("Main Class %s found in JAVA_MAIN_CLASS", mainClass)
186247
}
187248

188-
var cmd string
189-
if j.jarFile != "" {
190-
// Run from JAR
191-
// Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity)
192-
cmd = fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -jar %s", j.jarFile)
193-
} else {
194-
// Build classpath and embed it directly in the command
195-
// (Don't rely on $CLASSPATH environment variable)
196-
classpath, err := j.buildClasspath()
197-
if err != nil {
198-
return "", fmt.Errorf("failed to build classpath: %w", err)
199-
}
200-
// Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity)
201-
cmd = fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp %s %s", classpath, mainClass)
202-
}
203-
204-
return cmd, nil
249+
// Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity)
250+
return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s", mainClass), nil
205251
}

src/java/containers/java_main_test.go

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package containers_test
22

33
import (
4+
"archive/zip"
5+
"bytes"
46
"os"
57
"path/filepath"
68

@@ -11,6 +13,23 @@ import (
1113
. "github.com/onsi/gomega"
1214
)
1315

16+
// createJar writes a JAR file at jarPath containing META-INF/MANIFEST.MF with the given content.
17+
func createJar(jarPath, manifestContent string) error {
18+
buf := new(bytes.Buffer)
19+
w := zip.NewWriter(buf)
20+
f, err := w.Create("META-INF/MANIFEST.MF")
21+
if err != nil {
22+
return err
23+
}
24+
if _, err := f.Write([]byte(manifestContent)); err != nil {
25+
return err
26+
}
27+
if err := w.Close(); err != nil {
28+
return err
29+
}
30+
return os.WriteFile(jarPath, buf.Bytes(), 0644)
31+
}
32+
1433
var _ = Describe("Java Main Container", func() {
1534
var (
1635
ctx *common.Context
@@ -58,9 +77,12 @@ var _ = Describe("Java Main Container", func() {
5877
})
5978

6079
Describe("Detect", func() {
61-
Context("with JAR file", func() {
80+
Context("with JAR file containing Main-Class manifest", func() {
6281
BeforeEach(func() {
63-
os.WriteFile(filepath.Join(buildDir, "app.jar"), []byte{}, 0644)
82+
Expect(createJar(
83+
filepath.Join(buildDir, "app.jar"),
84+
"Manifest-Version: 1.0\nMain-Class: com.example.Main\n",
85+
)).To(Succeed())
6486
})
6587

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

95+
Context("with JAR file without Main-Class manifest", func() {
96+
BeforeEach(func() {
97+
Expect(createJar(
98+
filepath.Join(buildDir, "lib.jar"),
99+
"Manifest-Version: 1.0\nCreated-By: test\n",
100+
)).To(Succeed())
101+
})
102+
103+
It("does not detect via JAR alone", func() {
104+
name, err := container.Detect()
105+
Expect(err).NotTo(HaveOccurred())
106+
Expect(name).To(BeEmpty())
107+
})
108+
})
109+
73110
Context("with .class files", func() {
74111
BeforeEach(func() {
75112
os.WriteFile(filepath.Join(buildDir, "Main.class"), []byte{}, 0644)
@@ -119,11 +156,10 @@ var _ = Describe("Java Main Container", func() {
119156
Describe("Release", func() {
120157
Context("with JAR file", func() {
121158
BeforeEach(func() {
122-
metaInfDir := filepath.Join(buildDir, "META-INF")
123-
os.MkdirAll(metaInfDir, 0755)
124-
manifest := "Manifest-Version: 1.0\nMain-Class: com.example.Main\n"
125-
os.WriteFile(filepath.Join(metaInfDir, "MANIFEST.MF"), []byte(manifest), 0644)
126-
os.WriteFile(filepath.Join(buildDir, "app.jar"), []byte("fake"), 0644)
159+
Expect(createJar(
160+
filepath.Join(buildDir, "app.jar"),
161+
"Manifest-Version: 1.0\nMain-Class: com.example.Main\n",
162+
)).To(Succeed())
127163
container.Detect()
128164
})
129165

@@ -134,6 +170,12 @@ var _ = Describe("Java Main Container", func() {
134170
Expect(cmd).To(ContainSubstring("-jar"))
135171
Expect(cmd).To(ContainSubstring("app.jar"))
136172
})
173+
174+
It("does not require JAVA_MAIN_CLASS", func() {
175+
cmd, err := container.Release()
176+
Expect(err).NotTo(HaveOccurred())
177+
Expect(cmd).NotTo(ContainSubstring("JAVA_MAIN_CLASS"))
178+
})
137179
})
138180

139181
Context("with JAVA_MAIN_CLASS env variable", func() {

0 commit comments

Comments
 (0)