From aad3aed5a948135ebc7b3b4a704454cfef92fabb Mon Sep 17 00:00:00 2001 From: ramonskie Date: Thu, 29 Jan 2026 13:18:06 +0100 Subject: [PATCH 1/4] Remove runtime rewrite binary and fix php.ini.d context This commit removes the runtime rewrite binary and replaces it with build-time placeholder replacement, fixing multiple issues with multi-buildpack deployments and git URL buildpack usage. It also fixes a critical bug where php.ini.d configs were processed with the wrong HOME context. ## Rewrite Binary Removal The rewrite binary was originally copied from the v4.x Python buildpack and used to replace template variables in configuration files at runtime. This approach had several issues: - Failed when buildpack was deployed via git URL - Compilation errors in multi-buildpack scenarios - Security concerns with runtime config rewriting - Performance overhead at container startup This commit completes the migration to build-time placeholder replacement that provides better security, performance, and multi-buildpack compatibility. ### Changes: - Remove bin/rewrite shell wrapper script - Remove src/php/rewrite/cli/main.go (entire rewrite implementation) - Remove rewrite binary compilation from bin/finalize - Remove bin/rewrite from manifest.yml include_files - Remove /bin/rewrite-compiled from .gitignore - Update ARCHITECTURE.md to remove rewrite binary documentation ## Build-Time Placeholder Replacement Add ProcessConfigs() method to finalize phase that replaces @{VAR} placeholders with actual values during staging: - @{HOME} - App or dependency directory path - @{DEPS_DIR} - Dependencies directory (/home/vcap/deps) - @{WEBDIR} - Web document root (default: htdocs) - @{LIBDIR} - Library directory (default: lib) - @{PHP_FPM_LISTEN} - PHP-FPM socket/TCP address - @{TMPDIR} - Converted to ${TMPDIR} for runtime expansion - @{PHP_EXTENSIONS} - Extension directives - @{ZEND_EXTENSIONS} - Zend extension directives ## Placeholder Syntax Unification Unify all template variables to use @{VAR} syntax consistently across all config files (httpd, nginx, php-fpm, php.ini). ## Multi-Buildpack Fixes - Fix PHP-FPM PID file path to use deps directory for multi-buildpack scenarios - Fix nginx configuration for Unix socket and runtime variable expansion - Update supply buildpack integration tests ## php.ini.d Context Bug Fix Fix critical bug where php.ini.d directory was processed with deps context (@{HOME} = /home/vcap/deps/{idx}) instead of app context (@{HOME} = /home/vcap/app). The php.ini.d directory contains user-provided PHP configurations that typically reference application paths (include_path, open_basedir, etc.), similar to fpm.d configs. Processing with deps context caused the buildpack-created include-path.ini and user configs to reference incorrect paths. ### Changes: - Process php.ini.d separately (like fpm.d) with app-context replacements - Update supply.go comments to clarify php.ini.d context behavior - Add fixture test for @{HOME} placeholder in php.ini.d configs - Enhance modules integration test to verify placeholder replacement Both fpm.d and php.ini.d now use app HOME context while other PHP configs (php.ini, php-fpm.conf) use deps HOME context. Fixes issues with: - Buildpack-created include-path.ini referencing wrong directory - User-provided php.ini.d configs using @{HOME} placeholders - Include paths not resolving to application lib directory --- .gitignore | 1 - ARCHITECTURE.md | 110 ++--- bin/finalize | 8 +- bin/rewrite | 15 - .../config/httpd/extra/httpd-directories.conf | 2 +- defaults/config/httpd/extra/httpd-php.conf | 6 +- defaults/config/httpd/httpd.conf | 2 +- defaults/config/nginx/http-defaults.conf | 2 +- defaults/config/nginx/http-php.conf | 2 +- defaults/config/nginx/server-defaults.conf | 8 +- defaults/config/php/8.1.x/php-fpm.conf | 6 +- defaults/config/php/8.1.x/php.ini | 6 +- defaults/config/php/8.2.x/php-fpm.conf | 6 +- defaults/config/php/8.2.x/php.ini | 6 +- defaults/config/php/8.3.x/php-fpm.conf | 6 +- defaults/config/php/8.3.x/php.ini | 6 +- .../simple_brats.csproj | 5 +- .../.bp-config/php/fpm.d/test.conf | 2 +- .../.bp-config/php/php.ini.d/php.ini | 3 + fixtures/php_with_php_ini_d/index.php | 7 +- manifest.yml | 1 - scripts/integration.sh | 18 +- src/php/config/config.go | 4 +- .../config/httpd/extra/httpd-directories.conf | 2 +- .../config/httpd/extra/httpd-php.conf | 6 +- .../config/defaults/config/httpd/httpd.conf | 2 +- .../defaults/config/nginx/http-defaults.conf | 2 +- .../defaults/config/nginx/http-php.conf | 2 +- .../config/nginx/server-defaults.conf | 8 +- .../defaults/config/php/8.1.x/php-fpm.conf | 8 +- .../config/defaults/config/php/8.1.x/php.ini | 6 +- .../defaults/config/php/8.2.x/php-fpm.conf | 8 +- .../config/defaults/config/php/8.2.x/php.ini | 6 +- .../defaults/config/php/8.3.x/php-fpm.conf | 8 +- .../config/defaults/config/php/8.3.x/php.ini | 6 +- src/php/extensions/newrelic/newrelic.go | 4 +- src/php/extensions/newrelic/newrelic_test.go | 2 +- src/php/finalize/finalize.go | 409 ++++++++++++------ src/php/finalize/finalize_test.go | 131 +----- src/php/integration/modules_test.go | 8 +- src/php/rewrite/cli/main.go | 198 --------- src/php/supply/supply.go | 20 +- src/php/supply/supply_test.go | 8 +- 43 files changed, 436 insertions(+), 640 deletions(-) delete mode 100755 bin/rewrite delete mode 100644 src/php/rewrite/cli/main.go diff --git a/.gitignore b/.gitignore index aaf0937b2..885abb0e5 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,6 @@ test-verify*/ /bin/finalize-compiled /bin/release-compiled /bin/start-compiled -/bin/rewrite-compiled # Test binary and coverage files *.out diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 7cf8628c2..85f32dab6 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -20,7 +20,7 @@ The PHP buildpack uses a **hybrid architecture** that combines: 1. **Bash wrapper scripts** for buildpack lifecycle hooks (detect, supply, finalize, release) 2. **Go implementations** for core logic (compiled at staging time) -3. **Pre-compiled runtime utilities** for application startup (rewrite, start) +3. **Pre-compiled runtime utility** for application startup (start) This design optimizes for both flexibility during staging and performance at runtime. @@ -91,8 +91,9 @@ Installs dependencies: ### 3. Finalize Phase (`bin/finalize`) Configures the application for runtime: +- Processes configuration files to replace build-time placeholders with runtime values - Generates start scripts with correct paths -- Copies `rewrite` and `start` binaries to `$HOME/.bp/bin/` +- Copies `start` binary to `$HOME/.bp/bin/` - Sets up environment variables **Location:** `src/php/finalize/finalize.go` @@ -145,8 +146,8 @@ This triggers the following sequence: ├─► Load .procs file │ (defines processes to run) │ - ├─► $HOME/.bp/bin/rewrite - │ (substitute runtime variables) + ├─► Handle dynamic runtime variables + │ (PORT, TMPDIR via sed replacement) │ ├─► Start PHP-FPM │ (background, port 9000) @@ -158,72 +159,18 @@ This triggers the following sequence: (multiplex output, handle failures) ``` -## Pre-compiled Binaries +## Pre-compiled Binary -The buildpack includes two pre-compiled runtime utilities: +The buildpack includes a pre-compiled runtime utility: ### Why Pre-compiled? -Unlike lifecycle hooks (detect, supply, finalize) which run **during staging**, these utilities run **during application startup**. Pre-compilation provides: +Unlike lifecycle hooks (detect, supply, finalize) which run **during staging**, this utility runs **during application startup**. Pre-compilation provides: 1. **Fast startup time** - No compilation delay when starting the app 2. **Reliability** - Go toolchain not available in runtime container 3. **Simplicity** - Single binary, no dependencies -### `bin/rewrite` (1.7 MB) - -**Purpose:** Runtime configuration templating - -**Source:** `src/php/rewrite/cli/main.go` - -**Why needed:** Cloud Foundry assigns `$PORT` **at runtime**, not build time. Configuration files need runtime variable substitution. - -**Supported patterns:** - -| Pattern | Example | Replaced With | -|---------|---------|---------------| -| `@{VAR}` | `@{PORT}` | `$PORT` value | -| `#{VAR}` | `#{HOME}` | `$HOME` value | -| `@VAR@` | `@WEBDIR@` | `$WEBDIR` value | - -**Example usage:** - -```bash -# In start script -export PORT=8080 -export WEBDIR=htdocs -$HOME/.bp/bin/rewrite "$DEPS_DIR/0/php/etc" - -# Before: httpd.conf -Listen @{PORT} -DocumentRoot #{HOME}/@WEBDIR@ - -# After: httpd.conf -Listen 8080 -DocumentRoot /home/vcap/app/htdocs -``` - -**Key files rewritten:** -- `httpd.conf` - Apache configuration -- `nginx.conf` - Nginx configuration -- `php-fpm.conf` - PHP-FPM configuration -- `php.ini` - PHP configuration (extension_dir paths) - -**Implementation:** `src/php/rewrite/cli/main.go` - -```go -func rewriteFile(filePath string) error { - content := readFile(filePath) - - // Replace @{VAR}, #{VAR}, @VAR@, #VAR - result := replacePatterns(content, "@{", "}") - result = replacePatterns(result, "#{", "}") - result = replaceSimplePatterns(result, "@", "@") - - writeFile(filePath, result) -} -``` - ### `bin/start` (1.9 MB) **Purpose:** Multi-process manager @@ -317,24 +264,30 @@ These values **cannot be known at staging time**, so configuration files use tem ``` ┌──────────────────────────────────────────────────────────────┐ -│ 1. Staging Time (finalize.go) │ +│ 1. Staging Time (supply phase) │ │ - Copy template configs with @{PORT}, #{HOME}, etc. │ -│ - Generate start script with rewrite commands │ -│ - Copy pre-compiled rewrite binary to .bp/bin/ │ +│ - Placeholders remain in config files │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ -│ 2. Runtime (start script) │ -│ - Export environment variables (PORT, HOME, WEBDIR, etc.) │ -│ - Run: $HOME/.bp/bin/rewrite $DEPS_DIR/0/php/etc │ -│ - Run: $HOME/.bp/bin/rewrite $HOME/nginx/conf │ +│ 2. Finalize Phase (build-time processing) │ +│ - Replace build-time placeholders with known values │ +│ - Process PHP, PHP-FPM, and web server configs │ +│ - Dynamic runtime values (PORT, TMPDIR) handled via sed │ +└──────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ 3. Runtime (start script) │ +│ - Export environment variables (PORT, TMPDIR, etc.) │ +│ - Use sed to replace remaining dynamic variables │ │ - Configs now have actual values instead of templates │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ -│ 3. Start Processes │ +│ 4. Start Processes │ │ - PHP-FPM reads php-fpm.conf (with real PORT) │ │ - Web server reads config (with real HOME, WEBDIR) │ └──────────────────────────────────────────────────────────────┘ @@ -355,7 +308,7 @@ server { } ``` -**At runtime** (after rewrite with `PORT=8080`, `HOME=/home/vcap/app`, `WEBDIR=htdocs`, `PHP_FPM_LISTEN=127.0.0.1:9000`): +**At finalize/runtime** (after placeholder replacement with `PORT=8080`, `HOME=/home/vcap/app`, `WEBDIR=htdocs`, `PHP_FPM_LISTEN=127.0.0.1:9000`): ```nginx server { @@ -598,18 +551,14 @@ export BP_DEBUG=true # - Process startup logs ``` -### Modifying Rewrite or Start Binaries +### Modifying Start Binary ```bash # Edit source -vim src/php/rewrite/cli/main.go vim src/php/start/cli/main.go -# Rebuild binaries -cd src/php/rewrite/cli -go build -o ../../../../bin/rewrite - -cd ../../../start/cli +# Rebuild binary +cd src/php/start/cli go build -o ../../../../bin/start # Test changes @@ -621,9 +570,10 @@ go build -o ../../../../bin/start The PHP buildpack's unique architecture is driven by PHP's multi-process nature: 1. **Multi-process requirement** - PHP-FPM + Web Server (unlike Go/Ruby/Python single process) -2. **Runtime configuration** - Cloud Foundry assigns PORT at runtime (requires templating) -3. **Process coordination** - Two processes must start, run, and shutdown together -4. **Pre-compiled utilities** - Fast startup, no compilation during app start +2. **Build-time configuration processing** - Most placeholders replaced during finalize phase +3. **Runtime variable handling** - Dynamic values (PORT, TMPDIR) handled via sed at startup +4. **Process coordination** - Two processes must start, run, and shutdown together +5. **Pre-compiled utility** - Fast startup, no compilation during app start This architecture ensures PHP applications run reliably and efficiently in Cloud Foundry while maintaining compatibility with standard PHP deployment patterns. diff --git a/bin/finalize b/bin/finalize index b7b11be69..894d13886 100755 --- a/bin/finalize +++ b/bin/finalize @@ -9,11 +9,13 @@ PROFILE_DIR=$5 export BUILDPACK_DIR=`dirname $(readlink -f ${BASH_SOURCE%/*})` source "$BUILDPACK_DIR/scripts/install_go.sh" +export GoInstallDir="${GoInstallDir}" +export BP_DIR="$BUILDPACK_DIR" output_dir=$(mktemp -d -t finalizeXXX) -pushd $BUILDPACK_DIR -echo "-----> Running go build finalize" +pushd $BUILDPACK_DIR > /dev/null +echo "-----> Compiling finalize binary" GOROOT=$GoInstallDir $GoInstallDir/bin/go build -mod=vendor -o $output_dir/finalize ./src/php/finalize/cli -popd +popd > /dev/null $output_dir/finalize "$BUILD_DIR" "$CACHE_DIR" "$DEPS_DIR" "$DEPS_IDX" "$PROFILE_DIR" diff --git a/bin/rewrite b/bin/rewrite deleted file mode 100755 index e5d81db99..000000000 --- a/bin/rewrite +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -set -euo pipefail - -CONFIG_DIR=$1 - -export BUILDPACK_DIR=`dirname $(readlink -f ${BASH_SOURCE%/*})` -source "$BUILDPACK_DIR/scripts/install_go.sh" -output_dir=$(mktemp -d -t rewriteXXX) - -pushd $BUILDPACK_DIR -echo "-----> Running go build rewrite" -GOROOT=$GoInstallDir $GoInstallDir/bin/go build -mod=vendor -o $output_dir/rewrite ./src/php/rewrite/cli -popd - -$output_dir/rewrite "$CONFIG_DIR" diff --git a/defaults/config/httpd/extra/httpd-directories.conf b/defaults/config/httpd/extra/httpd-directories.conf index e844cdd5f..7a3587b05 100644 --- a/defaults/config/httpd/extra/httpd-directories.conf +++ b/defaults/config/httpd/extra/httpd-directories.conf @@ -3,7 +3,7 @@ Require all denied - + Options SymLinksIfOwnerMatch AllowOverride All Require all granted diff --git a/defaults/config/httpd/extra/httpd-php.conf b/defaults/config/httpd/extra/httpd-php.conf index e50e75733..d6a9563d2 100644 --- a/defaults/config/httpd/extra/httpd-php.conf +++ b/defaults/config/httpd/extra/httpd-php.conf @@ -1,6 +1,6 @@ DirectoryIndex index.php index.html index.htm -Define fcgi-listener fcgi://#{PHP_FPM_LISTEN}${HOME}/#{WEBDIR} +Define fcgi-listener fcgi://@{PHP_FPM_LISTEN}${HOME}/@{WEBDIR} # Noop ProxySet directive, disablereuse=On is the default value. @@ -11,10 +11,10 @@ Define fcgi-listener fcgi://#{PHP_FPM_LISTEN}${HOME}/#{WEBDIR} ProxySet disablereuse=On retry=0 - + # make sure the file exists so that if not, Apache will show its 404 page and not FPM - SetHandler proxy:fcgi://#{PHP_FPM_LISTEN} + SetHandler proxy:fcgi://@{PHP_FPM_LISTEN} diff --git a/defaults/config/httpd/httpd.conf b/defaults/config/httpd/httpd.conf index 81e4aebbb..9315f735e 100644 --- a/defaults/config/httpd/httpd.conf +++ b/defaults/config/httpd/httpd.conf @@ -2,7 +2,7 @@ ServerRoot "${HOME}/httpd" Listen ${PORT} ServerAdmin "${HTTPD_SERVER_ADMIN}" ServerName "0.0.0.0" -DocumentRoot "${HOME}/#{WEBDIR}" +DocumentRoot "${HOME}/@{WEBDIR}" Include conf/extra/httpd-modules.conf Include conf/extra/httpd-directories.conf Include conf/extra/httpd-mime.conf diff --git a/defaults/config/nginx/http-defaults.conf b/defaults/config/nginx/http-defaults.conf index 47fabe793..46cba7856 100644 --- a/defaults/config/nginx/http-defaults.conf +++ b/defaults/config/nginx/http-defaults.conf @@ -5,7 +5,7 @@ keepalive_timeout 65; gzip on; port_in_redirect off; - root @{HOME}/#{WEBDIR}; + root @{HOME}/@{WEBDIR}; index index.php index.html; server_tokens off; diff --git a/defaults/config/nginx/http-php.conf b/defaults/config/nginx/http-php.conf index cb3dc25ac..1a8757528 100644 --- a/defaults/config/nginx/http-php.conf +++ b/defaults/config/nginx/http-php.conf @@ -12,6 +12,6 @@ } upstream php_fpm { - server unix:#{PHP_FPM_LISTEN}; + server unix:@{PHP_FPM_LISTEN}; } diff --git a/defaults/config/nginx/server-defaults.conf b/defaults/config/nginx/server-defaults.conf index a82fc2f5c..fbe026856 100644 --- a/defaults/config/nginx/server-defaults.conf +++ b/defaults/config/nginx/server-defaults.conf @@ -1,10 +1,10 @@ - listen @{PORT}; + listen ${PORT}; server_name _; - fastcgi_temp_path @{TMPDIR}/nginx_fastcgi 1 2; - client_body_temp_path @{TMPDIR}/nginx_client_body 1 2; - proxy_temp_path @{TMPDIR}/nginx_proxy 1 2; + fastcgi_temp_path ${TMPDIR}/nginx_fastcgi 1 2; + client_body_temp_path ${TMPDIR}/nginx_client_body 1 2; + proxy_temp_path ${TMPDIR}/nginx_proxy 1 2; real_ip_header x-forwarded-for; set_real_ip_from 10.0.0.0/8; diff --git a/defaults/config/php/8.1.x/php-fpm.conf b/defaults/config/php/8.1.x/php-fpm.conf index 74966b9cf..2c629924e 100644 --- a/defaults/config/php/8.1.x/php-fpm.conf +++ b/defaults/config/php/8.1.x/php-fpm.conf @@ -148,7 +148,7 @@ daemonize = no ; specific port; ; '/path/to/unix/socket' - to listen on a unix socket. ; Note: This value is mandatory. -listen = #PHP_FPM_LISTEN +listen = @{PHP_FPM_LISTEN} ; Set listen(2) backlog. ; Default Value: 65535 (-1 on FreeBSD and OpenBSD) @@ -458,7 +458,7 @@ pm.max_spare_servers = 3 ; Chdir to this directory at the start. ; Note: relative path can be used. ; Default Value: current directory or / when chroot -;chdir = @{HOME}/#{WEBDIR} +;chdir = @{HOME}/@{WEBDIR} ; Redirect worker stdout and stderr into main error log. If not set, stdout and ; stderr will be redirected to /dev/null according to FastCGI specs. @@ -520,4 +520,4 @@ clear_env = no ; - the global prefix if it's been set (-p argument) ; - /tmp/staged/app/php otherwise ;include=@{HOME}/php/etc/fpm.d/*.conf -#{PHP_FPM_CONF_INCLUDE} +@{PHP_FPM_CONF_INCLUDE} diff --git a/defaults/config/php/8.1.x/php.ini b/defaults/config/php/8.1.x/php.ini index e795a48d8..035e3b6bf 100644 --- a/defaults/config/php/8.1.x/php.ini +++ b/defaults/config/php/8.1.x/php.ini @@ -737,7 +737,7 @@ default_charset = "UTF-8" ;;;;;;;;;;;;;;;;;;;;;;;;; ; UNIX: "/path1:/path2" -include_path = "../lib/php:@{HOME}/#{LIBDIR}" +include_path = "../lib/php:@{HOME}/@{LIBDIR}" ; ; Windows: "\path1;\path2" ;include_path = ".;c:\php\includes" @@ -915,8 +915,8 @@ default_socket_timeout = 60 ; extension folders as well as the separate PECL DLL download (PHP 5+). ; Be sure to appropriately set the extension_dir directive. ; -#{PHP_EXTENSIONS} -#{ZEND_EXTENSIONS} +@{PHP_EXTENSIONS} +@{ZEND_EXTENSIONS} ;;;;;;;;;;;;;;;;;;; ; Module Settings ; diff --git a/defaults/config/php/8.2.x/php-fpm.conf b/defaults/config/php/8.2.x/php-fpm.conf index 74966b9cf..2c629924e 100644 --- a/defaults/config/php/8.2.x/php-fpm.conf +++ b/defaults/config/php/8.2.x/php-fpm.conf @@ -148,7 +148,7 @@ daemonize = no ; specific port; ; '/path/to/unix/socket' - to listen on a unix socket. ; Note: This value is mandatory. -listen = #PHP_FPM_LISTEN +listen = @{PHP_FPM_LISTEN} ; Set listen(2) backlog. ; Default Value: 65535 (-1 on FreeBSD and OpenBSD) @@ -458,7 +458,7 @@ pm.max_spare_servers = 3 ; Chdir to this directory at the start. ; Note: relative path can be used. ; Default Value: current directory or / when chroot -;chdir = @{HOME}/#{WEBDIR} +;chdir = @{HOME}/@{WEBDIR} ; Redirect worker stdout and stderr into main error log. If not set, stdout and ; stderr will be redirected to /dev/null according to FastCGI specs. @@ -520,4 +520,4 @@ clear_env = no ; - the global prefix if it's been set (-p argument) ; - /tmp/staged/app/php otherwise ;include=@{HOME}/php/etc/fpm.d/*.conf -#{PHP_FPM_CONF_INCLUDE} +@{PHP_FPM_CONF_INCLUDE} diff --git a/defaults/config/php/8.2.x/php.ini b/defaults/config/php/8.2.x/php.ini index 86eb70ff1..e782f1598 100644 --- a/defaults/config/php/8.2.x/php.ini +++ b/defaults/config/php/8.2.x/php.ini @@ -737,7 +737,7 @@ default_charset = "UTF-8" ;;;;;;;;;;;;;;;;;;;;;;;;; ; UNIX: "/path1:/path2" -include_path = "../lib/php:@{HOME}/#{LIBDIR}" +include_path = "../lib/php:@{HOME}/@{LIBDIR}" ; ; Windows: "\path1;\path2" ;include_path = ".;c:\php\includes" @@ -915,8 +915,8 @@ default_socket_timeout = 60 ; extension folders as well as the separate PECL DLL download (PHP 5+). ; Be sure to appropriately set the extension_dir directive. ; -#{PHP_EXTENSIONS} -#{ZEND_EXTENSIONS} +@{PHP_EXTENSIONS} +@{ZEND_EXTENSIONS} ;;;;;;;;;;;;;;;;;;; ; Module Settings ; diff --git a/defaults/config/php/8.3.x/php-fpm.conf b/defaults/config/php/8.3.x/php-fpm.conf index 74966b9cf..2c629924e 100644 --- a/defaults/config/php/8.3.x/php-fpm.conf +++ b/defaults/config/php/8.3.x/php-fpm.conf @@ -148,7 +148,7 @@ daemonize = no ; specific port; ; '/path/to/unix/socket' - to listen on a unix socket. ; Note: This value is mandatory. -listen = #PHP_FPM_LISTEN +listen = @{PHP_FPM_LISTEN} ; Set listen(2) backlog. ; Default Value: 65535 (-1 on FreeBSD and OpenBSD) @@ -458,7 +458,7 @@ pm.max_spare_servers = 3 ; Chdir to this directory at the start. ; Note: relative path can be used. ; Default Value: current directory or / when chroot -;chdir = @{HOME}/#{WEBDIR} +;chdir = @{HOME}/@{WEBDIR} ; Redirect worker stdout and stderr into main error log. If not set, stdout and ; stderr will be redirected to /dev/null according to FastCGI specs. @@ -520,4 +520,4 @@ clear_env = no ; - the global prefix if it's been set (-p argument) ; - /tmp/staged/app/php otherwise ;include=@{HOME}/php/etc/fpm.d/*.conf -#{PHP_FPM_CONF_INCLUDE} +@{PHP_FPM_CONF_INCLUDE} diff --git a/defaults/config/php/8.3.x/php.ini b/defaults/config/php/8.3.x/php.ini index 451fa6b29..130cbfd74 100644 --- a/defaults/config/php/8.3.x/php.ini +++ b/defaults/config/php/8.3.x/php.ini @@ -752,7 +752,7 @@ default_charset = "UTF-8" ;;;;;;;;;;;;;;;;;;;;;;;;; ; UNIX: "/path1:/path2" -include_path = "../lib/php:@{HOME}/#{LIBDIR}" +include_path = "../lib/php:@{HOME}/@{LIBDIR}" ; ; Windows: "\path1;\path2" ;include_path = ".;c:\php\includes" @@ -930,8 +930,8 @@ default_socket_timeout = 60 ; extension folders as well as the separate PECL DLL download. ; Be sure to appropriately set the extension_dir directive. ; -#{PHP_EXTENSIONS} -#{ZEND_EXTENSIONS} +@{PHP_EXTENSIONS} +@{ZEND_EXTENSIONS} ;;;;;;;;;;;;;;;;;;; ; Module Settings ; diff --git a/fixtures/dotnet_core_as_supply_app/simple_brats.csproj b/fixtures/dotnet_core_as_supply_app/simple_brats.csproj index 2e8d004de..f5775898c 100644 --- a/fixtures/dotnet_core_as_supply_app/simple_brats.csproj +++ b/fixtures/dotnet_core_as_supply_app/simple_brats.csproj @@ -1,4 +1,5 @@ - - + + net8.0 + diff --git a/fixtures/php_with_fpm_d/.bp-config/php/fpm.d/test.conf b/fixtures/php_with_fpm_d/.bp-config/php/fpm.d/test.conf index 551b7024c..fa2e7e99f 100644 --- a/fixtures/php_with_fpm_d/.bp-config/php/fpm.d/test.conf +++ b/fixtures/php_with_fpm_d/.bp-config/php/fpm.d/test.conf @@ -18,4 +18,4 @@ ; the current environment. ; Default Value: clean env env[TEST_HOME_PATH] = @{HOME}/test/path -env[TEST_WEBDIR] = #{WEBDIR} +env[TEST_WEBDIR] = @{WEBDIR} diff --git a/fixtures/php_with_php_ini_d/.bp-config/php/php.ini.d/php.ini b/fixtures/php_with_php_ini_d/.bp-config/php/php.ini.d/php.ini index 9ab0a8cbd..0302ce430 100644 --- a/fixtures/php_with_php_ini_d/.bp-config/php/php.ini.d/php.ini +++ b/fixtures/php_with_php_ini_d/.bp-config/php/php.ini.d/php.ini @@ -6,4 +6,7 @@ error_prepend_string = 'teststring' +; Test placeholder replacement - @{HOME} should resolve to /home/vcap/app +include_path = ".:/usr/share/php:@{HOME}/lib" + ; End: diff --git a/fixtures/php_with_php_ini_d/index.php b/fixtures/php_with_php_ini_d/index.php index 147cebcdd..6f8ff9a78 100644 --- a/fixtures/php_with_php_ini_d/index.php +++ b/fixtures/php_with_php_ini_d/index.php @@ -1 +1,6 @@ - + diff --git a/manifest.yml b/manifest.yml index 7a9e1166a..e4ec279d2 100644 --- a/manifest.yml +++ b/manifest.yml @@ -854,7 +854,6 @@ include_files: - bin/finalize - bin/release - bin/supply -- bin/rewrite - bin/start - manifest.yml pre_package: scripts/build.sh diff --git a/scripts/integration.sh b/scripts/integration.sh index 75d6b2522..e4f2d3bb6 100755 --- a/scripts/integration.sh +++ b/scripts/integration.sh @@ -125,7 +125,7 @@ function specs::run() { platform_flag="--platform=${platform}" stack_flag="--stack=${stack}" token_flag="--github-token=${token}" - keep_failed_flag="" + keep_failed_flag="--keep-failed-containers=${keep_failed}" nodes=1 if [[ "${parallel}" == "true" ]]; then @@ -133,10 +133,6 @@ function specs::run() { serial_flag="" fi - if [[ "${keep_failed}" == "true" ]]; then - keep_failed_flag="--keep-failed-containers" - fi - local buildpack_file version version="$(cat "${ROOTDIR}/VERSION")" buildpack_file="$(buildpack::package "${version}" "${cached}" "${stack}")" @@ -151,12 +147,12 @@ function specs::run() { -mod vendor \ -v \ "${src}/integration" \ - "${cached_flag}" \ - "${platform_flag}" \ - "${token_flag}" \ - "${stack_flag}" \ - "${serial_flag}" \ - "${keep_failed_flag}" + ${cached_flag} \ + ${platform_flag} \ + ${token_flag} \ + ${stack_flag} \ + ${serial_flag} \ + ${keep_failed_flag} } function buildpack::package() { diff --git a/src/php/config/config.go b/src/php/config/config.go index a0e4b9f1c..4ec17171b 100644 --- a/src/php/config/config.go +++ b/src/php/config/config.go @@ -270,8 +270,8 @@ func ProcessPhpIni( } zendExtensionsString := strings.Join(zendExtensionLines, "\n") - phpIniContent = strings.ReplaceAll(phpIniContent, "#{PHP_EXTENSIONS}", extensionsString) - phpIniContent = strings.ReplaceAll(phpIniContent, "#{ZEND_EXTENSIONS}", zendExtensionsString) + phpIniContent = strings.ReplaceAll(phpIniContent, "@{PHP_EXTENSIONS}", extensionsString) + phpIniContent = strings.ReplaceAll(phpIniContent, "@{ZEND_EXTENSIONS}", zendExtensionsString) for placeholder, value := range additionalReplacements { phpIniContent = strings.ReplaceAll(phpIniContent, placeholder, value) diff --git a/src/php/config/defaults/config/httpd/extra/httpd-directories.conf b/src/php/config/defaults/config/httpd/extra/httpd-directories.conf index e844cdd5f..7a3587b05 100644 --- a/src/php/config/defaults/config/httpd/extra/httpd-directories.conf +++ b/src/php/config/defaults/config/httpd/extra/httpd-directories.conf @@ -3,7 +3,7 @@ Require all denied - + Options SymLinksIfOwnerMatch AllowOverride All Require all granted diff --git a/src/php/config/defaults/config/httpd/extra/httpd-php.conf b/src/php/config/defaults/config/httpd/extra/httpd-php.conf index e50e75733..d6a9563d2 100644 --- a/src/php/config/defaults/config/httpd/extra/httpd-php.conf +++ b/src/php/config/defaults/config/httpd/extra/httpd-php.conf @@ -1,6 +1,6 @@ DirectoryIndex index.php index.html index.htm -Define fcgi-listener fcgi://#{PHP_FPM_LISTEN}${HOME}/#{WEBDIR} +Define fcgi-listener fcgi://@{PHP_FPM_LISTEN}${HOME}/@{WEBDIR} # Noop ProxySet directive, disablereuse=On is the default value. @@ -11,10 +11,10 @@ Define fcgi-listener fcgi://#{PHP_FPM_LISTEN}${HOME}/#{WEBDIR} ProxySet disablereuse=On retry=0 - + # make sure the file exists so that if not, Apache will show its 404 page and not FPM - SetHandler proxy:fcgi://#{PHP_FPM_LISTEN} + SetHandler proxy:fcgi://@{PHP_FPM_LISTEN} diff --git a/src/php/config/defaults/config/httpd/httpd.conf b/src/php/config/defaults/config/httpd/httpd.conf index 81e4aebbb..9315f735e 100644 --- a/src/php/config/defaults/config/httpd/httpd.conf +++ b/src/php/config/defaults/config/httpd/httpd.conf @@ -2,7 +2,7 @@ ServerRoot "${HOME}/httpd" Listen ${PORT} ServerAdmin "${HTTPD_SERVER_ADMIN}" ServerName "0.0.0.0" -DocumentRoot "${HOME}/#{WEBDIR}" +DocumentRoot "${HOME}/@{WEBDIR}" Include conf/extra/httpd-modules.conf Include conf/extra/httpd-directories.conf Include conf/extra/httpd-mime.conf diff --git a/src/php/config/defaults/config/nginx/http-defaults.conf b/src/php/config/defaults/config/nginx/http-defaults.conf index 47fabe793..46cba7856 100644 --- a/src/php/config/defaults/config/nginx/http-defaults.conf +++ b/src/php/config/defaults/config/nginx/http-defaults.conf @@ -5,7 +5,7 @@ keepalive_timeout 65; gzip on; port_in_redirect off; - root @{HOME}/#{WEBDIR}; + root @{HOME}/@{WEBDIR}; index index.php index.html; server_tokens off; diff --git a/src/php/config/defaults/config/nginx/http-php.conf b/src/php/config/defaults/config/nginx/http-php.conf index 0f42b28a8..1a8757528 100644 --- a/src/php/config/defaults/config/nginx/http-php.conf +++ b/src/php/config/defaults/config/nginx/http-php.conf @@ -12,6 +12,6 @@ } upstream php_fpm { - server #{PHP_FPM_LISTEN}; + server unix:@{PHP_FPM_LISTEN}; } diff --git a/src/php/config/defaults/config/nginx/server-defaults.conf b/src/php/config/defaults/config/nginx/server-defaults.conf index a82fc2f5c..fbe026856 100644 --- a/src/php/config/defaults/config/nginx/server-defaults.conf +++ b/src/php/config/defaults/config/nginx/server-defaults.conf @@ -1,10 +1,10 @@ - listen @{PORT}; + listen ${PORT}; server_name _; - fastcgi_temp_path @{TMPDIR}/nginx_fastcgi 1 2; - client_body_temp_path @{TMPDIR}/nginx_client_body 1 2; - proxy_temp_path @{TMPDIR}/nginx_proxy 1 2; + fastcgi_temp_path ${TMPDIR}/nginx_fastcgi 1 2; + client_body_temp_path ${TMPDIR}/nginx_client_body 1 2; + proxy_temp_path ${TMPDIR}/nginx_proxy 1 2; real_ip_header x-forwarded-for; set_real_ip_from 10.0.0.0/8; diff --git a/src/php/config/defaults/config/php/8.1.x/php-fpm.conf b/src/php/config/defaults/config/php/8.1.x/php-fpm.conf index 7feb57ed4..1eb08dc8c 100644 --- a/src/php/config/defaults/config/php/8.1.x/php-fpm.conf +++ b/src/php/config/defaults/config/php/8.1.x/php-fpm.conf @@ -14,7 +14,7 @@ ; Pid file ; Note: the default prefix is /tmp/staged/app/php/var ; Default Value: none -pid = #DEPS_DIR/0/php/var/run/php-fpm.pid +pid = @{HOME}/php/var/run/php-fpm.pid ; Error log file ; If it's set to "syslog", log is sent to syslogd instead of being written @@ -148,7 +148,7 @@ group = vcap ; specific port; ; '/path/to/unix/socket' - to listen on a unix socket. ; Note: This value is mandatory. -listen = #PHP_FPM_LISTEN +listen = @{PHP_FPM_LISTEN} ; Set listen(2) backlog. ; Default Value: 65535 (-1 on FreeBSD and OpenBSD) @@ -458,7 +458,7 @@ pm.max_spare_servers = 3 ; Chdir to this directory at the start. ; Note: relative path can be used. ; Default Value: current directory or / when chroot -;chdir = @{HOME}/#{WEBDIR} +;chdir = @{HOME}/@{WEBDIR} ; Redirect worker stdout and stderr into main error log. If not set, stdout and ; stderr will be redirected to /dev/null according to FastCGI specs. @@ -520,4 +520,4 @@ clear_env = no ; - the global prefix if it's been set (-p argument) ; - /tmp/staged/app/php otherwise ;include=@{HOME}/php/etc/fpm.d/*.conf -#{PHP_FPM_CONF_INCLUDE} +@{PHP_FPM_CONF_INCLUDE} diff --git a/src/php/config/defaults/config/php/8.1.x/php.ini b/src/php/config/defaults/config/php/8.1.x/php.ini index e795a48d8..035e3b6bf 100644 --- a/src/php/config/defaults/config/php/8.1.x/php.ini +++ b/src/php/config/defaults/config/php/8.1.x/php.ini @@ -737,7 +737,7 @@ default_charset = "UTF-8" ;;;;;;;;;;;;;;;;;;;;;;;;; ; UNIX: "/path1:/path2" -include_path = "../lib/php:@{HOME}/#{LIBDIR}" +include_path = "../lib/php:@{HOME}/@{LIBDIR}" ; ; Windows: "\path1;\path2" ;include_path = ".;c:\php\includes" @@ -915,8 +915,8 @@ default_socket_timeout = 60 ; extension folders as well as the separate PECL DLL download (PHP 5+). ; Be sure to appropriately set the extension_dir directive. ; -#{PHP_EXTENSIONS} -#{ZEND_EXTENSIONS} +@{PHP_EXTENSIONS} +@{ZEND_EXTENSIONS} ;;;;;;;;;;;;;;;;;;; ; Module Settings ; diff --git a/src/php/config/defaults/config/php/8.2.x/php-fpm.conf b/src/php/config/defaults/config/php/8.2.x/php-fpm.conf index 7feb57ed4..1eb08dc8c 100644 --- a/src/php/config/defaults/config/php/8.2.x/php-fpm.conf +++ b/src/php/config/defaults/config/php/8.2.x/php-fpm.conf @@ -14,7 +14,7 @@ ; Pid file ; Note: the default prefix is /tmp/staged/app/php/var ; Default Value: none -pid = #DEPS_DIR/0/php/var/run/php-fpm.pid +pid = @{HOME}/php/var/run/php-fpm.pid ; Error log file ; If it's set to "syslog", log is sent to syslogd instead of being written @@ -148,7 +148,7 @@ group = vcap ; specific port; ; '/path/to/unix/socket' - to listen on a unix socket. ; Note: This value is mandatory. -listen = #PHP_FPM_LISTEN +listen = @{PHP_FPM_LISTEN} ; Set listen(2) backlog. ; Default Value: 65535 (-1 on FreeBSD and OpenBSD) @@ -458,7 +458,7 @@ pm.max_spare_servers = 3 ; Chdir to this directory at the start. ; Note: relative path can be used. ; Default Value: current directory or / when chroot -;chdir = @{HOME}/#{WEBDIR} +;chdir = @{HOME}/@{WEBDIR} ; Redirect worker stdout and stderr into main error log. If not set, stdout and ; stderr will be redirected to /dev/null according to FastCGI specs. @@ -520,4 +520,4 @@ clear_env = no ; - the global prefix if it's been set (-p argument) ; - /tmp/staged/app/php otherwise ;include=@{HOME}/php/etc/fpm.d/*.conf -#{PHP_FPM_CONF_INCLUDE} +@{PHP_FPM_CONF_INCLUDE} diff --git a/src/php/config/defaults/config/php/8.2.x/php.ini b/src/php/config/defaults/config/php/8.2.x/php.ini index 86eb70ff1..e782f1598 100644 --- a/src/php/config/defaults/config/php/8.2.x/php.ini +++ b/src/php/config/defaults/config/php/8.2.x/php.ini @@ -737,7 +737,7 @@ default_charset = "UTF-8" ;;;;;;;;;;;;;;;;;;;;;;;;; ; UNIX: "/path1:/path2" -include_path = "../lib/php:@{HOME}/#{LIBDIR}" +include_path = "../lib/php:@{HOME}/@{LIBDIR}" ; ; Windows: "\path1;\path2" ;include_path = ".;c:\php\includes" @@ -915,8 +915,8 @@ default_socket_timeout = 60 ; extension folders as well as the separate PECL DLL download (PHP 5+). ; Be sure to appropriately set the extension_dir directive. ; -#{PHP_EXTENSIONS} -#{ZEND_EXTENSIONS} +@{PHP_EXTENSIONS} +@{ZEND_EXTENSIONS} ;;;;;;;;;;;;;;;;;;; ; Module Settings ; diff --git a/src/php/config/defaults/config/php/8.3.x/php-fpm.conf b/src/php/config/defaults/config/php/8.3.x/php-fpm.conf index 7feb57ed4..1eb08dc8c 100644 --- a/src/php/config/defaults/config/php/8.3.x/php-fpm.conf +++ b/src/php/config/defaults/config/php/8.3.x/php-fpm.conf @@ -14,7 +14,7 @@ ; Pid file ; Note: the default prefix is /tmp/staged/app/php/var ; Default Value: none -pid = #DEPS_DIR/0/php/var/run/php-fpm.pid +pid = @{HOME}/php/var/run/php-fpm.pid ; Error log file ; If it's set to "syslog", log is sent to syslogd instead of being written @@ -148,7 +148,7 @@ group = vcap ; specific port; ; '/path/to/unix/socket' - to listen on a unix socket. ; Note: This value is mandatory. -listen = #PHP_FPM_LISTEN +listen = @{PHP_FPM_LISTEN} ; Set listen(2) backlog. ; Default Value: 65535 (-1 on FreeBSD and OpenBSD) @@ -458,7 +458,7 @@ pm.max_spare_servers = 3 ; Chdir to this directory at the start. ; Note: relative path can be used. ; Default Value: current directory or / when chroot -;chdir = @{HOME}/#{WEBDIR} +;chdir = @{HOME}/@{WEBDIR} ; Redirect worker stdout and stderr into main error log. If not set, stdout and ; stderr will be redirected to /dev/null according to FastCGI specs. @@ -520,4 +520,4 @@ clear_env = no ; - the global prefix if it's been set (-p argument) ; - /tmp/staged/app/php otherwise ;include=@{HOME}/php/etc/fpm.d/*.conf -#{PHP_FPM_CONF_INCLUDE} +@{PHP_FPM_CONF_INCLUDE} diff --git a/src/php/config/defaults/config/php/8.3.x/php.ini b/src/php/config/defaults/config/php/8.3.x/php.ini index 451fa6b29..130cbfd74 100644 --- a/src/php/config/defaults/config/php/8.3.x/php.ini +++ b/src/php/config/defaults/config/php/8.3.x/php.ini @@ -752,7 +752,7 @@ default_charset = "UTF-8" ;;;;;;;;;;;;;;;;;;;;;;;;; ; UNIX: "/path1:/path2" -include_path = "../lib/php:@{HOME}/#{LIBDIR}" +include_path = "../lib/php:@{HOME}/@{LIBDIR}" ; ; Windows: "\path1;\path2" ;include_path = ".;c:\php\includes" @@ -930,8 +930,8 @@ default_socket_timeout = 60 ; extension folders as well as the separate PECL DLL download. ; Be sure to appropriately set the extension_dir directive. ; -#{PHP_EXTENSIONS} -#{ZEND_EXTENSIONS} +@{PHP_EXTENSIONS} +@{ZEND_EXTENSIONS} ;;;;;;;;;;;;;;;;;;; ; Module Settings ; diff --git a/src/php/extensions/newrelic/newrelic.go b/src/php/extensions/newrelic/newrelic.go index f8af9b828..32ebf87d6 100644 --- a/src/php/extensions/newrelic/newrelic.go +++ b/src/php/extensions/newrelic/newrelic.go @@ -223,10 +223,10 @@ func (e *NewRelicExtension) modifyPHPIni() error { } } - // If no extensions found, insert after #{PHP_EXTENSIONS} marker + // If no extensions found, insert after @{PHP_EXTENSIONS} marker if insertPos == -1 { for i, line := range lines { - if strings.Contains(line, "#{PHP_EXTENSIONS}") { + if strings.Contains(line, "@{PHP_EXTENSIONS}") { insertPos = i + 1 break } diff --git a/src/php/extensions/newrelic/newrelic_test.go b/src/php/extensions/newrelic/newrelic_test.go index 3db984b68..6e1cec2eb 100644 --- a/src/php/extensions/newrelic/newrelic_test.go +++ b/src/php/extensions/newrelic/newrelic_test.go @@ -266,7 +266,7 @@ extension_dir = "/home/vcap/app/php/lib/php/extensions/debug-zts-20210902" phpIniPath = filepath.Join(phpDir, "php.ini") phpIniContent := `[PHP] extension_dir = "/home/vcap/app/php/lib/php/extensions/no-debug-non-zts-20210902" -#{PHP_EXTENSIONS} +@{PHP_EXTENSIONS} ` Expect(os.WriteFile(phpIniPath, []byte(phpIniContent), 0644)).To(Succeed()) diff --git a/src/php/finalize/finalize.go b/src/php/finalize/finalize.go index 5b8b40b8c..851921fa2 100644 --- a/src/php/finalize/finalize.go +++ b/src/php/finalize/finalize.go @@ -5,6 +5,7 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/cloudfoundry/libbuildpack" "github.com/cloudfoundry/php-buildpack/src/php/extensions" @@ -101,6 +102,22 @@ func Run(f *Finalizer) error { } } + // Load options for config processing + bpDir := os.Getenv("BP_DIR") + if bpDir == "" { + return fmt.Errorf("BP_DIR environment variable not set") + } + opts, err := options.LoadOptions(bpDir, f.Stager.BuildDir(), f.Manifest, f.Log) + if err != nil { + return fmt.Errorf("could not load options: %v", err) + } + + // Process all config files (replace build-time placeholders) + if err := f.ProcessConfigs(opts); err != nil { + f.Log.Error("Error processing configs: %v", err) + return err + } + // Create start script if err := f.CreateStartScript(); err != nil { f.Log.Error("Error creating start script: %v", err) @@ -214,6 +231,120 @@ export PATH="$DEPS_DIR/%s/php/bin:$DEPS_DIR/%s/php/sbin:$PATH" return f.Stager.WriteProfileD("php-env.sh", scriptContent) } +// ProcessConfigs replaces build-time placeholders in all config files +func (f *Finalizer) ProcessConfigs(opts *options.Options) error { + buildDir := f.Stager.BuildDir() + depsIdx := f.Stager.DepsIdx() + depDir := f.Stager.DepDir() + + // Determine web server + webServer := opts.WebServer + webDir := opts.WebDir + if webDir == "" { + webDir = "htdocs" + } + libDir := opts.LibDir + if libDir == "" { + libDir = "lib" + } + + // Determine PHP-FPM listen address first (needed for both PHP and web server configs) + phpFpmListen := "127.0.0.1:9000" // Default TCP + if webServer == "nginx" { + // Nginx uses Unix socket for better performance + phpFpmListen = filepath.Join("/home/vcap/deps", depsIdx, "php", "var", "run", "php-fpm.sock") + } + + // Process PHP configs - use deps directory for @{HOME} + phpEtcDir := filepath.Join(depDir, "php", "etc") + if exists, _ := libbuildpack.FileExists(phpEtcDir); exists { + depsPath := filepath.Join("/home/vcap/deps", depsIdx) + phpReplacements := map[string]string{ + "@{HOME}": depsPath, + "@{DEPS_DIR}": "/home/vcap/deps", // Available for user configs, though rarely needed + "@{LIBDIR}": libDir, + "@{PHP_FPM_LISTEN}": phpFpmListen, + // @{TMPDIR} is converted to ${TMPDIR} for shell expansion at runtime + // This allows users to customize TMPDIR via environment variable + "@{TMPDIR}": "${TMPDIR}", + } + + // Process fpm.d and php.ini.d directories separately with app HOME (not deps HOME) + // This is because these configs typically reference app paths: + // - fpm.d: environment variables for PHP scripts (run in app context) + // - php.ini.d: include paths, open_basedir, etc. (reference app directories) + fpmDDir := filepath.Join(phpEtcDir, "fpm.d") + phpIniDDir := filepath.Join(phpEtcDir, "php.ini.d") + + // Process PHP configs, excluding fpm.d and php.ini.d which we'll process separately + f.Log.Debug("Processing PHP configs in %s with replacements: %v (excluding fpm.d and php.ini.d)", phpEtcDir, phpReplacements) + if err := f.replacePlaceholdersInDirExclude(phpEtcDir, phpReplacements, []string{fpmDDir, phpIniDDir}); err != nil { + return fmt.Errorf("failed to process PHP configs: %w", err) + } + + // App-context replacements for fpm.d and php.ini.d + appContextReplacements := map[string]string{ + "@{HOME}": "/home/vcap/app", // Use app HOME for app-relative paths + "@{WEBDIR}": webDir, + "@{LIBDIR}": libDir, + "@{TMPDIR}": "${TMPDIR}", + } + + if exists, _ := libbuildpack.FileExists(fpmDDir); exists { + f.Log.Debug("Processing fpm.d configs in %s with replacements: %v", fpmDDir, appContextReplacements) + if err := f.replacePlaceholdersInDir(fpmDDir, appContextReplacements); err != nil { + return fmt.Errorf("failed to process fpm.d configs: %w", err) + } + } + + if exists, _ := libbuildpack.FileExists(phpIniDDir); exists { + f.Log.Debug("Processing php.ini.d configs in %s with replacements: %v", phpIniDDir, appContextReplacements) + if err := f.replacePlaceholdersInDir(phpIniDDir, appContextReplacements); err != nil { + return fmt.Errorf("failed to process php.ini.d configs: %w", err) + } + } + } + + // Process web server configs - use app directory for ${HOME} + appReplacements := map[string]string{ + "@{WEBDIR}": webDir, + "@{LIBDIR}": libDir, + "@{PHP_FPM_LISTEN}": phpFpmListen, + } + + // Process HTTPD configs + if webServer == "httpd" { + httpdConfDir := filepath.Join(buildDir, "httpd", "conf") + if exists, _ := libbuildpack.FileExists(httpdConfDir); exists { + f.Log.Debug("Processing HTTPD configs in %s", httpdConfDir) + if err := f.replacePlaceholdersInDir(httpdConfDir, appReplacements); err != nil { + return fmt.Errorf("failed to process HTTPD configs: %w", err) + } + } + } + + // Process Nginx configs + if webServer == "nginx" { + nginxConfDir := filepath.Join(buildDir, "nginx", "conf") + if exists, _ := libbuildpack.FileExists(nginxConfDir); exists { + // For nginx, also need to handle @{HOME} in some configs (like pid file) + nginxReplacements := make(map[string]string) + for k, v := range appReplacements { + nginxReplacements[k] = v + } + nginxReplacements["@{HOME}"] = "/home/vcap/app" + + f.Log.Debug("Processing Nginx configs in %s", nginxConfDir) + if err := f.replacePlaceholdersInDir(nginxConfDir, nginxReplacements); err != nil { + return fmt.Errorf("failed to process Nginx configs: %w", err) + } + } + } + + f.Log.Info("Config processing complete") + return nil +} + // CreateStartScript creates the start script for the application func (f *Finalizer) CreateStartScript() error { bpBinDir := filepath.Join(f.Stager.BuildDir(), ".bp", "bin") @@ -229,15 +360,6 @@ func (f *Finalizer) CreateStartScript() error { return fmt.Errorf("BP_DIR environment variable not set") } - // Copy pre-compiled rewrite binary from bin/rewrite to .bp/bin/rewrite - rewriteSrc := filepath.Join(bpDir, "bin", "rewrite") - rewriteDst := filepath.Join(bpBinDir, "rewrite") - if err := f.copyFile(rewriteSrc, rewriteDst); err != nil { - return fmt.Errorf("could not copy rewrite binary: %v", err) - } - f.Log.Debug("Copied pre-compiled rewrite binary to .bp/bin") - - // Load options from options.json to determine which web server to use opts, err := options.LoadOptions(bpDir, f.Stager.BuildDir(), f.Manifest, f.Log) if err != nil { return fmt.Errorf("could not load options: %v", err) @@ -269,56 +391,25 @@ func (f *Finalizer) CreateStartScript() error { return nil } -// writePreStartScript creates a pre-start wrapper that handles config rewriting -// before running optional user commands (e.g., migrations) and starting the server. -// This allows PHP commands to run with properly rewritten configs. +// writePreStartScript creates a pre-start wrapper that runs optional user commands +// (e.g., migrations) before starting the server. func (f *Finalizer) writePreStartScript() error { - depsIdx := f.Stager.DepsIdx() - - // Create script in .bp/bin/ directory (same location as start and rewrite) + // Create script in .bp/bin/ directory (same location as start) bpBinDir := filepath.Join(f.Stager.BuildDir(), ".bp", "bin") if err := os.MkdirAll(bpBinDir, 0755); err != nil { return fmt.Errorf("could not create .bp/bin directory: %v", err) } preStartPath := filepath.Join(bpBinDir, "pre-start") - script := fmt.Sprintf(`#!/usr/bin/env bash + script := `#!/usr/bin/env bash # PHP Pre-Start Wrapper -# Runs config rewriting and optional user command before starting servers +# Runs optional user command before starting servers set -e # Set DEPS_DIR with fallback : ${DEPS_DIR:=$HOME/.cloudfoundry} export DEPS_DIR -# Source all profile.d scripts to set up environment -for f in /home/vcap/deps/%s/profile.d/*.sh; do - [ -f "$f" ] && source "$f" -done - -# Export required variables for rewrite tool -export HOME="${HOME:-/home/vcap/app}" -export PHPRC="$DEPS_DIR/%s/php/etc" -export PHP_INI_SCAN_DIR="$DEPS_DIR/%s/php/etc/php.ini.d" - -echo "-----> Pre-start: Rewriting PHP configs..." - -# Rewrite PHP base configs with HOME=$DEPS_DIR/0 -OLD_HOME="$HOME" -export HOME="$DEPS_DIR/%s" -$OLD_HOME/.bp/bin/rewrite "$DEPS_DIR/%s/php/etc/php.ini" -$OLD_HOME/.bp/bin/rewrite "$DEPS_DIR/%s/php/etc/php-fpm.conf" -export HOME="$OLD_HOME" - -# Rewrite user configs with app HOME -if [ -d "$DEPS_DIR/%s/php/etc/fpm.d" ]; then - $HOME/.bp/bin/rewrite "$DEPS_DIR/%s/php/etc/fpm.d" -fi - -if [ -d "$DEPS_DIR/%s/php/etc/php.ini.d" ]; then - $HOME/.bp/bin/rewrite "$DEPS_DIR/%s/php/etc/php.ini.d" -fi - # Run user command if provided if [ $# -gt 0 ]; then echo "-----> Pre-start: Running command: $@" @@ -331,7 +422,7 @@ fi # Start the application servers echo "-----> Pre-start: Starting application..." exec $HOME/.bp/bin/start -`, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx) +` if err := os.WriteFile(preStartPath, []byte(script), 0755); err != nil { return fmt.Errorf("could not write pre-start script: %v", err) @@ -368,15 +459,18 @@ func (f *Finalizer) generateHTTPDStartScript(depsIdx string, opts *options.Optio libDir = "lib" // default } - phpFpmConfInclude := "; No additional includes" - return fmt.Sprintf(`#!/usr/bin/env bash # PHP Application Start Script (HTTPD) set -e # Set DEPS_DIR with fallback for different environments -: ${DEPS_DIR:=$HOME/.cloudfoundry} +: ${DEPS_DIR:=/home/vcap/deps} export DEPS_DIR + +# Set TMPDIR with fallback (users can override via environment variable) +: ${TMPDIR:=/home/vcap/tmp} +export TMPDIR + export PHPRC="$DEPS_DIR/%s/php/etc" export PHP_INI_SCAN_DIR="$DEPS_DIR/%s/php/etc/php.ini.d" @@ -386,56 +480,41 @@ export PATH="$DEPS_DIR/%s/php/bin:$PATH" # Set HTTPD_SERVER_ADMIN if not already set export HTTPD_SERVER_ADMIN="${HTTPD_SERVER_ADMIN:-noreply@vcap.me}" -# Set template variables for rewrite tool - use absolute paths! -export HOME="${HOME:-/home/vcap/app}" -export WEBDIR="%s" -export LIBDIR="%s" -export PHP_FPM_LISTEN="127.0.0.1:9000" -export PHP_FPM_CONF_INCLUDE="%s" - echo "Starting PHP application with HTTPD..." echo "DEPS_DIR: $DEPS_DIR" -echo "WEBDIR: $WEBDIR" +echo "TMPDIR: $TMPDIR" echo "PHP-FPM: $DEPS_DIR/%s/php/sbin/php-fpm" echo "HTTPD: $DEPS_DIR/%s/httpd/bin/httpd" -echo "Checking if binaries exist..." -ls -la "$DEPS_DIR/%s/php/sbin/php-fpm" || echo "PHP-FPM not found!" -ls -la "$DEPS_DIR/%s/httpd/bin/httpd" || echo "HTTPD not found!" # Create symlinks for httpd files (httpd config expects them relative to ServerRoot) ln -sf "$DEPS_DIR/%s/httpd/modules" "$HOME/httpd/modules" ln -sf "$DEPS_DIR/%s/httpd/conf/mime.types" "$HOME/httpd/conf/mime.types" 2>/dev/null || \ touch "$HOME/httpd/conf/mime.types" -# Create httpd logs directory if it doesn't exist +# Create required directories mkdir -p "$HOME/httpd/logs" +mkdir -p "$DEPS_DIR/%s/php/var/run" +mkdir -p "$TMPDIR" + +# Expand ${TMPDIR} in PHP configs (php.ini uses ${TMPDIR} placeholder) +# This allows users to customize TMPDIR via environment variable +for config_file in "$PHPRC/php.ini" "$PHPRC/php-fpm.conf"; do + if [ -f "$config_file" ]; then + sed "s|\${TMPDIR}|$TMPDIR|g" "$config_file" > "$config_file.tmp" + mv "$config_file.tmp" "$config_file" + fi +done -# Run rewrite to update config with runtime values -$HOME/.bp/bin/rewrite "$HOME/httpd/conf" - -# Rewrite PHP base configs (php.ini, php-fpm.conf) with HOME=$DEPS_DIR/0 -# This ensures @{HOME} placeholders in extension_dir are replaced with correct deps path -OLD_HOME="$HOME" -export HOME="$DEPS_DIR/%s" -export DEPS_DIR -$OLD_HOME/.bp/bin/rewrite "$DEPS_DIR/%s/php/etc/php.ini" -$OLD_HOME/.bp/bin/rewrite "$DEPS_DIR/%s/php/etc/php-fpm.conf" -export HOME="$OLD_HOME" - -# Rewrite user fpm.d configs with HOME=/home/vcap/app -# User configs expect HOME to be the app directory, not deps directory -if [ -d "$DEPS_DIR/%s/php/etc/fpm.d" ]; then - $HOME/.bp/bin/rewrite "$DEPS_DIR/%s/php/etc/fpm.d" -fi - -# Rewrite php.ini.d configs with app HOME as well (may contain user overrides) -if [ -d "$DEPS_DIR/%s/php/etc/php.ini.d" ]; then - $HOME/.bp/bin/rewrite "$DEPS_DIR/%s/php/etc/php.ini.d" +# Also process php.ini.d directory if it exists +if [ -d "$PHP_INI_SCAN_DIR" ]; then + for config_file in "$PHP_INI_SCAN_DIR"/*.ini; do + if [ -f "$config_file" ]; then + sed "s|\${TMPDIR}|$TMPDIR|g" "$config_file" > "$config_file.tmp" + mv "$config_file.tmp" "$config_file" + fi + done fi -# Create PHP-FPM socket directory if it doesn't exist -mkdir -p "$DEPS_DIR/%s/php/var/run" - # Start PHP-FPM in background $DEPS_DIR/%s/php/sbin/php-fpm -F -y $PHPRC/php-fpm.conf & PHP_FPM_PID=$! @@ -446,7 +525,7 @@ HTTPD_PID=$! # Wait for both processes wait $PHP_FPM_PID $HTTPD_PID -`, depsIdx, depsIdx, depsIdx, webDir, libDir, phpFpmConfInclude, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx) +`, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx) } // generateNginxStartScript generates a start script for Nginx with PHP-FPM @@ -470,56 +549,56 @@ func (f *Finalizer) generateNginxStartScript(depsIdx string, opts *options.Optio set -e # Set DEPS_DIR with fallback for different environments -: ${DEPS_DIR:=$HOME/.cloudfoundry} +: ${DEPS_DIR:=/home/vcap/deps} export DEPS_DIR + +# Set TMPDIR with fallback (users can override via environment variable) +: ${TMPDIR:=/home/vcap/tmp} +export TMPDIR + export PHPRC="$DEPS_DIR/%s/php/etc" export PHP_INI_SCAN_DIR="$DEPS_DIR/%s/php/etc/php.ini.d" # Add PHP binaries to PATH for CLI commands (e.g., bin/cake migrations) export PATH="$DEPS_DIR/%s/php/bin:$PATH" -# Set template variables for rewrite tool - use absolute paths! -export HOME="${HOME:-/home/vcap/app}" -export WEBDIR="%s" -export LIBDIR="%s" -export PHP_FPM_LISTEN="127.0.0.1:9000" -export PHP_FPM_CONF_INCLUDE="" - echo "Starting PHP application with Nginx..." echo "DEPS_DIR: $DEPS_DIR" -echo "WEBDIR: $WEBDIR" +echo "TMPDIR: $TMPDIR" echo "PHP-FPM: $DEPS_DIR/%s/php/sbin/php-fpm" echo "Nginx: $DEPS_DIR/%s/nginx/sbin/nginx" -echo "Checking if binaries exist..." -ls -la "$DEPS_DIR/%s/php/sbin/php-fpm" || echo "PHP-FPM not found!" -ls -la "$DEPS_DIR/%s/nginx/sbin/nginx" || echo "Nginx not found!" -# Run rewrite to update config with runtime values -$HOME/.bp/bin/rewrite "$HOME/nginx/conf" - -# Rewrite PHP base configs (php.ini, php-fpm.conf) with HOME=$DEPS_DIR/0 -# This ensures @{HOME} placeholders in extension_dir are replaced with correct deps path -OLD_HOME="$HOME" -export HOME="$DEPS_DIR/%s" -export DEPS_DIR -$OLD_HOME/.bp/bin/rewrite "$DEPS_DIR/%s/php/etc/php.ini" -$OLD_HOME/.bp/bin/rewrite "$DEPS_DIR/%s/php/etc/php-fpm.conf" -export HOME="$OLD_HOME" - -# Rewrite user fpm.d configs with HOME=/home/vcap/app -# User configs expect HOME to be the app directory, not deps directory -if [ -d "$DEPS_DIR/%s/php/etc/fpm.d" ]; then - $HOME/.bp/bin/rewrite "$DEPS_DIR/%s/php/etc/fpm.d" -fi +# Substitute runtime variables in nginx config +# PORT is assigned by Cloud Foundry, TMPDIR can be customized by user +sed -e "s|\${PORT}|$PORT|g" -e "s|\${TMPDIR}|$TMPDIR|g" "$HOME/nginx/conf/server-defaults.conf" > "$HOME/nginx/conf/server-defaults.conf.tmp" +mv "$HOME/nginx/conf/server-defaults.conf.tmp" "$HOME/nginx/conf/server-defaults.conf" + +# Expand ${TMPDIR} in PHP configs (php.ini uses ${TMPDIR} placeholder) +# This allows users to customize TMPDIR via environment variable +for config_file in "$PHPRC/php.ini" "$PHPRC/php-fpm.conf"; do + if [ -f "$config_file" ]; then + sed "s|\${TMPDIR}|$TMPDIR|g" "$config_file" > "$config_file.tmp" + mv "$config_file.tmp" "$config_file" + fi +done -# Rewrite php.ini.d configs with app HOME as well (may contain user overrides) -if [ -d "$DEPS_DIR/%s/php/etc/php.ini.d" ]; then - $HOME/.bp/bin/rewrite "$DEPS_DIR/%s/php/etc/php.ini.d" +# Also process php.ini.d directory if it exists +if [ -d "$PHP_INI_SCAN_DIR" ]; then + for config_file in "$PHP_INI_SCAN_DIR"/*.ini; do + if [ -f "$config_file" ]; then + sed "s|\${TMPDIR}|$TMPDIR|g" "$config_file" > "$config_file.tmp" + mv "$config_file.tmp" "$config_file" + fi + done fi # Create required directories mkdir -p "$DEPS_DIR/%s/php/var/run" mkdir -p "$HOME/nginx/logs" +mkdir -p "$TMPDIR" +mkdir -p "$TMPDIR/nginx_fastcgi" +mkdir -p "$TMPDIR/nginx_client_body" +mkdir -p "$TMPDIR/nginx_proxy" # Start PHP-FPM in background $DEPS_DIR/%s/php/sbin/php-fpm -F -y $PHPRC/php-fpm.conf & @@ -531,7 +610,7 @@ NGINX_PID=$! # Wait for both processes wait $PHP_FPM_PID $NGINX_PID -`, depsIdx, depsIdx, depsIdx, webDir, libDir, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx) +`, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx) } // generatePHPFPMStartScript generates a start script for PHP-FPM only (no web server) @@ -555,38 +634,46 @@ func (f *Finalizer) generatePHPFPMStartScript(depsIdx string, opts *options.Opti set -e # Set DEPS_DIR with fallback for different environments -: ${DEPS_DIR:=$HOME/.cloudfoundry} +: ${DEPS_DIR:=/home/vcap/deps} export DEPS_DIR + +# Set TMPDIR with fallback (users can override via environment variable) +: ${TMPDIR:=/home/vcap/tmp} +export TMPDIR + export PHPRC="$DEPS_DIR/%s/php/etc" export PHP_INI_SCAN_DIR="$DEPS_DIR/%s/php/etc/php.ini.d" -# Set template variables for rewrite tool - use absolute paths! -export HOME="${HOME:-/home/vcap/app}" -export WEBDIR="%s" -export LIBDIR="%s" -export PHP_FPM_LISTEN="$DEPS_DIR/%s/php/var/run/php-fpm.sock" -export PHP_FPM_CONF_INCLUDE="" - echo "Starting PHP-FPM only..." echo "DEPS_DIR: $DEPS_DIR" -echo "WEBDIR: $WEBDIR" +echo "TMPDIR: $TMPDIR" echo "PHP-FPM path: $DEPS_DIR/%s/php/sbin/php-fpm" -ls -la "$DEPS_DIR/%s/php/sbin/php-fpm" || echo "PHP-FPM not found!" -# Temporarily set HOME to DEPS_DIR/0 for PHP config rewriting -# This ensures @{HOME} placeholders in extension_dir are replaced with the correct path -OLD_HOME="$HOME" -export HOME="$DEPS_DIR/%s" -export DEPS_DIR -$OLD_HOME/.bp/bin/rewrite "$DEPS_DIR/%s/php/etc" -export HOME="$OLD_HOME" +# Expand ${TMPDIR} in PHP configs +for config_file in "$PHPRC/php.ini" "$PHPRC/php-fpm.conf"; do + if [ -f "$config_file" ]; then + sed "s|\${TMPDIR}|$TMPDIR|g" "$config_file" > "$config_file.tmp" + mv "$config_file.tmp" "$config_file" + fi +done + +# Also process php.ini.d directory if it exists +if [ -d "$PHP_INI_SCAN_DIR" ]; then + for config_file in "$PHP_INI_SCAN_DIR"/*.ini; do + if [ -f "$config_file" ]; then + sed "s|\${TMPDIR}|$TMPDIR|g" "$config_file" > "$config_file.tmp" + mv "$config_file.tmp" "$config_file" + fi + done +fi # Create PHP-FPM socket directory if it doesn't exist mkdir -p "$DEPS_DIR/%s/php/var/run" +mkdir -p "$TMPDIR" # Start PHP-FPM in foreground exec $DEPS_DIR/%s/php/sbin/php-fpm -F -y $PHPRC/php-fpm.conf -`, depsIdx, depsIdx, webDir, libDir, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx) +`, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx) } // SetupProcessTypes creates the process types for the application @@ -611,6 +698,54 @@ func (f *Finalizer) SetupProcessTypes() error { return nil } +// replacePlaceholders replaces build-time placeholders in a file +func (f *Finalizer) replacePlaceholders(filePath string, replacements map[string]string) error { + content, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read file %s: %w", filePath, err) + } + + result := string(content) + + // Replace all placeholders + for placeholder, value := range replacements { + result = strings.ReplaceAll(result, placeholder, value) + } + + if err := os.WriteFile(filePath, []byte(result), 0644); err != nil { + return fmt.Errorf("failed to write file %s: %w", filePath, err) + } + + return nil +} + +// replacePlaceholdersInDir replaces placeholders in all files in a directory recursively +func (f *Finalizer) replacePlaceholdersInDir(dirPath string, replacements map[string]string) error { + return f.replacePlaceholdersInDirExclude(dirPath, replacements, nil) +} + +func (f *Finalizer) replacePlaceholdersInDirExclude(dirPath string, replacements map[string]string, excludeDirs []string) error { + return filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Skip directories, but check if we should skip their contents + if info.IsDir() { + // Check if this directory should be excluded + for _, exclude := range excludeDirs { + if path == exclude { + return filepath.SkipDir + } + } + return nil + } + + // Replace placeholders in this file + return f.replacePlaceholders(path, replacements) + }) +} + func (f *Finalizer) copyFile(src, dst string) error { sourceFile, err := os.Open(src) if err != nil { diff --git a/src/php/finalize/finalize_test.go b/src/php/finalize/finalize_test.go index 3167a2fd6..ff70f8458 100644 --- a/src/php/finalize/finalize_test.go +++ b/src/php/finalize/finalize_test.go @@ -6,6 +6,7 @@ import ( "io" "os" "path/filepath" + "runtime" "github.com/cloudfoundry/libbuildpack" "github.com/cloudfoundry/php-buildpack/src/php/finalize" @@ -38,11 +39,16 @@ var _ = Describe("Finalize", func() { buffer = new(bytes.Buffer) logger = libbuildpack.NewLogger(buffer) + + cwd, err := os.Getwd() + Expect(err).To(BeNil()) + os.Setenv("BP_DIR", filepath.Join(cwd, "..", "..", "..")) }) AfterEach(func() { Expect(os.RemoveAll(buildDir)).To(Succeed()) Expect(os.RemoveAll(depsDir)).To(Succeed()) + os.Unsetenv("BP_DIR") }) Describe("Stager interface", func() { @@ -237,6 +243,7 @@ var _ = Describe("Finalize", func() { manifest *testManifest stager *testStager command *testCommand + bpDir string ) BeforeEach(func() { @@ -257,13 +264,15 @@ var _ = Describe("Finalize", func() { command = &testCommand{} - // Set required environment variables - os.Setenv("BP_DIR", buildDir) + cwd, err := os.Getwd() + Expect(err).To(BeNil()) + bpDir = filepath.Join(cwd, "..", "..", "..") + os.Setenv("BP_DIR", bpDir) + os.Setenv("GoInstallDir", runtime.GOROOT()) }) Context("when web server is httpd", func() { It("creates HTTPD start script", func() { - // Create options.json with httpd optionsFile := filepath.Join(buildDir, ".bp-config", "options.json") err := os.MkdirAll(filepath.Dir(optionsFile), 0755) Expect(err).To(BeNil()) @@ -272,13 +281,6 @@ var _ = Describe("Finalize", func() { err = os.WriteFile(optionsFile, []byte(optionsJSON), 0644) Expect(err).To(BeNil()) - // Create rewrite binary source (empty file for test) - rewriteSrc := filepath.Join(buildDir, "bin", "rewrite") - err = os.MkdirAll(filepath.Dir(rewriteSrc), 0755) - Expect(err).To(BeNil()) - err = os.WriteFile(rewriteSrc, []byte("#!/bin/bash\n"), 0755) - Expect(err).To(BeNil()) - finalizer = &finalize.Finalizer{ Manifest: manifest, Stager: stager, @@ -289,11 +291,9 @@ var _ = Describe("Finalize", func() { err = finalizer.CreateStartScript() Expect(err).To(BeNil()) - // Verify start script was created startScript := filepath.Join(buildDir, ".bp", "bin", "start") Expect(startScript).To(BeAnExistingFile()) - // Verify script content contents, err := os.ReadFile(startScript) Expect(err).To(BeNil()) scriptContent := string(contents) @@ -305,7 +305,6 @@ var _ = Describe("Finalize", func() { Context("when web server is nginx", func() { It("creates Nginx start script", func() { - // Create options.json with nginx optionsFile := filepath.Join(buildDir, ".bp-config", "options.json") err := os.MkdirAll(filepath.Dir(optionsFile), 0755) Expect(err).To(BeNil()) @@ -314,13 +313,6 @@ var _ = Describe("Finalize", func() { err = os.WriteFile(optionsFile, []byte(optionsJSON), 0644) Expect(err).To(BeNil()) - // Create rewrite binary source - rewriteSrc := filepath.Join(buildDir, "bin", "rewrite") - err = os.MkdirAll(filepath.Dir(rewriteSrc), 0755) - Expect(err).To(BeNil()) - err = os.WriteFile(rewriteSrc, []byte("#!/bin/bash\n"), 0755) - Expect(err).To(BeNil()) - finalizer = &finalize.Finalizer{ Manifest: manifest, Stager: stager, @@ -331,11 +323,9 @@ var _ = Describe("Finalize", func() { err = finalizer.CreateStartScript() Expect(err).To(BeNil()) - // Verify start script was created startScript := filepath.Join(buildDir, ".bp", "bin", "start") Expect(startScript).To(BeAnExistingFile()) - // Verify script content contents, err := os.ReadFile(startScript) Expect(err).To(BeNil()) scriptContent := string(contents) @@ -347,7 +337,6 @@ var _ = Describe("Finalize", func() { Context("when web server is none", func() { It("creates PHP-FPM only start script", func() { - // Create options.json with none (PHP-FPM only) optionsFile := filepath.Join(buildDir, ".bp-config", "options.json") err := os.MkdirAll(filepath.Dir(optionsFile), 0755) Expect(err).To(BeNil()) @@ -356,13 +345,6 @@ var _ = Describe("Finalize", func() { err = os.WriteFile(optionsFile, []byte(optionsJSON), 0644) Expect(err).To(BeNil()) - // Create rewrite binary source - rewriteSrc := filepath.Join(buildDir, "bin", "rewrite") - err = os.MkdirAll(filepath.Dir(rewriteSrc), 0755) - Expect(err).To(BeNil()) - err = os.WriteFile(rewriteSrc, []byte("#!/bin/bash\n"), 0755) - Expect(err).To(BeNil()) - finalizer = &finalize.Finalizer{ Manifest: manifest, Stager: stager, @@ -373,11 +355,9 @@ var _ = Describe("Finalize", func() { err = finalizer.CreateStartScript() Expect(err).To(BeNil()) - // Verify start script was created startScript := filepath.Join(buildDir, ".bp", "bin", "start") Expect(startScript).To(BeAnExistingFile()) - // Verify script content contents, err := os.ReadFile(startScript) Expect(err).To(BeNil()) scriptContent := string(contents) @@ -404,21 +384,6 @@ var _ = Describe("Finalize", func() { Expect(err.Error()).To(ContainSubstring("BP_DIR")) }) }) - - Context("when rewrite binary doesn't exist in bin/", func() { - It("returns an error", func() { - finalizer = &finalize.Finalizer{ - Manifest: manifest, - Stager: stager, - Command: command, - Log: logger, - } - - err = finalizer.CreateStartScript() - Expect(err).NotTo(BeNil()) - Expect(err.Error()).To(ContainSubstring("rewrite")) - }) - }) }) Describe("Start script file creation", func() { @@ -441,13 +406,11 @@ var _ = Describe("Finalize", func() { Log: logger, } - // Set BP_DIR and create necessary files - os.Setenv("BP_DIR", buildDir) - rewriteSrc := filepath.Join(buildDir, "bin", "rewrite") - err = os.MkdirAll(filepath.Dir(rewriteSrc), 0755) - Expect(err).To(BeNil()) - err = os.WriteFile(rewriteSrc, []byte("#!/bin/bash\n"), 0755) + cwd, err := os.Getwd() Expect(err).To(BeNil()) + bpDir := filepath.Join(cwd, "..", "..", "..") + os.Setenv("BP_DIR", bpDir) + os.Setenv("GoInstallDir", runtime.GOROOT()) optionsFile := filepath.Join(buildDir, ".bp-config", "options.json") err = os.MkdirAll(filepath.Dir(optionsFile), 0755) @@ -458,53 +421,9 @@ var _ = Describe("Finalize", func() { err = finalizer.CreateStartScript() Expect(err).To(BeNil()) - // Verify directory structure bpBinDir := filepath.Join(buildDir, ".bp", "bin") Expect(bpBinDir).To(BeADirectory()) }) - - It("copies pre-compiled rewrite binary to .bp/bin", func() { - stager := &testStager{ - buildDir: buildDir, - depsDir: depsDir, - depsIdx: depsIdx, - } - - manifest := &testManifest{ - versions: map[string][]string{"php": {"8.1.32"}}, - defaults: map[string]string{"php": "8.1.32"}, - } - - finalizer = &finalize.Finalizer{ - Manifest: manifest, - Stager: stager, - Command: &testCommand{}, - Log: logger, - } - - os.Setenv("BP_DIR", buildDir) - rewriteSrc := filepath.Join(buildDir, "bin", "rewrite") - err = os.MkdirAll(filepath.Dir(rewriteSrc), 0755) - Expect(err).To(BeNil()) - err = os.WriteFile(rewriteSrc, []byte("#!/bin/bash\necho test rewrite\n"), 0755) - Expect(err).To(BeNil()) - - optionsFile := filepath.Join(buildDir, ".bp-config", "options.json") - err = os.MkdirAll(filepath.Dir(optionsFile), 0755) - Expect(err).To(BeNil()) - err = os.WriteFile(optionsFile, []byte(`{"WEB_SERVER": "httpd"}`), 0644) - Expect(err).To(BeNil()) - - err = finalizer.CreateStartScript() - Expect(err).To(BeNil()) - - rewriteDst := filepath.Join(buildDir, ".bp", "bin", "rewrite") - Expect(rewriteDst).To(BeAnExistingFile()) - - contents, err := os.ReadFile(rewriteDst) - Expect(err).To(BeNil()) - Expect(string(contents)).To(ContainSubstring("echo test rewrite")) - }) }) Describe("Service commands and environment", func() { @@ -578,12 +497,11 @@ var _ = Describe("Finalize", func() { Log: logger, } - os.Setenv("BP_DIR", buildDir) - rewriteSrc := filepath.Join(buildDir, "bin", "rewrite") - err = os.MkdirAll(filepath.Dir(rewriteSrc), 0755) - Expect(err).To(BeNil()) - err = os.WriteFile(rewriteSrc, []byte("#!/bin/bash\n"), 0755) + cwd, err := os.Getwd() Expect(err).To(BeNil()) + bpDir := filepath.Join(cwd, "..", "..", "..") + os.Setenv("BP_DIR", bpDir) + os.Setenv("GoInstallDir", runtime.GOROOT()) // Create options with custom WEBDIR optionsFile := filepath.Join(buildDir, ".bp-config", "options.json") @@ -622,12 +540,11 @@ var _ = Describe("Finalize", func() { Log: logger, } - os.Setenv("BP_DIR", buildDir) - rewriteSrc := filepath.Join(buildDir, "bin", "rewrite") - err = os.MkdirAll(filepath.Dir(rewriteSrc), 0755) - Expect(err).To(BeNil()) - err = os.WriteFile(rewriteSrc, []byte("#!/bin/bash\n"), 0755) + cwd, err := os.Getwd() Expect(err).To(BeNil()) + bpDir := filepath.Join(cwd, "..", "..", "..") + os.Setenv("BP_DIR", bpDir) + os.Setenv("GoInstallDir", runtime.GOROOT()) optionsFile := filepath.Join(buildDir, ".bp-config", "options.json") err = os.MkdirAll(filepath.Dir(optionsFile), 0755) diff --git a/src/php/integration/modules_test.go b/src/php/integration/modules_test.go index 6a5b02ceb..b61e41650 100644 --- a/src/php/integration/modules_test.go +++ b/src/php/integration/modules_test.go @@ -116,14 +116,16 @@ func testModules(platform switchblade.Platform, fixtures string) func(*testing.T }) context("app with custom conf files in php.ini.d dir in app root", func() { - it("app sets custom conf", func() { + it("app sets custom conf and replaces placeholders", func() { deployment, _, err := platform.Deploy. Execute(name, filepath.Join(fixtures, "php_with_php_ini_d")) Expect(err).NotTo(HaveOccurred()) - Eventually(deployment).Should(Serve( + Eventually(deployment).Should(Serve(SatisfyAll( ContainSubstring("teststring"), - )) + // Verify @{HOME} was replaced with /home/vcap/app in include_path + ContainSubstring("/home/vcap/app/lib"), + ))) }) }) diff --git a/src/php/rewrite/cli/main.go b/src/php/rewrite/cli/main.go deleted file mode 100644 index 395d78651..000000000 --- a/src/php/rewrite/cli/main.go +++ /dev/null @@ -1,198 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - "log" - "os" - "path/filepath" - "strings" -) - -// rewriteFile replaces template patterns in a file with environment variable values -// Supports: @{VAR}, #{VAR}, @VAR@, and #VAR patterns -func rewriteFile(filePath string) error { - // Read the file - content, err := ioutil.ReadFile(filePath) - if err != nil { - return fmt.Errorf("failed to read file %s: %w", filePath, err) - } - - result := string(content) - - // Replace patterns with braces: @{VAR} and #{VAR} - result = replacePatterns(result, "@{", "}") - result = replacePatterns(result, "#{", "}") - - // Replace patterns without braces: @VAR@ and #VAR (word boundary after) - result = replaceSimplePatterns(result, "@", "@") - result = replaceSimplePatterns(result, "#", "") - - // Write back to file - err = ioutil.WriteFile(filePath, []byte(result), 0644) - if err != nil { - return fmt.Errorf("failed to write file %s: %w", filePath, err) - } - - return nil -} - -// replacePatterns replaces all occurrences of startDelim + VAR + endDelim with env var values -func replacePatterns(content, startDelim, endDelim string) string { - result := content - pos := 0 - - for pos < len(result) { - start := strings.Index(result[pos:], startDelim) - if start == -1 { - break - } - start += pos - - end := strings.Index(result[start+len(startDelim):], endDelim) - if end == -1 { - // No matching end delimiter, skip this start delimiter - pos = start + len(startDelim) - continue - } - end += start + len(startDelim) - - // Extract variable name - varName := result[start+len(startDelim) : end] - - // Get environment variable value - varValue := os.Getenv(varName) - - // Replace the pattern (keep pattern if variable not found - safe_substitute behavior) - if varValue != "" { - result = result[:start] + varValue + result[end+len(endDelim):] - pos = start + len(varValue) - } else { - // Keep the pattern and continue searching after it - pos = end + len(endDelim) - } - } - - return result -} - -// replaceSimplePatterns replaces patterns like @VAR@ or #VAR (without braces) -// For #VAR patterns, endDelim is empty and we match until a non-alphanumeric/underscore character -func replaceSimplePatterns(content, startDelim, endDelim string) string { - result := content - pos := 0 - - for pos < len(result) { - start := strings.Index(result[pos:], startDelim) - if start == -1 { - break - } - start += pos - - // Find the end of the variable name - varStart := start + len(startDelim) - varEnd := varStart - - if endDelim != "" { - // Pattern like @VAR@ - find matching end delimiter - end := strings.Index(result[varStart:], endDelim) - if end == -1 { - pos = varStart - continue - } - varEnd = varStart + end - } else { - // Pattern like #VAR - match until non-alphanumeric/underscore - for varEnd < len(result) { - c := result[varEnd] - if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') { - break - } - varEnd++ - } - - // If we didn't match any characters, skip this delimiter - if varEnd == varStart { - pos = varStart - continue - } - } - - // Extract variable name - varName := result[varStart:varEnd] - - // Skip if variable name is empty - if varName == "" { - pos = varStart - continue - } - - // Get environment variable value - varValue := os.Getenv(varName) - - // Replace the pattern (keep pattern if variable not found - safe_substitute behavior) - if varValue != "" { - endPos := varEnd - if endDelim != "" { - endPos = varEnd + len(endDelim) - } - result = result[:start] + varValue + result[endPos:] - pos = start + len(varValue) - } else { - // Keep the pattern and continue searching after it - pos = varEnd - if endDelim != "" { - pos += len(endDelim) - } - } - } - - return result -} - -// rewriteConfigsRecursive walks a directory and rewrites all files -func rewriteConfigsRecursive(dirPath string) error { - return filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // Skip directories - if info.IsDir() { - return nil - } - - log.Printf("Rewriting config file: %s", path) - return rewriteFile(path) - }) -} - -func main() { - if len(os.Args) != 2 { - fmt.Fprintln(os.Stderr, "Argument required! Specify path to configuration directory.") - os.Exit(1) - } - - toPath := os.Args[1] - - // Check if path exists - info, err := os.Stat(toPath) - if err != nil { - fmt.Fprintf(os.Stderr, "Path [%s] not found.\n", toPath) - os.Exit(1) - } - - // Process directory or single file - if info.IsDir() { - log.Printf("Rewriting configuration under [%s]", toPath) - err = rewriteConfigsRecursive(toPath) - } else { - log.Printf("Rewriting configuration file [%s]", toPath) - err = rewriteFile(toPath) - } - - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } -} diff --git a/src/php/supply/supply.go b/src/php/supply/supply.go index 5fa46ec7a..756881cba 100644 --- a/src/php/supply/supply.go +++ b/src/php/supply/supply.go @@ -412,7 +412,7 @@ func (s *Supplier) InstallPHP() error { } // Process php.ini to replace build-time extension placeholders only - // Runtime placeholders (@{HOME}, etc.) will be replaced by the rewrite tool in start script + // Runtime placeholders (@{HOME}, etc.) are replaced during finalize phase phpIniPath := filepath.Join(phpEtcDir, "php.ini") if err := s.processPhpIni(phpIniPath); err != nil { return fmt.Errorf("failed to process php.ini: %w", err) @@ -431,7 +431,7 @@ func (s *Supplier) InstallPHP() error { } // Note: User's .bp-config/php/fpm.d/*.conf files are already copied by copyUserConfigs() above - // They will be processed by the rewrite tool at runtime (in start script) + // They will be processed during the finalize phase (build-time placeholder replacement) return nil } @@ -495,7 +495,7 @@ func (s *Supplier) processPhpFpmConf(phpFpmConfPath, phpEtcDir string) error { // Set the include directive based on whether user has fpm.d configs var includeDirective string if hasFpmDConfigs { - // Use DEPS_DIR with dynamic index which will be replaced by rewrite tool at runtime + // Use DEPS_DIR with dynamic index which will be replaced during finalize phase depsIdx := s.Stager.DepsIdx() includeDirective = fmt.Sprintf("include=@{DEPS_DIR}/%s/php/etc/fpm.d/*.conf", depsIdx) s.Log.Info("Enabling fpm.d config includes") @@ -505,7 +505,7 @@ func (s *Supplier) processPhpFpmConf(phpFpmConfPath, phpEtcDir string) error { } // Replace the placeholder - phpFpmConfContent = strings.ReplaceAll(phpFpmConfContent, "#{PHP_FPM_CONF_INCLUDE}", includeDirective) + phpFpmConfContent = strings.ReplaceAll(phpFpmConfContent, "@{PHP_FPM_CONF_INCLUDE}", includeDirective) // Write back to php-fpm.conf if err := os.WriteFile(phpFpmConfPath, []byte(phpFpmConfContent), 0644); err != nil { @@ -516,16 +516,16 @@ func (s *Supplier) processPhpFpmConf(phpFpmConfPath, phpEtcDir string) error { } // createIncludePathIni creates a separate include-path.ini file in php.ini.d -// This file uses @{HOME} placeholder which gets rewritten AFTER HOME is restored -// to /home/vcap/app, avoiding the issue where php.ini gets rewritten while HOME -// points to the deps directory +// This file uses @{HOME} placeholder which gets replaced during finalize phase with /home/vcap/app +// (app context, not deps context). The php.ini.d directory is processed separately from other +// PHP configs because it contains app-relative paths like include_path. func (s *Supplier) createIncludePathIni(phpIniDDir string) error { includePathIniPath := filepath.Join(phpIniDDir, "include-path.ini") - // Use @{HOME} placeholder which will be replaced by rewrite tool at runtime - // after HOME is restored to /home/vcap/app + // Use @{HOME} placeholder which will be replaced during finalize phase + // with /home/vcap/app (app context) content := `; Include path configuration -; This file is rewritten at runtime after HOME is restored to /home/vcap/app +; This file is processed during finalize phase with @{HOME} = /home/vcap/app include_path = ".:/usr/share/php:@{HOME}/lib" ` diff --git a/src/php/supply/supply_test.go b/src/php/supply/supply_test.go index e41f1b5f9..47bd96108 100644 --- a/src/php/supply/supply_test.go +++ b/src/php/supply/supply_test.go @@ -340,7 +340,7 @@ var _ = Describe("Supply", func() { Expect(os.WriteFile(testConfPath, []byte("[test]\nlisten = 9001\n"), 0644)).To(Succeed()) phpFpmConfPath := filepath.Join(phpEtcDir, "php-fpm.conf") - phpFpmConfContent := "[global]\npid = /tmp/php-fpm.pid\n\n#{PHP_FPM_CONF_INCLUDE}\n\n[www]\nlisten = 9000\n" + phpFpmConfContent := "[global]\npid = /tmp/php-fpm.pid\n\n@{PHP_FPM_CONF_INCLUDE}\n\n[www]\nlisten = 9000\n" Expect(os.WriteFile(phpFpmConfPath, []byte(phpFpmConfContent), 0644)).To(Succeed()) err = supplier.ProcessPhpFpmConfForTesting(phpFpmConfPath, phpEtcDir) @@ -351,7 +351,7 @@ var _ = Describe("Supply", func() { Expect(string(content)).To(ContainSubstring("include=@{DEPS_DIR}/13/php/etc/fpm.d/*.conf")) Expect(string(content)).NotTo(ContainSubstring("@{DEPS_DIR}/0/")) - Expect(string(content)).NotTo(ContainSubstring("#{PHP_FPM_CONF_INCLUDE}")) + Expect(string(content)).NotTo(ContainSubstring("@{PHP_FPM_CONF_INCLUDE}")) }) It("removes include directive when no user fpm.d configs exist", func() { @@ -371,7 +371,7 @@ var _ = Describe("Supply", func() { Expect(os.MkdirAll(phpEtcDir, 0755)).To(Succeed()) phpFpmConfPath := filepath.Join(phpEtcDir, "php-fpm.conf") - phpFpmConfContent := "[global]\npid = /tmp/php-fpm.pid\n\n#{PHP_FPM_CONF_INCLUDE}\n\n[www]\nlisten = 9000\n" + phpFpmConfContent := "[global]\npid = /tmp/php-fpm.pid\n\n@{PHP_FPM_CONF_INCLUDE}\n\n[www]\nlisten = 9000\n" Expect(os.WriteFile(phpFpmConfPath, []byte(phpFpmConfContent), 0644)).To(Succeed()) err = supplier.ProcessPhpFpmConfForTesting(phpFpmConfPath, phpEtcDir) @@ -381,7 +381,7 @@ var _ = Describe("Supply", func() { Expect(err).To(BeNil()) Expect(string(content)).NotTo(ContainSubstring("include=")) - Expect(string(content)).NotTo(ContainSubstring("#{PHP_FPM_CONF_INCLUDE}")) + Expect(string(content)).NotTo(ContainSubstring("@{PHP_FPM_CONF_INCLUDE}")) }) }) From e0af47e88179c4b886d0ffb32045b0f92f6f29bc Mon Sep 17 00:00:00 2001 From: ramonskie Date: Thu, 29 Jan 2026 13:18:59 +0100 Subject: [PATCH 2/4] Improve test infrastructure and cleanup This commit improves the integration test infrastructure with better cleanup, more robust regex matching, and proper handling of unsupported features. Changes: - Fix integration test regex patterns for more reliable matching - Skip custom extensions test (feature not yet supported in v5.x) - Cleanup buildpack files after integration tests to prevent disk space issues Test Infrastructure Improvements: - Add buildpack cleanup in init_test.go to remove uploaded buildpacks after tests - Refactor default_test.go regex patterns for better readability - Add explicit skip for custom extensions with clear explanation These changes improve test reliability and reduce CI/CD resource usage by properly cleaning up test artifacts. --- src/php/integration/default_test.go | 40 ++++++++---------- src/php/integration/init_test.go | 16 +------- src/php/integration/python_extension_test.go | 43 +++++++++++++++++++- 3 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/php/integration/default_test.go b/src/php/integration/default_test.go index baf965a96..5212245f4 100644 --- a/src/php/integration/default_test.go +++ b/src/php/integration/default_test.go @@ -44,22 +44,16 @@ func testDefault(platform switchblade.Platform, fixtures string) func(*testing.T "BP_DEBUG": "1", }). Execute(name, filepath.Join(fixtures, "default")) - Expect(err).NotTo(HaveOccurred()) + Expect(err).NotTo(HaveOccurred(), logs.String) - Eventually(logs).Should(SatisfyAll( - ContainLines("Installing PHP"), - ContainLines(MatchRegexp(`PHP [\d\.]+`)), - ContainSubstring(`"update_default_version" is setting [PHP_VERSION]`), - ContainSubstring("DEBUG: default_version_for composer is"), + Expect(logs).To(ContainLines(MatchRegexp(`Installing PHP [\d\.]+`))) + Expect(logs).To(ContainSubstring("PHP buildpack supply phase complete")) - Not(ContainSubstring("WARNING: A version of PHP has been specified in both `composer.json` and `./bp-config/options.json`.")), - Not(ContainSubstring("WARNING: The version defined in `composer.json` will be used.")), - )) + Expect(logs).NotTo(ContainSubstring("WARNING: A version of PHP has been specified in both `composer.json` and `./bp-config/options.json`.")) + Expect(logs).NotTo(ContainSubstring("WARNING: The version defined in `composer.json` will be used.")) if settings.Cached { - Eventually(logs).Should( - ContainLines(MatchRegexp(`Downloaded \[file://.*/dependencies/https___buildpacks.cloudfoundry.org_dependencies_php_php.*_linux_x64_.*.tgz\] to \[/tmp\]`)), - ) + Expect(logs).To(ContainLines(MatchRegexp(`Copy \[.*/dependencies/.*/php_[\d\.]+_linux_x64_.*\.tgz\]`))) } Eventually(deployment).Should(Serve( @@ -76,18 +70,20 @@ func testDefault(platform switchblade.Platform, fixtures string) func(*testing.T context("PHP web app with a supply buildpack", func() { it("builds and runs the app", func() { + if settings.Platform == "docker" { + t.Skip("Git URL buildpacks require CF platform - Docker platform cannot clone git repos") + } + deployment, logs, err := platform.Deploy. - WithBuildpacks("dotnet_core_buildpack", "php_buildpack"). + WithBuildpacks("https://github.com/cloudfoundry/dotnet-core-buildpack#master", "php_buildpack"). Execute(name, filepath.Join(fixtures, "dotnet_core_as_supply_app")) - Expect(err).NotTo(HaveOccurred()) + Expect(err).NotTo(HaveOccurred(), logs.String) - Eventually(logs).Should(SatisfyAll( - ContainSubstring("Supplying Dotnet Core"), - )) + Expect(logs).To(ContainSubstring("Supplying Dotnet Core"), logs.String) Eventually(deployment).Should(Serve( MatchRegexp(`dotnet: \d+\.\d+\.\d+`), - )) + ), logs.String) }) }) @@ -98,7 +94,7 @@ func testDefault(platform switchblade.Platform, fixtures string) func(*testing.T } deployment, logs, err := platform.Deploy. - WithBuildpacks("https://github.com/cloudfoundry/php-buildpack.git"). + WithBuildpacks("https://github.com/cloudfoundry/php-buildpack.git#fix-rewrite-binary-compilation"). WithEnv(map[string]string{ "BP_DEBUG": "1", }). @@ -106,10 +102,8 @@ func testDefault(platform switchblade.Platform, fixtures string) func(*testing.T Expect(err).NotTo(HaveOccurred(), logs.String) - Eventually(logs).Should(SatisfyAll( - ContainLines("Installing PHP"), - ContainLines(MatchRegexp(`PHP [\d\.]+`)), - )) + Expect(logs).To(ContainLines(MatchRegexp(`Installing PHP [\d\.]+`))) + Expect(logs).To(ContainSubstring("PHP buildpack supply phase complete")) Eventually(deployment).Should(Serve( ContainSubstring("PHP Version"), diff --git a/src/php/integration/init_test.go b/src/php/integration/init_test.go index e6310cbb6..eda87aa45 100644 --- a/src/php/integration/init_test.go +++ b/src/php/integration/init_test.go @@ -58,16 +58,6 @@ func TestIntegration(t *testing.T) { Name: "php_buildpack", URI: os.Getenv("BUILDPACK_FILE"), }, - // Go buildpack is needed for dynatrace tests - TEMPORARILY COMMENTED OUT - // switchblade.Buildpack{ - // Name: "go_buildpack", - // URI: "https://github.com/cloudfoundry/go-buildpack/archive/master.zip", - // }, - // .NET Core buildpack is needed for the supply test - TEMPORARILY COMMENTED OUT - // switchblade.Buildpack{ - // Name: "dotnet_core_buildpack", - // URI: "https://github.com/cloudfoundry/dotnet-core-buildpack/archive/master.zip", - // }, ) Expect(err).NotTo(HaveOccurred()) @@ -80,7 +70,7 @@ func TestIntegration(t *testing.T) { // Expect(err).NotTo(HaveOccurred()) suite := spec.New("integration", spec.Report(report.Terminal{}), spec.Parallel()) - // suite("Default", testDefault(platform, fixtures)) // Uses dotnet_core_buildpack - skipped + suite("Default", testDefault(platform, fixtures)) suite("Modules", testModules(platform, fixtures)) suite("Composer", testComposer(platform, fixtures)) suite("WebServers", testWebServers(platform, fixtures)) @@ -93,8 +83,6 @@ func TestIntegration(t *testing.T) { suite.Run(t) - // Expect(platform.Delete.Execute(dynatraceName)).To(Succeed()) // No dynatrace deployment to delete - // Commenting out buildpack.zip removal for testing - prevents parallel test failures - // Expect(os.Remove(os.Getenv("BUILDPACK_FILE"))).To(Succeed()) + Expect(os.Remove(os.Getenv("BUILDPACK_FILE"))).To(Succeed()) Expect(platform.Deinitialize()).To(Succeed()) } diff --git a/src/php/integration/python_extension_test.go b/src/php/integration/python_extension_test.go index 66c1feaff..78419e83a 100644 --- a/src/php/integration/python_extension_test.go +++ b/src/php/integration/python_extension_test.go @@ -7,6 +7,7 @@ import ( "github.com/cloudfoundry/switchblade" "github.com/sclevine/spec" + . "github.com/cloudfoundry/switchblade/matchers" . "github.com/onsi/gomega" ) @@ -36,7 +37,25 @@ func testPythonExtension(platform switchblade.Platform, fixtures string) func(*t }) context("app with buildpack-supported custom extension in python", func() { - it("builds and runs the app", func() { + it.Pend("builds and runs the app", func() { + // NOTE: Python-based user extensions (.extensions//extension.py) are NOT supported + // in the Go-based v5 buildpack. The Python extension system allowed arbitrary code execution + // and complex build-time operations (downloading binaries, file manipulation, etc). + // + // The v5 buildpack provides JSON-based user extensions instead (.extensions//extension.json) + // which support: + // - preprocess_commands: Run shell commands at container startup + // - service_commands: Long-running background processes + // - service_environment: Environment variables + // + // JSON extensions are simpler, more secure, and sufficient for most use cases. + // For complex build-time operations (like installing PHPMyAdmin), users should: + // 1. Use a multi-buildpack approach with separate buildpacks for each component + // 2. Include pre-built binaries in the app repository + // 3. Use preprocess_commands to download/setup at runtime (if acceptable) + // + // See docs/user-extensions.md for JSON extension documentation. + // See fixtures/json_extension for a working example. _, logs, err := platform.Deploy. Execute(name, filepath.Join(fixtures, "python_extension")) Expect(err).NotTo(HaveOccurred()) @@ -47,5 +66,27 @@ func testPythonExtension(platform switchblade.Platform, fixtures string) func(*t }) }) + context("app with JSON-based user extension", func() { + it("loads and runs the extension", func() { + deployment, logs, err := platform.Deploy. + WithEnv(map[string]string{ + "BP_DEBUG": "1", + }). + Execute(name, filepath.Join(fixtures, "json_extension")) + Expect(err).NotTo(HaveOccurred(), logs.String) + + // Verify user extension was loaded during staging + Expect(logs).To(ContainSubstring("Loaded user extension: myapp-initializer")) + + // Verify the app runs and shows extension effects + Eventually(deployment).Should(Serve(SatisfyAll( + ContainSubstring("JSON Extension Test"), + ContainSubstring("Extension Loaded: YES"), + ContainSubstring("Extension Version: 1.0.0"), + ContainSubstring("Marker File: myapp-extension-loaded"), + ))) + }) + }) + } } From cb30492ab9e274bff35d58393cde3eb5b325c3c2 Mon Sep 17 00:00:00 2001 From: ramonskie Date: Tue, 24 Feb 2026 12:43:37 +0100 Subject: [PATCH 3/4] Fix Go 1.26 build compatibility: remove bin stubs before go build Go 1.26 refuses to overwrite non-object files with 'go build -o'. Add 'rm -f "${output}"' before each go build invocation so the shell script stubs in bin/ are removed first. --- scripts/build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build.sh b/scripts/build.sh index f4af4728a..d459bf2cc 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -30,6 +30,7 @@ function main() { fi echo "-----> Building ${name} for ${os}" + rm -f "${output}" CGO_ENABLED=0 \ GOOS="${os}" \ go build \ From 9ec8975bd12843914b6f34d09956b2a424027856 Mon Sep 17 00:00:00 2001 From: ramonskie Date: Wed, 25 Feb 2026 17:15:33 +0100 Subject: [PATCH 4/4] fix: embed WEBDIR into generated start scripts The generate*StartScript functions read webDir from options but never interpolated it into the script body. Unit tests for custom and default WEBDIR were failing because the variable was never written out. Add WEBDIR="%s" line to all three generators (HTTPD, Nginx, PHP-FPM) and pass webDir as the first fmt.Sprintf argument. --- src/php/finalize/finalize.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/php/finalize/finalize.go b/src/php/finalize/finalize.go index 851921fa2..da3d3ec2d 100644 --- a/src/php/finalize/finalize.go +++ b/src/php/finalize/finalize.go @@ -471,6 +471,8 @@ export DEPS_DIR : ${TMPDIR:=/home/vcap/tmp} export TMPDIR +WEBDIR="%s" + export PHPRC="$DEPS_DIR/%s/php/etc" export PHP_INI_SCAN_DIR="$DEPS_DIR/%s/php/etc/php.ini.d" @@ -525,7 +527,7 @@ HTTPD_PID=$! # Wait for both processes wait $PHP_FPM_PID $HTTPD_PID -`, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx) +`, webDir, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx) } // generateNginxStartScript generates a start script for Nginx with PHP-FPM @@ -556,6 +558,8 @@ export DEPS_DIR : ${TMPDIR:=/home/vcap/tmp} export TMPDIR +WEBDIR="%s" + export PHPRC="$DEPS_DIR/%s/php/etc" export PHP_INI_SCAN_DIR="$DEPS_DIR/%s/php/etc/php.ini.d" @@ -610,7 +614,7 @@ NGINX_PID=$! # Wait for both processes wait $PHP_FPM_PID $NGINX_PID -`, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx) +`, webDir, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx) } // generatePHPFPMStartScript generates a start script for PHP-FPM only (no web server) @@ -641,6 +645,8 @@ export DEPS_DIR : ${TMPDIR:=/home/vcap/tmp} export TMPDIR +WEBDIR="%s" + export PHPRC="$DEPS_DIR/%s/php/etc" export PHP_INI_SCAN_DIR="$DEPS_DIR/%s/php/etc/php.ini.d" @@ -673,7 +679,7 @@ mkdir -p "$TMPDIR" # Start PHP-FPM in foreground exec $DEPS_DIR/%s/php/sbin/php-fpm -F -y $PHPRC/php-fpm.conf -`, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx) +`, webDir, depsIdx, depsIdx, depsIdx, depsIdx, depsIdx) } // SetupProcessTypes creates the process types for the application