-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Expand file tree
/
Copy pathplay.go
More file actions
557 lines (475 loc) · 16.9 KB
/
Copy pathplay.go
File metadata and controls
557 lines (475 loc) · 16.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
package containers
import (
"fmt"
"github.com/cloudfoundry/java-buildpack/src/java/common"
"os"
"path/filepath"
"regexp"
"strings"
)
// PlayContainer represents a Play Framework application container
type PlayContainer struct {
context *common.Context
playType string // "pre22_dist", "pre22_staged", "post22_dist", "post22_staged"
playVersion string
startScript string
libDir string
}
// NewPlayContainer creates a new Play Framework container
func NewPlayContainer(ctx *common.Context) *PlayContainer {
return &PlayContainer{
context: ctx,
}
}
// Detect checks if this is a Play Framework application
func (p *PlayContainer) Detect() (string, error) {
buildDir := p.context.Stager.BuildDir()
p.context.Log.Debug("Play: Checking buildDir: %s", buildDir)
// Run each detector once, collecting matches. Using a temporary container per
// detector avoids mutating p until we know exactly one type matched.
type candidate struct {
name string
c *PlayContainer
}
var matches []candidate
detectors := []struct {
name string
fn func(*PlayContainer, string) bool
}{
{"Pre22Staged", (*PlayContainer).detectPre22Staged},
{"Post22Staged", (*PlayContainer).detectPost22Staged},
{"Post22Dist", (*PlayContainer).detectPost22Dist},
{"Pre22Dist", (*PlayContainer).detectPre22Dist},
}
for _, d := range detectors {
p.context.Log.Debug("Play: Trying %s detection", d.name)
tmp := &PlayContainer{context: p.context}
if d.fn(tmp, buildDir) {
p.context.Log.Debug("Play: %s matched (version %s)", d.name, tmp.playVersion)
matches = append(matches, candidate{d.name, tmp})
}
}
if len(matches) > 1 {
names := make([]string, len(matches))
for i, m := range matches {
names[i] = m.name
}
return "", fmt.Errorf("Play Framework application version cannot be determined: %v", names)
}
if len(matches) == 1 {
m := matches[0]
p.playType = m.c.playType
p.playVersion = m.c.playVersion
p.startScript = m.c.startScript
p.libDir = m.c.libDir
p.context.Log.Info("Play: Detected %s - version %s", m.name, p.playVersion)
return "Play", nil
}
p.context.Log.Debug("Play: No Play Framework detected")
return "", nil
}
// Validate checks for ambiguous Play configurations without mutating the receiver.
func (p *PlayContainer) Validate() error {
_, err := p.Detect()
return err
}
// detectPost22Dist detects Play 2.2+ distributed applications
// Structure: application-root/bin/<script>, application-root/lib/com.typesafe.play.play_*.jar
func (p *PlayContainer) detectPost22Dist(buildDir string) bool {
// Check for application-root/bin/ directory
binDir := filepath.Join(buildDir, "application-root", "bin")
binStat, binErr := os.Stat(binDir)
if binErr != nil || !binStat.IsDir() {
return false
}
// Check for application-root/lib/ directory
libDir := filepath.Join(buildDir, "application-root", "lib")
libStat, libErr := os.Stat(libDir)
if libErr != nil || !libStat.IsDir() {
return false
}
// Find Play JAR in lib/ (com.typesafe.play.play_*.jar)
playJar, version := p.findPlayJar(libDir)
if playJar == "" {
return false
}
// Parse version - must be 2.2 or higher
if !p.isPost22Version(version) {
return false
}
// Find start script in bin/ (non-.bat file)
startScript := p.findStartScript(binDir)
if startScript == "" {
return false
}
p.playType = "post22_dist"
p.playVersion = version
p.startScript = filepath.Join("application-root", "bin", startScript)
p.libDir = libDir
p.context.Log.Debug("Detected Play Framework %s (Post22Dist)", version)
return true
}
// detectPost22Staged detects Play 2.2+ staged applications
// Structure: lib/com.typesafe.play.play_*.jar (may or may not have bin/ with script)
func (p *PlayContainer) detectPost22Staged(buildDir string) bool {
// Check for lib/ directory at root
libDir := filepath.Join(buildDir, "lib")
libStat, libErr := os.Stat(libDir)
if libErr != nil || !libStat.IsDir() {
return false
}
// Find Play JAR in lib/
playJar, version := p.findPlayJar(libDir)
if playJar == "" {
return false
}
// Parse version - must be 2.2 or higher
if !p.isPost22Version(version) {
return false
}
// Check for bin/ directory at root (optional)
binDir := filepath.Join(buildDir, "bin")
binStat, binErr := os.Stat(binDir)
if binErr == nil && binStat.IsDir() {
// Try to find start script in bin/
startScript := p.findStartScript(binDir)
if startScript != "" {
p.startScript = filepath.Join("bin", startScript)
} else {
p.startScript = "" // No start script, will need to use java command
}
} else {
p.startScript = "" // No bin/ directory, will need to use java command
}
p.playType = "post22_staged"
p.playVersion = version
p.libDir = libDir
p.context.Log.Debug("Detected Play Framework %s (Post22Staged)", version)
return true
}
// detectPre22Dist detects Play 2.0-2.1 distributed applications
// Structure: application-root/start, application-root/lib/play_*.jar
func (p *PlayContainer) detectPre22Dist(buildDir string) bool {
// Check for application-root/ directory
appRoot := filepath.Join(buildDir, "application-root")
appRootStat, err := os.Stat(appRoot)
if err != nil || !appRootStat.IsDir() {
return false
}
// Check for start script
startScript := filepath.Join(appRoot, "start")
if _, err := os.Stat(startScript); err != nil {
return false
}
// Check for lib/ directory
libDir := filepath.Join(appRoot, "lib")
libStat, libErr := os.Stat(libDir)
if libErr != nil || !libStat.IsDir() {
return false
}
// Find Play JAR (play.play_*.jar or play_*.jar)
playJar, version := p.findPlayJar(libDir)
if playJar == "" {
return false
}
// Version should be 2.0 or 2.1
if p.isPost22Version(version) {
return false
}
p.playType = "pre22_dist"
p.playVersion = version
p.startScript = filepath.Join("application-root", "start")
p.libDir = libDir
p.context.Log.Debug("Detected Play Framework %s (Pre22Dist)", version)
return true
}
// detectPre22Staged detects Play 2.0-2.1 staged applications
// Structure: staged/play_*.jar (may or may not have start script)
func (p *PlayContainer) detectPre22Staged(buildDir string) bool {
// Check for staged/ directory
stagedDir := filepath.Join(buildDir, "staged")
p.context.Log.Debug("Play Pre22Staged: Checking for staged dir: %s", stagedDir)
stagedStat, err := os.Stat(stagedDir)
if err != nil || !stagedStat.IsDir() {
p.context.Log.Debug("Play Pre22Staged: Staged dir not found or not a directory: %v", err)
return false
}
p.context.Log.Debug("Play Pre22Staged: Staged dir found")
// Find Play JAR in staged/
playJar, version := p.findPlayJar(stagedDir)
p.context.Log.Debug("Play Pre22Staged: findPlayJar returned jar=%s, version=%s", playJar, version)
if playJar == "" {
p.context.Log.Debug("Play Pre22Staged: No Play JAR found")
return false
}
// Version should be 2.0 or 2.1
if p.isPost22Version(version) {
p.context.Log.Debug("Play Pre22Staged: Version %s is Post22, not Pre22", version)
return false
}
// Check if there's a start script (optional)
startScript := filepath.Join(buildDir, "start")
p.context.Log.Debug("Play Pre22Staged: Checking for start script: %s", startScript)
if _, err := os.Stat(startScript); err == nil {
p.context.Log.Debug("Play Pre22Staged: Start script found")
p.startScript = "start"
} else {
p.context.Log.Debug("Play Pre22Staged: Start script not found, will use java command")
p.startScript = "" // No start script, will need to use java command
}
p.playType = "pre22_staged"
p.playVersion = version
p.libDir = stagedDir
p.context.Log.Debug("Detected Play Framework %s (Pre22Staged)", version)
return true
}
// findPlayJar finds the Play Framework JAR and extracts version
// Returns jar filename and version string
func (p *PlayContainer) findPlayJar(libDir string) (string, string) {
entries, err := os.ReadDir(libDir)
if err != nil {
return "", ""
}
// Match patterns:
// - com.typesafe.play.play_2.10-2.2.0.jar (Play 2.2+)
// - play.play_2.9.1-2.0.jar (Play 2.0)
// - play_2.10-2.1.4.jar (Play 2.1)
playJarPattern := regexp.MustCompile(`^(?:com\.typesafe\.)?play(?:\.play)?_.*-(.+)\.jar$`)
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if matches := playJarPattern.FindStringSubmatch(name); matches != nil {
version := matches[1]
p.context.Log.Debug("Found Play JAR: %s (version: %s)", name, version)
return name, version
}
}
return "", ""
}
// findStartScript finds a non-.bat startup script in the given directory
func (p *PlayContainer) findStartScript(binDir string) string {
entries, err := os.ReadDir(binDir)
if err != nil {
return ""
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
// Skip .bat files
if filepath.Ext(name) != ".bat" {
return name
}
}
return ""
}
// isPost22Version checks if version is 2.2 or higher
func (p *PlayContainer) isPost22Version(version string) bool {
// Parse major.minor version
parts := strings.Split(version, ".")
if len(parts) < 2 {
return false
}
major := parts[0]
minor := parts[1]
// Check for 2.2+
if major == "2" {
// Extract numeric minor version
minorInt := 0
fmt.Sscanf(minor, "%d", &minorInt)
return minorInt >= 2
}
// Version 3+ would also be post-2.2
majorInt := 0
fmt.Sscanf(major, "%d", &majorInt)
return majorInt > 2
}
// Supply installs and configures the Play Framework application
func (p *PlayContainer) Supply() error {
p.context.Log.BeginStep("Installing Play Framework %s (%s)", p.playVersion, p.playType)
// Make start script executable
if err := p.makeStartScriptExecutable(); err != nil {
return fmt.Errorf("failed to make start script executable: %w", err)
}
p.context.Log.Info("Play Framework %s installation complete", p.playVersion)
return nil
}
// makeStartScriptExecutable ensures the start script has execute permissions
func (p *PlayContainer) makeStartScriptExecutable() error {
buildDir := p.context.Stager.BuildDir()
scriptPath := filepath.Join(buildDir, p.startScript)
if err := os.Chmod(scriptPath, 0755); err != nil {
p.context.Log.Warning("Could not make %s executable: %s", p.startScript, err.Error())
return err
}
p.context.Log.Debug("Made %s executable", p.startScript)
return nil
}
// Finalize performs final configuration for the Play Framework application
func (p *PlayContainer) Finalize() error {
p.context.Log.BeginStep("Finalizing Play Framework %s", p.playVersion)
// Collect additional libraries (JVMKill agent, frameworks, etc.)
additionalLibs := p.collectAdditionalLibraries()
p.context.Log.Info("Found %d additional libraries for CLASSPATH", len(additionalLibs))
// Build CLASSPATH from additional libraries
// Convert staging paths to runtime paths
classpathParts := p.buildRuntimeClasspath(additionalLibs)
// Determine the script directory based on Play type
var scriptDir string
switch p.playType {
case "post22_dist":
scriptDir = "application-root/bin"
case "post22_staged":
scriptDir = "bin"
case "pre22_dist":
scriptDir = "application-root"
case "pre22_staged":
scriptDir = "."
default:
scriptDir = "bin"
}
// Write profile.d script that sets up environment variables
// This follows the immutable BuildDir pattern: configure via environment, don't modify files
envContent := fmt.Sprintf(`export DEPS_DIR=${DEPS_DIR:-/home/vcap/deps}
export PLAY_HOME=$HOME
export PLAY_BIN=$HOME/%s
export PATH=$PLAY_BIN:$PATH
# Prepend additional libraries to CLASSPATH
# Play start scripts respect CLASSPATH environment variable
# This includes JVMKill agent, framework JARs, JDBC drivers, etc.
`, scriptDir)
// Add CLASSPATH if we have additional libraries
if len(classpathParts) > 0 {
classpathValue := strings.Join(classpathParts, ":")
envContent += fmt.Sprintf("export CLASSPATH=\"%s:${CLASSPATH:-}\"\n", classpathValue)
p.context.Log.Info("Configured CLASSPATH with %d additional libraries", len(classpathParts))
}
if err := p.context.Stager.WriteProfileD("play.sh", envContent); err != nil {
p.context.Log.Warning("Could not write play.sh profile.d script: %s", err.Error())
} else {
p.context.Log.Debug("Created profile.d script: play.sh")
}
// Configure JAVA_OPTS to be picked up by Play startup scripts
// Play uses -Dhttp.port system property to configure the HTTP port
// Note: JVMKill agent is configured by the JRE component via .profile.d/java_opts.sh
javaOpts := []string{
"-Dhttp.port=$PORT",
"-Djava.io.tmpdir=$TMPDIR",
"-XX:+ExitOnOutOfMemoryError",
}
// Play start scripts respect JAVA_OPTS environment variable
javaOptsScript := fmt.Sprintf("export JAVA_OPTS=\"%s\"\n", strings.Join(javaOpts, " "))
if err := p.context.Stager.WriteProfileD("play_java_opts.sh", javaOptsScript); err != nil {
return fmt.Errorf("failed to write JAVA_OPTS profile.d script: %w", err)
}
p.context.Log.Info("Play Framework finalization complete (using environment variables, not modifying scripts)")
return nil
}
// collectAdditionalLibraries gathers all additional libraries that should be added to CLASSPATH
// This includes framework-provided JAR libraries installed during supply phase by any buildpack.
func (p *PlayContainer) collectAdditionalLibraries() []string {
var libs []string
// DepDir() returns e.g. /tmp/deps/0 — the current buildpack's slot.
// The parent directory contains all supply buildpack slots (0, 1, 2, …).
allDepsDir := filepath.Dir(p.context.Stager.DepDir())
// Scan $DEPS_DIR/ for all index slots
slots, err := os.ReadDir(allDepsDir)
if err != nil {
p.context.Log.Debug("Unable to read deps directory: %s", err.Error())
return libs
}
for _, slot := range slots {
if !slot.IsDir() {
continue
}
slotDir := filepath.Join(allDepsDir, slot.Name())
// Each slot contains framework subdirectories installed by that buildpack
frameworks, err := os.ReadDir(slotDir)
if err != nil {
p.context.Log.Debug("Unable to read slot directory %s: %s", slotDir, err.Error())
continue
}
for _, fw := range frameworks {
if !fw.IsDir() {
continue
}
frameworkDir := filepath.Join(slotDir, fw.Name())
// Find all *.jar files directly in this framework directory
jarPattern := filepath.Join(frameworkDir, "*.jar")
matches, err := filepath.Glob(jarPattern)
if err != nil {
p.context.Log.Debug("Error globbing JARs in %s: %s", frameworkDir, err.Error())
continue
}
libs = append(libs, matches...)
}
}
return libs
}
// buildRuntimeClasspath converts staging-time library paths to runtime paths.
// At staging time, libraries are in $DEPS_DIR/<idx>/<framework>/*.jar
// At runtime, they'll be in /home/vcap/deps/<idx>/<framework>/*.jar
func (p *PlayContainer) buildRuntimeClasspath(libs []string) []string {
var classpathParts []string
allDepsDir := filepath.Dir(p.context.Stager.DepDir())
buildDir := p.context.Stager.BuildDir()
for _, lib := range libs {
var runtimePath string
if strings.HasPrefix(lib, allDepsDir) {
// e.g. /tmp/deps/1/new_relic_agent/newrelic.jar
// → relPath = 1/new_relic_agent/newrelic.jar
// → $DEPS_DIR/1/new_relic_agent/newrelic.jar
relPath, err := filepath.Rel(allDepsDir, lib)
if err != nil {
p.context.Log.Warning("Could not calculate relative path for %s: %s", lib, err.Error())
continue
}
runtimePath = fmt.Sprintf("$DEPS_DIR/%s", filepath.ToSlash(relPath))
} else if strings.HasPrefix(lib, buildDir) {
relPath, err := filepath.Rel(buildDir, lib)
if err != nil {
p.context.Log.Warning("Could not calculate relative path for %s: %s", lib, err.Error())
continue
}
runtimePath = fmt.Sprintf("$HOME/%s", filepath.ToSlash(relPath))
} else {
p.context.Log.Warning("Library path %s doesn't match deps or build directory, using as-is", lib)
runtimePath = lib
}
classpathParts = append(classpathParts, runtimePath)
}
return classpathParts
}
// Release returns the command to start the Play Framework application
func (p *PlayContainer) Release() (string, error) {
// Check if Detect() was called successfully
if p.playType == "" {
return "", fmt.Errorf("no Play application detected, Detect() must be called first")
}
// Play Framework start command varies by type
var cmd string
// If we have a start script, use it
if p.startScript != "" {
// Use absolute path with $HOME prefix to ensure the script can be found at runtime
// Cloud Foundry sets $HOME to the application root directory
cmd = fmt.Sprintf("$HOME/%s", p.startScript)
} else {
// No start script - use java command with NettyServer
// This is for staged apps without start scripts
libPath := filepath.ToSlash(p.libDir)
// For staged apps, libDir is relative to buildDir, convert to $HOME
if p.playType == "pre22_staged" || p.playType == "post22_staged" {
relPath, err := filepath.Rel(p.context.Stager.BuildDir(), p.libDir)
if err == nil {
libPath = filepath.ToSlash(relPath)
}
}
// Use quoted eval argument to prevent glob/word-splitting of $JAVA_OPTS before eval.
cmd = fmt.Sprintf(`eval "exec java $JAVA_OPTS -cp $HOME/%s/* play.core.server.NettyServer $HOME"`, libPath)
}
p.context.Log.Debug("Play Framework release command: %s", cmd)
return cmd, nil
}