Skip to content

Commit 64c1975

Browse files
committed
Fix #1157: Load PHP extensions from .bp-config/php/php.ini.d during composer
Parse extension= and zend_extension= directives from user .ini files and add them to the buildpack context so extensions are available during both composer install (build-time) and application runtime. Implementation: - Add loadUserExtensions() method to composer extension (composer.go) - Add loadUserExtensions() method to supply phase (supply.go) - Parse .bp-config/php/php.ini.d/*.ini files for extension directives - Extract extension names (strip .so suffix, quotes) - Add to context PHP_EXTENSIONS and ZEND_EXTENSIONS lists - Use uniqueStrings() helper to prevent duplicates This approach integrates with the buildpack's existing extension loading mechanism, avoiding duplicate loading and ensuring consistency between build-time and runtime configurations.
1 parent 59c6018 commit 64c1975

2 files changed

Lines changed: 189 additions & 8 deletions

File tree

src/php/extensions/composer/composer.go

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,12 @@ func (e *ComposerExtension) Compile(ctx *extensions.Context, installer *extensio
655655
return fmt.Errorf("failed to install PHP: %w", err)
656656
}
657657

658+
// Load user-requested extensions from .bp-config/php/php.ini.d/*.ini files
659+
// and add them to the context so they're available during composer
660+
if err := e.loadUserExtensions(ctx); err != nil {
661+
return fmt.Errorf("failed to load user extensions: %w", err)
662+
}
663+
658664
// Setup PHP configuration (config files + process extensions in php.ini)
659665
if err := e.setupPHPConfig(ctx); err != nil {
660666
return fmt.Errorf("failed to setup PHP config: %w", err)
@@ -905,39 +911,32 @@ func (e *ComposerExtension) setupPHPConfig(ctx *extensions.Context) error {
905911
phpInstallDir := filepath.Join(e.buildDir, "php")
906912
phpEtcDir := filepath.Join(phpInstallDir, "etc")
907913

908-
// Get PHP version from context to determine config path
909914
phpVersion := ctx.GetString("PHP_VERSION")
910915
if phpVersion == "" {
911916
return fmt.Errorf("PHP_VERSION not set in context")
912917
}
913918

914-
// Extract major.minor version (e.g., "8.1.32" -> "8.1")
915919
versionParts := strings.Split(phpVersion, ".")
916920
if len(versionParts) < 2 {
917921
return fmt.Errorf("invalid PHP version format: %s", phpVersion)
918922
}
919923
majorMinor := fmt.Sprintf("%s.%s", versionParts[0], versionParts[1])
920924
phpConfigPath := fmt.Sprintf("php/%s.x", majorMinor)
921925

922-
// Extract PHP config files from embedded defaults
923926
if err := config.ExtractConfig(phpConfigPath, phpEtcDir); err != nil {
924927
return fmt.Errorf("failed to extract PHP config: %w", err)
925928
}
926929

927-
// Create php.ini.d directory for extension configs
928930
phpIniDir := filepath.Join(phpEtcDir, "php.ini.d")
929931
if err := os.MkdirAll(phpIniDir, 0755); err != nil {
930932
return fmt.Errorf("failed to create php.ini.d directory: %w", err)
931933
}
932934

933-
// Process php.ini to replace extension placeholders
934935
phpIniPath := filepath.Join(phpEtcDir, "php.ini")
935936
if err := e.processPhpIni(ctx, phpIniPath); err != nil {
936937
return fmt.Errorf("failed to process php.ini: %w", err)
937938
}
938939

939-
// Copy processed php.ini to TMPDIR for Composer to use
940-
// This matches the Python buildpack behavior where PHPRC points to TMPDIR
941940
tmpPhpIniPath := filepath.Join(e.tmpDir, "php.ini")
942941
if err := e.copyFile(phpIniPath, tmpPhpIniPath); err != nil {
943942
return fmt.Errorf("failed to copy php.ini to TMPDIR: %w", err)
@@ -1200,10 +1199,11 @@ func (e *ComposerExtension) runComposerCommand(ctx *extensions.Context, phpPath,
12001199
func (e *ComposerExtension) buildComposerEnv() []string {
12011200
env := os.Environ()
12021201

1203-
// Add Composer-specific variables
12041202
vendorDir := filepath.Join(e.buildDir, e.composerVendorDir)
12051203
binDir := filepath.Join(e.buildDir, "php", "bin")
12061204
cacheDir := filepath.Join(e.composerHome, "cache")
1205+
phpEtcDir := filepath.Join(e.buildDir, "php", "etc")
1206+
phpIniScanDir := filepath.Join(phpEtcDir, "php.ini.d")
12071207

12081208
env = append(env,
12091209
fmt.Sprintf("COMPOSER_HOME=%s", e.composerHome),
@@ -1212,6 +1212,7 @@ func (e *ComposerExtension) buildComposerEnv() []string {
12121212
fmt.Sprintf("COMPOSER_CACHE_DIR=%s", cacheDir),
12131213
fmt.Sprintf("LD_LIBRARY_PATH=%s", filepath.Join(e.buildDir, "php", "lib")),
12141214
fmt.Sprintf("PHPRC=%s", e.tmpDir),
1215+
fmt.Sprintf("PHP_INI_SCAN_DIR=%s", phpIniScanDir),
12151216
)
12161217

12171218
return env
@@ -1250,6 +1251,86 @@ func (e *ComposerExtension) copyFile(src, dst string) error {
12501251
return err
12511252
}
12521253

1254+
func (e *ComposerExtension) loadUserExtensions(ctx *extensions.Context) error {
1255+
userPhpIniDir := filepath.Join(e.buildDir, ".bp-config", "php", "php.ini.d")
1256+
if _, err := os.Stat(userPhpIniDir); os.IsNotExist(err) {
1257+
return nil
1258+
}
1259+
1260+
fmt.Println(" Loading user-requested extensions from .bp-config/php/php.ini.d")
1261+
1262+
currentExtensions := ctx.GetStringSlice("PHP_EXTENSIONS")
1263+
currentZendExtensions := ctx.GetStringSlice("ZEND_EXTENSIONS")
1264+
1265+
extensionsToAdd := make(map[string]bool)
1266+
zendExtensionsToAdd := make(map[string]bool)
1267+
1268+
err := filepath.Walk(userPhpIniDir, func(path string, info os.FileInfo, err error) error {
1269+
if err != nil {
1270+
return err
1271+
}
1272+
1273+
if info.IsDir() || !strings.HasSuffix(strings.ToLower(path), ".ini") {
1274+
return nil
1275+
}
1276+
1277+
content, err := os.ReadFile(path)
1278+
if err != nil {
1279+
fmt.Printf(" WARNING: Failed to read %s: %v\n", path, err)
1280+
return nil
1281+
}
1282+
1283+
for _, line := range strings.Split(string(content), "\n") {
1284+
line = strings.TrimSpace(line)
1285+
1286+
if strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") || line == "" {
1287+
continue
1288+
}
1289+
1290+
if strings.HasPrefix(line, "extension=") || strings.HasPrefix(line, "extension =") {
1291+
extLine := strings.TrimSpace(strings.TrimPrefix(line, "extension="))
1292+
extLine = strings.TrimSpace(strings.TrimPrefix(extLine, "extension ="))
1293+
extName := strings.TrimSuffix(extLine, ".so")
1294+
extName = strings.Trim(extName, "\"' ")
1295+
if extName != "" {
1296+
extensionsToAdd[extName] = true
1297+
}
1298+
} else if strings.HasPrefix(line, "zend_extension=") || strings.HasPrefix(line, "zend_extension =") {
1299+
extLine := strings.TrimSpace(strings.TrimPrefix(line, "zend_extension="))
1300+
extLine = strings.TrimSpace(strings.TrimPrefix(extLine, "zend_extension ="))
1301+
extName := strings.TrimSuffix(extLine, ".so")
1302+
extName = strings.Trim(extName, "\"' ")
1303+
if extName != "" {
1304+
zendExtensionsToAdd[extName] = true
1305+
}
1306+
}
1307+
}
1308+
1309+
return nil
1310+
})
1311+
1312+
if err != nil {
1313+
return fmt.Errorf("failed to scan user ini files: %w", err)
1314+
}
1315+
1316+
for ext := range extensionsToAdd {
1317+
currentExtensions = append(currentExtensions, ext)
1318+
}
1319+
for ext := range zendExtensionsToAdd {
1320+
currentZendExtensions = append(currentZendExtensions, ext)
1321+
}
1322+
1323+
ctx.Set("PHP_EXTENSIONS", uniqueStrings(currentExtensions))
1324+
ctx.Set("ZEND_EXTENSIONS", uniqueStrings(currentZendExtensions))
1325+
1326+
if len(extensionsToAdd) > 0 || len(zendExtensionsToAdd) > 0 {
1327+
fmt.Printf(" Found %d extension(s) and %d zend extension(s) in user config\n",
1328+
len(extensionsToAdd), len(zendExtensionsToAdd))
1329+
}
1330+
1331+
return nil
1332+
}
1333+
12531334
// uniqueStrings returns a slice with duplicate strings removed
12541335
func uniqueStrings(input []string) []string {
12551336
seen := make(map[string]bool)

src/php/supply/supply.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,12 @@ func (s *Supplier) createExtensionContext() (*extensions.Context, error) {
198198
ctx.Set("WEB_SERVER", s.Options.WebServer)
199199
ctx.Set("COMPOSER_VERSION", ctx.GetString("COMPOSER_DEFAULT")) // Use default from manifest
200200

201+
// Load user-requested extensions from .bp-config/php/php.ini.d/*.ini files
202+
// and add them to the context so they're available at runtime
203+
if err := s.loadUserExtensions(ctx); err != nil {
204+
return nil, fmt.Errorf("failed to load user extensions: %w", err)
205+
}
206+
201207
// Set additional options
202208
ctx.Set("ADMIN_EMAIL", s.Options.AdminEmail)
203209
ctx.Set("COMPOSER_VENDOR_DIR", s.Options.ComposerVendorDir)
@@ -840,3 +846,97 @@ func (s *Supplier) copyFile(src, dest string) error {
840846
func (s *Supplier) ProcessPhpFpmConfForTesting(phpFpmConfPath, phpEtcDir string) error {
841847
return s.processPhpFpmConf(phpFpmConfPath, phpEtcDir)
842848
}
849+
850+
func (s *Supplier) loadUserExtensions(ctx *extensions.Context) error {
851+
userPhpIniDir := filepath.Join(s.Stager.BuildDir(), ".bp-config", "php", "php.ini.d")
852+
if _, err := os.Stat(userPhpIniDir); os.IsNotExist(err) {
853+
return nil
854+
}
855+
856+
fmt.Println(" Loading user-requested extensions from .bp-config/php/php.ini.d")
857+
858+
currentExtensions := ctx.GetStringSlice("PHP_EXTENSIONS")
859+
currentZendExtensions := ctx.GetStringSlice("ZEND_EXTENSIONS")
860+
861+
extensionsToAdd := make(map[string]bool)
862+
zendExtensionsToAdd := make(map[string]bool)
863+
864+
err := filepath.Walk(userPhpIniDir, func(path string, info os.FileInfo, err error) error {
865+
if err != nil {
866+
return err
867+
}
868+
869+
if info.IsDir() || !strings.HasSuffix(strings.ToLower(path), ".ini") {
870+
return nil
871+
}
872+
873+
content, err := os.ReadFile(path)
874+
if err != nil {
875+
fmt.Printf(" WARNING: Failed to read %s: %v\n", path, err)
876+
return nil
877+
}
878+
879+
for _, line := range strings.Split(string(content), "\n") {
880+
line = strings.TrimSpace(line)
881+
882+
if strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") || line == "" {
883+
continue
884+
}
885+
886+
if strings.HasPrefix(line, "extension=") || strings.HasPrefix(line, "extension =") {
887+
extLine := strings.TrimSpace(strings.TrimPrefix(line, "extension="))
888+
extLine = strings.TrimSpace(strings.TrimPrefix(extLine, "extension ="))
889+
extName := strings.TrimSuffix(extLine, ".so")
890+
extName = strings.Trim(extName, "\"' ")
891+
if extName != "" {
892+
extensionsToAdd[extName] = true
893+
}
894+
} else if strings.HasPrefix(line, "zend_extension=") || strings.HasPrefix(line, "zend_extension =") {
895+
extLine := strings.TrimSpace(strings.TrimPrefix(line, "zend_extension="))
896+
extLine = strings.TrimSpace(strings.TrimPrefix(extLine, "zend_extension ="))
897+
extName := strings.TrimSuffix(extLine, ".so")
898+
extName = strings.Trim(extName, "\"' ")
899+
if extName != "" {
900+
zendExtensionsToAdd[extName] = true
901+
}
902+
}
903+
}
904+
905+
return nil
906+
})
907+
908+
if err != nil {
909+
return fmt.Errorf("failed to scan user ini files: %w", err)
910+
}
911+
912+
for ext := range extensionsToAdd {
913+
currentExtensions = append(currentExtensions, ext)
914+
}
915+
for ext := range zendExtensionsToAdd {
916+
currentZendExtensions = append(currentZendExtensions, ext)
917+
}
918+
919+
ctx.Set("PHP_EXTENSIONS", uniqueStrings(currentExtensions))
920+
ctx.Set("ZEND_EXTENSIONS", uniqueStrings(currentZendExtensions))
921+
922+
if len(extensionsToAdd) > 0 || len(zendExtensionsToAdd) > 0 {
923+
fmt.Printf(" Found %d extension(s) and %d zend extension(s) in user config\n",
924+
len(extensionsToAdd), len(zendExtensionsToAdd))
925+
}
926+
927+
return nil
928+
}
929+
930+
func uniqueStrings(input []string) []string {
931+
seen := make(map[string]bool)
932+
result := []string{}
933+
934+
for _, item := range input {
935+
if !seen[item] {
936+
seen[item] = true
937+
result = append(result, item)
938+
}
939+
}
940+
941+
return result
942+
}

0 commit comments

Comments
 (0)