-
Notifications
You must be signed in to change notification settings - Fork 347
Expand file tree
/
Copy pathoptions.go
More file actions
347 lines (304 loc) · 12.1 KB
/
options.go
File metadata and controls
347 lines (304 loc) · 12.1 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
package options
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/cloudfoundry/libbuildpack"
"github.com/cloudfoundry/php-buildpack/src/php/config"
"github.com/cloudfoundry/php-buildpack/src/php/util"
)
// Manifest interface abstracts the buildpack manifest operations needed for options
type Manifest interface {
AllDependencyVersions(depName string) []string
DefaultVersion(depName string) (libbuildpack.Dependency, error)
}
// Options represents the merged buildpack configuration from defaults/options.json and .bp-config/options.json
type Options struct {
Stack string `json:"STACK"`
LibDir string `json:"LIBDIR"` // Library directory (default: "lib")
WebDir string `json:"WEBDIR"` // Web root directory (default: "htdocs")
WebServer string `json:"WEB_SERVER"` // Web server: "httpd", "nginx", or "none"
PHPVM string `json:"PHP_VM"` // PHP VM type (default: "php")
PHPVersion string `json:"PHP_VERSION,omitempty"` // Specific PHP version to install
PHPDefault string `json:"PHP_DEFAULT,omitempty"` // Default PHP version from manifest
AdminEmail string `json:"ADMIN_EMAIL"` // Admin email for server config (used by httpd)
// STRIP flags control whether to strip the top-level directory when extracting archives.
// These are internal flags used during dependency installation and rarely need to be changed.
// The defaults (false for main packages, true for modules) work for standard buildpack usage.
HTTPDStrip bool `json:"HTTPD_STRIP"` // Strip top dir when extracting httpd (default: false)
HTTPDModulesStrip bool `json:"HTTPD_MODULES_STRIP"` // Strip top dir for httpd modules (default: true)
NginxStrip bool `json:"NGINX_STRIP"` // Strip top dir when extracting nginx (default: false)
PHPStrip bool `json:"PHP_STRIP"` // Strip top dir when extracting php (default: false)
PHPModulesStrip bool `json:"PHP_MODULES_STRIP"` // Strip top dir for php modules (default: true)
PHPModules []string `json:"PHP_MODULES"` // PHP modules to load
PHPExtensions []string `json:"PHP_EXTENSIONS"` // PHP extensions to enable
ZendExtensions []string `json:"ZEND_EXTENSIONS"` // Zend extensions to enable
ComposerVendorDir string `json:"COMPOSER_VENDOR_DIR,omitempty"` // Custom composer vendor directory
ComposerInstallOptions []string `json:"COMPOSER_INSTALL_OPTIONS,omitempty"` // Additional composer install options
// Additional preprocess commands to run before app starts
// Supports: string, []string, or [][]string formats (v4.x compatibility)
AdditionalPreprocessCmds interface{} `json:"ADDITIONAL_PREPROCESS_CMDS,omitempty"`
// Custom start command for standalone PHP applications (when WEB_SERVER=none)
// If not set, auto-detects: app.php, main.php, run.php, start.php
AppStartCmd string `json:"APP_START_CMD,omitempty"`
// Internal flags
OptionsJSONHasPHPExtensions bool `json:"OPTIONS_JSON_HAS_PHP_EXTENSIONS,omitempty"`
// Dynamic PHP version tracking (e.g., PHP_81_LATEST, PHP_82_LATEST)
PHPVersions map[string]string `json:"-"`
}
// LoadOptions loads and merges options from defaults/options.json and .bp-config/options.json
func LoadOptions(bpDir, buildDir string, manifest Manifest, logger *libbuildpack.Logger) (*Options, error) {
opts := &Options{
PHPVersions: make(map[string]string),
}
// Load default options from embedded defaults/options.json
logger.Debug("Loading default options from embedded config")
data, err := config.GetOptionsJSON()
if err != nil {
return nil, fmt.Errorf("failed to load default options: %w", err)
}
if err := json.Unmarshal(data, opts); err != nil {
return nil, fmt.Errorf("invalid default options.json: %w", err)
}
// Get PHP default version from manifest
defaultVersions := manifest.AllDependencyVersions("php")
if len(defaultVersions) > 0 {
// Find the default version from manifest
if dep, err := manifest.DefaultVersion("php"); err == nil {
opts.PHPDefault = dep.Version
logger.Debug("Set PHP_DEFAULT = %s from manifest", dep.Version)
}
}
// Build PHP version map (e.g., PHP_81_LATEST, PHP_82_LATEST)
phpVersions := manifest.AllDependencyVersions("php")
versionsByLine := make(map[string][]string)
for _, version := range phpVersions {
parts := strings.Split(version, ".")
if len(parts) >= 2 {
// Create key like "PHP_81_LATEST" for PHP 8.1.x
key := fmt.Sprintf("PHP_%s%s_LATEST", parts[0], parts[1])
versionsByLine[key] = append(versionsByLine[key], version)
}
}
// Sort and find highest patch version for each line
for key, versions := range versionsByLine {
if len(versions) > 0 {
// Sort versions and take the last (highest)
sortVersions(versions)
highest := versions[len(versions)-1]
opts.PHPVersions[key] = highest
logger.Debug("Set %s = %s", key, highest)
}
}
// Load user options from .bp-config/options.json (if exists)
userOptsPath := filepath.Join(buildDir, ".bp-config", "options.json")
if exists, err := libbuildpack.FileExists(userOptsPath); err != nil {
return nil, fmt.Errorf("failed to check for user options: %w", err)
} else if exists {
logger.Info("Loading user configuration from .bp-config/options.json")
userOpts := &Options{}
if err := loadJSONFile(userOptsPath, userOpts, logger); err != nil {
// Print the file contents on error for debugging
if content, readErr := os.ReadFile(userOptsPath); readErr == nil {
logger.Error("Invalid JSON in %s:\n%s", userOptsPath, string(content))
}
return nil, fmt.Errorf("failed to load user options: %w", err)
}
// Merge user options into default options
opts.mergeUserOptions(userOpts)
// Set flag if user specified PHP extensions
if len(userOpts.PHPExtensions) > 0 {
opts.OptionsJSONHasPHPExtensions = true
fmt.Println("Warning: PHP_EXTENSIONS in options.json is deprecated. See: http://docs.cloudfoundry.org/buildpacks/php/gsg-php-config.html")
}
}
// Validate required fields
if err := opts.validate(); err != nil {
return nil, err
}
return opts, nil
}
// loadJSONFile loads a JSON file into the target structure
func loadJSONFile(path string, target interface{}, logger *libbuildpack.Logger) error {
logger.Debug("Loading config from %s", path)
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("config file not found: %s", path)
}
return err
}
if err := json.Unmarshal(data, target); err != nil {
return fmt.Errorf("invalid JSON in %s: %w", path, err)
}
return nil
}
// mergeUserOptions merges user-provided options into the default options
// User options override defaults, but only for fields that are explicitly set
func (o *Options) mergeUserOptions(user *Options) {
if user.Stack != "" {
o.Stack = user.Stack
}
if user.LibDir != "" {
o.LibDir = user.LibDir
}
if user.WebDir != "" {
o.WebDir = user.WebDir
}
if user.WebServer != "" {
o.WebServer = user.WebServer
}
if user.PHPVM != "" {
o.PHPVM = user.PHPVM
}
if user.PHPVersion != "" {
o.PHPVersion = user.PHPVersion
}
if user.AdminEmail != "" {
o.AdminEmail = user.AdminEmail
}
if user.ComposerVendorDir != "" {
o.ComposerVendorDir = user.ComposerVendorDir
}
// Merge arrays - user values replace defaults
if len(user.PHPModules) > 0 {
o.PHPModules = user.PHPModules
}
if len(user.PHPExtensions) > 0 {
o.PHPExtensions = user.PHPExtensions
}
if len(user.ZendExtensions) > 0 {
o.ZendExtensions = user.ZendExtensions
}
if len(user.ComposerInstallOptions) > 0 {
o.ComposerInstallOptions = user.ComposerInstallOptions
}
// Merge ADDITIONAL_PREPROCESS_CMDS if user specified
if user.AdditionalPreprocessCmds != nil {
o.AdditionalPreprocessCmds = user.AdditionalPreprocessCmds
}
// Merge APP_START_CMD if user specified
if user.AppStartCmd != "" {
o.AppStartCmd = user.AppStartCmd
}
// Note: Boolean fields are not merged because we can't distinguish between
// false (user set) and false (default zero value). If needed, use pointers.
}
// validate checks that required options are set and valid
func (o *Options) validate() error {
// Check web server is valid
if o.WebServer != "httpd" && o.WebServer != "nginx" && o.WebServer != "none" {
return fmt.Errorf("invalid WEB_SERVER: %s (must be 'httpd', 'nginx', or 'none')", o.WebServer)
}
// Other validations can be added here
return nil
}
// GetPHPVersion returns the PHP version to use, either from user config or default
// Resolves placeholders like {PHP_83_LATEST} to actual versions
func (o *Options) GetPHPVersion() string {
if o.PHPVersion != "" {
// Check if it's a placeholder like {PHP_83_LATEST}
if strings.HasPrefix(o.PHPVersion, "{") && strings.HasSuffix(o.PHPVersion, "}") {
// Extract the placeholder name (remove { and })
placeholderName := strings.TrimPrefix(strings.TrimSuffix(o.PHPVersion, "}"), "{")
// Look up the actual version from PHPVersions map
if actualVersion, exists := o.PHPVersions[placeholderName]; exists {
return actualVersion
}
// If placeholder not found, return as-is (will fail with clear error message)
// This allows the buildpack to show which placeholder was invalid
}
return o.PHPVersion
}
return o.PHPDefault
}
// sortVersions sorts semantic versions in ascending order
func sortVersions(versions []string) {
// Simple bubble sort for semantic versions
for i := 0; i < len(versions); i++ {
for j := i + 1; j < len(versions); j++ {
if util.CompareVersions(versions[i], versions[j]) > 0 {
versions[i], versions[j] = versions[j], versions[i]
}
}
}
}
// GetPreprocessCommands returns the list of preprocess commands to run before app starts.
// Supports multiple formats for v4.x compatibility:
// - string: "source $HOME/scripts/bootstrap.sh"
// - []string: ["env", "run_something"]
// - [][]string: [["echo", "Hello"], ["run", "script"]]
//
// Returns a list of shell command strings ready to execute.
func (o *Options) GetPreprocessCommands() []string {
if o.AdditionalPreprocessCmds == nil {
return nil
}
var commands []string
switch v := o.AdditionalPreprocessCmds.(type) {
case string:
// Single string command
if v != "" {
commands = append(commands, v)
}
case []interface{}:
// Array - could be []string or [][]string
for _, item := range v {
switch cmd := item.(type) {
case string:
// Simple string in array: ["cmd1", "cmd2"]
if cmd != "" {
commands = append(commands, cmd)
}
case []interface{}:
// Array of strings: [["echo", "hello"]]
// Join with spaces to form a single command
var parts []string
for _, part := range cmd {
if s, ok := part.(string); ok {
parts = append(parts, s)
}
}
if len(parts) > 0 {
commands = append(commands, strings.Join(parts, " "))
}
}
}
}
return commands
}
// FindStandaloneApp returns the PHP file to run for standalone applications (WEB_SERVER=none).
// If APP_START_CMD is set, returns that. Otherwise auto-detects in order:
// 1. app.php
// 2. main.php
// 3. run.php
// 4. start.php
//
// Returns empty string if no standalone app found.
func (o *Options) FindStandaloneApp(buildDir string) (string, error) {
// If user explicitly set APP_START_CMD, use it
if o.AppStartCmd != "" {
// Verify the file exists
appPath := filepath.Join(buildDir, o.AppStartCmd)
if exists, err := libbuildpack.FileExists(appPath); err != nil {
return "", fmt.Errorf("failed to check APP_START_CMD file: %w", err)
} else if !exists {
return "", fmt.Errorf("APP_START_CMD file not found: %s", o.AppStartCmd)
}
return o.AppStartCmd, nil
}
// Auto-detect standalone app (v4.x compatibility)
candidates := []string{"app.php", "main.php", "run.php", "start.php"}
for _, candidate := range candidates {
appPath := filepath.Join(buildDir, candidate)
if exists, err := libbuildpack.FileExists(appPath); err != nil {
return "", fmt.Errorf("failed to check for %s: %w", candidate, err)
} else if exists {
return candidate, nil
}
}
// No standalone app found
return "", fmt.Errorf("no standalone app found: set APP_START_CMD or create one of: %s", strings.Join(candidates, ", "))
}