11package containers
22
33import (
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.
6065func (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
83122func (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
177232func (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}
0 commit comments