2023-03-18 17:32:21 +08:00
< ? php
declare ( strict_types = 1 );
namespace SPC\command ;
use SPC\builder\BuilderProvider ;
2025-08-11 10:48:48 +08:00
use SPC\exception\ExceptionHandler ;
2025-03-08 14:29:44 +08:00
use SPC\store\Config ;
2023-10-14 11:33:17 +08:00
use SPC\store\FileSystem ;
2023-08-02 22:14:45 +08:00
use SPC\store\SourcePatcher ;
2023-03-18 17:32:21 +08:00
use SPC\util\DependencyUtil ;
2024-04-07 15:52:24 +08:00
use SPC\util\GlobalEnvManager ;
2023-04-15 18:46:46 +08:00
use SPC\util\LicenseDumper ;
2025-06-28 16:36:05 +08:00
use SPC\util\SPCTarget ;
2023-04-22 17:45:43 +08:00
use Symfony\Component\Console\Attribute\AsCommand ;
2023-03-18 17:32:21 +08:00
use Symfony\Component\Console\Input\InputArgument ;
use Symfony\Component\Console\Input\InputOption ;
2023-04-23 20:31:58 +08:00
use ZM\Logger\ConsoleColor ;
2023-03-18 17:32:21 +08:00
2024-05-16 10:51:31 +08:00
#[AsCommand('build', 'build PHP', ['build:php'])]
2025-03-14 18:22:50 +08:00
class BuildPHPCommand extends BuildCommand
2023-03-18 17:32:21 +08:00
{
2023-08-20 19:51:45 +08:00
public function configure () : void
2023-03-18 17:32:21 +08:00
{
2024-10-18 14:46:00 +02:00
$isWindows = PHP_OS_FAMILY === 'Windows' ;
2023-03-18 17:32:21 +08:00
$this -> addArgument ( 'extensions' , InputArgument :: REQUIRED , 'The extensions will be compiled, comma separated' );
$this -> addOption ( 'with-libs' , null , InputOption :: VALUE_REQUIRED , 'add additional libraries, comma separated' , '' );
2025-03-26 12:39:55 +08:00
$this -> addOption ( 'build-shared' , 'D' , InputOption :: VALUE_REQUIRED , 'Shared extensions to build, comma separated' , '' );
2024-02-06 16:06:09 +08:00
$this -> addOption ( 'build-micro' , null , null , 'Build micro SAPI' );
$this -> addOption ( 'build-cli' , null , null , 'Build cli SAPI' );
2025-03-14 18:22:50 +08:00
$this -> addOption ( 'build-fpm' , null , null , 'Build fpm SAPI (not available on Windows)' );
2025-06-20 01:57:45 +08:00
$this -> addOption ( 'build-embed' , null , null , 'Build embed SAPI (not available on Windows)' );
2025-06-18 10:48:09 +07:00
$this -> addOption ( 'build-frankenphp' , null , null , 'Build FrankenPHP SAPI (not available on Windows)' );
2025-10-16 00:55:21 +08:00
$this -> addOption ( 'build-cgi' , null , null , 'Build cgi SAPI' );
2024-02-06 16:06:09 +08:00
$this -> addOption ( 'build-all' , null , null , 'Build all SAPI' );
2025-06-19 19:31:25 +08:00
$this -> addOption ( 'no-strip' , null , null , 'build without strip, keep symbols to debug' );
2023-10-16 14:05:07 +02:00
$this -> addOption ( 'disable-opcache-jit' , null , null , 'disable opcache jit' );
2024-10-18 14:46:00 +02:00
$this -> addOption ( 'with-config-file-path' , null , InputOption :: VALUE_REQUIRED , 'Set the path in which to look for php.ini' , $isWindows ? null : '/usr/local/etc/php' );
$this -> addOption ( 'with-config-file-scan-dir' , null , InputOption :: VALUE_REQUIRED , 'Set the directory to scan for .ini files after reading php.ini' , $isWindows ? null : '/usr/local/etc/php/conf.d' );
2023-08-02 22:14:45 +08:00
$this -> addOption ( 'with-hardcoded-ini' , 'I' , InputOption :: VALUE_IS_ARRAY | InputOption :: VALUE_REQUIRED , 'Patch PHP source code, inject hardcoded INI' );
2024-02-16 01:28:10 +08:00
$this -> addOption ( 'with-micro-fake-cli' , null , null , 'Let phpmicro\'s PHP_SAPI use "cli" instead of "micro"' );
2023-12-10 18:28:15 +08:00
$this -> addOption ( 'with-suggested-libs' , 'L' , null , 'Build with suggested libs for selected exts and libs' );
$this -> addOption ( 'with-suggested-exts' , 'E' , null , 'Build with suggested extensions for selected exts' );
2024-01-03 15:57:05 +08:00
$this -> addOption ( 'with-added-patch' , 'P' , InputOption :: VALUE_IS_ARRAY | InputOption :: VALUE_REQUIRED , 'Inject patch script outside' );
2024-01-29 10:04:21 +08:00
$this -> addOption ( 'without-micro-ext-test' , null , null , 'Disable phpmicro with extension test code' );
2024-02-19 12:17:03 +08:00
$this -> addOption ( 'with-upx-pack' , null , null , 'Compress / pack binary using UPX tool (linux/windows only)' );
2024-03-01 19:19:47 +08:00
$this -> addOption ( 'with-micro-logo' , null , InputOption :: VALUE_REQUIRED , 'Use custom .ico for micro.sfx (windows only)' );
2024-06-20 14:46:08 +08:00
$this -> addOption ( 'enable-micro-win32' , null , null , 'Enable win32 mode for phpmicro (Windows only)' );
2025-09-05 21:36:25 +07:00
$this -> addOption ( 'with-frankenphp-app' , null , InputOption :: VALUE_REQUIRED , 'Path to a folder to be embedded in FrankenPHP' );
2023-03-18 17:32:21 +08:00
}
2023-04-22 17:45:43 +08:00
public function handle () : int
2023-03-18 17:32:21 +08:00
{
2023-08-20 19:51:45 +08:00
// transform string to array
2023-04-22 17:45:43 +08:00
$libraries = array_map ( 'trim' , array_filter ( explode ( ',' , $this -> getOption ( 'with-libs' ))));
2023-08-20 19:51:45 +08:00
// transform string to array
2025-03-26 12:39:55 +08:00
$shared_extensions = array_map ( 'trim' , array_filter ( explode ( ',' , $this -> getOption ( 'build-shared' ))));
2025-03-24 22:39:45 +08:00
// transform string to array
$static_extensions = $this -> parseExtensionList ( $this -> getArgument ( 'extensions' ));
2023-03-18 17:32:21 +08:00
2023-12-10 18:28:15 +08:00
// parse rule with options
2025-03-25 16:13:41 +08:00
$rule = $this -> parseRules ( $shared_extensions );
2023-12-10 18:28:15 +08:00
2025-03-24 23:50:12 +08:00
// check dynamic extension build env
// linux must build with glibc
2025-06-29 22:49:48 +08:00
if ( ! empty ( $shared_extensions ) && SPCTarget :: isStatic ()) {
2025-07-01 11:00:24 +07:00
$this -> output -> writeln ( 'Linux does not support dynamic extension loading with fully static builds, please build with a shared C runtime target!' );
2025-03-24 23:50:12 +08:00
return static :: FAILURE ;
}
2025-03-30 23:50:15 +07:00
$static_and_shared = array_intersect ( $static_extensions , $shared_extensions );
if ( ! empty ( $static_and_shared )) {
2025-04-30 21:46:27 +07:00
$this -> output -> writeln ( '<comment>Building extensions [' . implode ( ',' , $static_and_shared ) . '] as both static and shared, tests may not be accurate or fail.</comment>' );
2025-03-30 23:50:15 +07:00
}
2025-03-24 23:50:12 +08:00
2025-03-25 16:13:41 +08:00
if ( $rule === BUILD_TARGET_NONE ) {
2025-03-24 22:39:45 +08:00
$this -> output -> writeln ( '<error>Please add at least one build SAPI!</error>' );
2025-06-20 00:27:02 +07:00
$this -> output -> writeln ( " <comment> \t --build-cli \t \t Build php-cli SAPI</comment> " );
$this -> output -> writeln ( " <comment> \t --build-micro \t \t Build phpmicro SAPI</comment> " );
$this -> output -> writeln ( " <comment> \t --build-fpm \t \t Build php-fpm SAPI</comment> " );
$this -> output -> writeln ( " <comment> \t --build-embed \t \t Build embed SAPI/libphp</comment> " );
2025-06-18 10:48:09 +07:00
$this -> output -> writeln ( " <comment> \t --build-frankenphp \t Build FrankenPHP SAPI/libphp</comment> " );
2025-06-20 00:27:02 +07:00
$this -> output -> writeln ( " <comment> \t --build-all \t \t Build all SAPI: cli, micro, fpm, embed, frankenphp</comment> " );
2023-08-06 10:43:20 +08:00
return static :: FAILURE ;
2023-03-18 17:32:21 +08:00
}
2024-02-18 14:25:42 +08:00
if ( $rule === BUILD_TARGET_ALL ) {
logger () -> warning ( '--build-all option makes `--no-strip` always true, be aware!' );
}
2024-02-29 15:35:02 +08:00
if (( $rule & BUILD_TARGET_MICRO ) === BUILD_TARGET_MICRO && $this -> getOption ( 'with-micro-logo' )) {
$logo = $this -> getOption ( 'with-micro-logo' );
if ( ! file_exists ( $logo )) {
logger () -> error ( 'Logo file ' . $logo . ' not exist !' );
return static :: FAILURE ;
}
}
2024-02-19 12:17:03 +08:00
// Check upx
$suffix = PHP_OS_FAMILY === 'Windows' ? '.exe' : '' ;
if ( $this -> getOption ( 'with-upx-pack' )) {
// only available for linux for now
if ( ! in_array ( PHP_OS_FAMILY , [ 'Linux' , 'Windows' ])) {
logger () -> error ( 'UPX is only available on Linux and Windows!' );
return static :: FAILURE ;
}
// need to install this manually
if ( ! file_exists ( PKG_ROOT_PATH . '/bin/upx' . $suffix )) {
global $argv ;
logger () -> error ( 'upx does not exist, please install it first:' );
logger () -> error ( '' );
logger () -> error ( " \t " . $argv [ 0 ] . ' install-pkg upx' );
logger () -> error ( '' );
return static :: FAILURE ;
}
// exclusive with no-strip
if ( $this -> getOption ( 'no-strip' )) {
logger () -> warning ( '--with-upx-pack conflicts with --no-strip, --no-strip won\'t work!' );
}
2024-05-16 13:01:11 +08:00
if (( $rule & BUILD_TARGET_MICRO ) === BUILD_TARGET_MICRO ) {
logger () -> warning ( 'Some cases micro.sfx cannot be packed via UPX due to dynamic size bug, be aware!' );
}
2024-02-19 12:17:03 +08:00
}
2025-08-06 20:45:16 +08:00
// create builder
$builder = BuilderProvider :: makeBuilderByInput ( $this -> input );
$include_suggest_ext = $this -> getOption ( 'with-suggested-exts' );
$include_suggest_lib = $this -> getOption ( 'with-suggested-libs' );
[ $extensions , $libraries , $not_included ] = DependencyUtil :: getExtsAndLibs ( array_merge ( $static_extensions , $shared_extensions ), $libraries , $include_suggest_ext , $include_suggest_lib );
$display_libs = array_filter ( $libraries , fn ( $lib ) => in_array ( Config :: getLib ( $lib , 'type' , 'lib' ), [ 'lib' , 'package' ]));
2023-12-10 18:28:15 +08:00
2025-08-06 20:45:16 +08:00
// separate static and shared extensions from $extensions
// filter rule: including shared extensions if they are in $static_extensions or $shared_extensions
$static_extensions = array_filter ( $extensions , fn ( $ext ) => ! in_array ( $ext , $shared_extensions ) || in_array ( $ext , $static_extensions ));
2025-04-18 12:18:20 +08:00
2025-08-06 20:45:16 +08:00
// print info
$indent_texts = [
'Build OS' => PHP_OS_FAMILY . ' (' . php_uname ( 'm' ) . ')' ,
'Build Target' => getenv ( 'SPC_TARGET' ),
'Build Toolchain' => getenv ( 'SPC_TOOLCHAIN' ),
'Build SAPI' => $builder -> getBuildTypeName ( $rule ),
'Static Extensions (' . count ( $static_extensions ) . ')' => implode ( ',' , $static_extensions ),
'Shared Extensions (' . count ( $shared_extensions ) . ')' => implode ( ',' , $shared_extensions ),
'Libraries (' . count ( $libraries ) . ')' => implode ( ',' , $display_libs ),
'Strip Binaries' => $builder -> getOption ( 'no-strip' ) ? 'no' : 'yes' ,
'Enable ZTS' => $builder -> getOption ( 'enable-zts' ) ? 'yes' : 'no' ,
];
if ( ! empty ( $shared_extensions ) || ( $rule & BUILD_TARGET_EMBED )) {
$indent_texts [ 'Build Dev' ] = 'yes' ;
}
if ( ! empty ( $this -> input -> getOption ( 'with-config-file-path' ))) {
$indent_texts [ 'Config File Path' ] = $this -> input -> getOption ( 'with-config-file-path' );
}
if ( ! empty ( $this -> input -> getOption ( 'with-hardcoded-ini' ))) {
$indent_texts [ 'Hardcoded INI' ] = $this -> input -> getOption ( 'with-hardcoded-ini' );
}
if ( $this -> input -> getOption ( 'disable-opcache-jit' )) {
$indent_texts [ 'Opcache JIT' ] = 'disabled' ;
}
if ( $this -> input -> getOption ( 'with-upx-pack' ) && in_array ( PHP_OS_FAMILY , [ 'Linux' , 'Windows' ])) {
$indent_texts [ 'UPX Pack' ] = 'enabled' ;
}
2025-06-13 13:01:02 +07:00
2025-08-06 20:45:16 +08:00
$ver = $builder -> getPHPVersionFromArchive () ? : $builder -> getPHPVersion ( false );
$indent_texts [ 'PHP Version' ] = $ver ;
2023-12-10 18:28:15 +08:00
2025-08-06 20:45:16 +08:00
if ( ! empty ( $not_included )) {
$indent_texts [ 'Extra Exts (' . count ( $not_included ) . ')' ] = implode ( ', ' , $not_included );
}
$this -> printFormatInfo ( $this -> getDefinedEnvs (), true );
$this -> printFormatInfo ( $indent_texts );
2025-08-11 10:48:48 +08:00
// bind extra info to exception handler
2025-08-11 13:29:42 +08:00
ExceptionHandler :: bindBuildPhpExtraInfo ( $indent_texts );
2024-05-16 13:01:11 +08:00
2025-08-06 20:45:16 +08:00
logger () -> notice ( 'Build will start after 2s ...' );
sleep ( 2 );
2023-12-10 18:28:15 +08:00
2025-08-06 20:45:16 +08:00
// compile libraries
$builder -> proveLibs ( $libraries );
// check extensions
$builder -> proveExts ( $static_extensions , $shared_extensions );
// validate libs and extensions
$builder -> validateLibsAndExts ();
2024-05-16 13:01:11 +08:00
2025-08-06 20:45:16 +08:00
// check some things before building all the things
$builder -> checkBeforeBuildPHP ( $rule );
2025-06-18 20:54:54 +08:00
2025-08-06 20:45:16 +08:00
// clean builds and sources
if ( $this -> input -> getOption ( 'with-clean' )) {
logger () -> info ( 'Cleaning source and previous build dir...' );
FileSystem :: removeDir ( SOURCE_PATH );
FileSystem :: removeDir ( BUILD_ROOT_PATH );
}
2023-08-20 19:51:45 +08:00
2025-08-06 20:45:16 +08:00
// build or install libraries
$builder -> setupLibs ();
2025-03-14 18:22:50 +08:00
2025-08-06 20:45:16 +08:00
// Process -I option
$custom_ini = [];
foreach ( $this -> input -> getOption ( 'with-hardcoded-ini' ) as $value ) {
[ $source_name , $ini_value ] = explode ( '=' , $value , 2 );
$custom_ini [ $source_name ] = $ini_value ;
logger () -> info ( 'Adding hardcoded INI [' . $source_name . ' = ' . $ini_value . ']' );
}
if ( ! empty ( $custom_ini )) {
SourcePatcher :: patchHardcodedINI ( $custom_ini );
}
2023-08-20 19:51:45 +08:00
2025-08-06 20:45:16 +08:00
// add static-php-cli.version to main.c, in order to debug php failure more easily
SourcePatcher :: patchSPCVersionToPHP ( $this -> getApplication () -> getVersion ());
2024-02-04 10:56:29 +08:00
2025-08-06 20:45:16 +08:00
// clean old modules that may conflict with the new php build
FileSystem :: removeDir ( BUILD_MODULES_PATH );
// start to build
$builder -> buildPHP ( $rule );
2023-08-20 19:51:45 +08:00
2025-08-06 20:45:16 +08:00
$builder -> testPHP ( $rule );
2025-05-21 18:35:48 +07:00
2025-08-06 20:45:16 +08:00
// compile stopwatch :P
$time = round ( microtime ( true ) - START_TIME , 3 );
logger () -> info ( '' );
logger () -> info ( ' Build complete, used ' . $time . ' s !' );
logger () -> info ( '' );
2023-08-20 19:51:45 +08:00
2025-08-06 20:45:16 +08:00
// ---------- When using bin/spc-alpine-docker, the build root path is different from the host system ----------
$build_root_path = BUILD_ROOT_PATH ;
$fixed = '' ;
2025-10-17 10:13:10 +08:00
$build_root_path = get_display_path ( $build_root_path );
2025-08-06 20:45:16 +08:00
if ( ! empty ( getenv ( 'SPC_FIX_DEPLOY_ROOT' ))) {
$fixed = ' (host system)' ;
}
if (( $rule & BUILD_TARGET_CLI ) === BUILD_TARGET_CLI ) {
$win_suffix = PHP_OS_FAMILY === 'Windows' ? '.exe' : '' ;
$path = FileSystem :: convertPath ( " { $build_root_path } /bin/php { $win_suffix } " );
logger () -> info ( " Static php binary path { $fixed } : { $path } " );
}
if (( $rule & BUILD_TARGET_MICRO ) === BUILD_TARGET_MICRO ) {
$path = FileSystem :: convertPath ( " { $build_root_path } /bin/micro.sfx " );
logger () -> info ( " phpmicro binary path { $fixed } : { $path } " );
}
if (( $rule & BUILD_TARGET_FPM ) === BUILD_TARGET_FPM && PHP_OS_FAMILY !== 'Windows' ) {
$path = FileSystem :: convertPath ( " { $build_root_path } /bin/php-fpm " );
logger () -> info ( " Static php-fpm binary path { $fixed } : { $path } " );
}
if ( ! empty ( $shared_extensions )) {
foreach ( $shared_extensions as $ext ) {
$path = FileSystem :: convertPath ( " { $build_root_path } /modules/ { $ext } .so " );
if ( file_exists ( BUILD_MODULES_PATH . " / { $ext } .so " )) {
logger () -> info ( " Shared extension [ { $ext } ] path { $fixed } : { $path } " );
2025-08-27 08:31:48 +07:00
} elseif ( Config :: getExt ( $ext , 'type' ) !== 'addon' ) {
2025-08-06 20:45:16 +08:00
logger () -> warning ( " Shared extension [ { $ext } ] not found, please check! " );
2025-03-24 23:50:12 +08:00
}
}
2023-03-18 17:32:21 +08:00
}
2025-08-06 20:45:16 +08:00
// export metadata
file_put_contents ( BUILD_ROOT_PATH . '/build-extensions.json' , json_encode ( $extensions , JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ));
file_put_contents ( BUILD_ROOT_PATH . '/build-libraries.json' , json_encode ( $libraries , JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ));
// export licenses
$dumper = new LicenseDumper ();
$dumper -> addExts ( $extensions ) -> addLibs ( $libraries ) -> addSources ([ 'php-src' ]) -> dump ( BUILD_ROOT_PATH . '/license' );
$path = FileSystem :: convertPath ( " { $build_root_path } /license/ " );
logger () -> info ( " License path { $fixed } : { $path } " );
return static :: SUCCESS ;
2023-03-18 17:32:21 +08:00
}
2023-12-10 18:28:15 +08:00
/**
* Parse build options to rule int .
*/
2025-03-30 23:50:15 +07:00
private function parseRules ( array $shared_extensions = []) : int
2023-12-10 18:28:15 +08:00
{
$rule = BUILD_TARGET_NONE ;
$rule |= ( $this -> getOption ( 'build-cli' ) ? BUILD_TARGET_CLI : BUILD_TARGET_NONE );
$rule |= ( $this -> getOption ( 'build-micro' ) ? BUILD_TARGET_MICRO : BUILD_TARGET_NONE );
$rule |= ( $this -> getOption ( 'build-fpm' ) ? BUILD_TARGET_FPM : BUILD_TARGET_NONE );
2025-06-20 01:57:45 +08:00
$rule |= $this -> getOption ( 'build-embed' ) || ! empty ( $shared_extensions ) ? BUILD_TARGET_EMBED : BUILD_TARGET_NONE ;
2025-06-18 20:54:54 +08:00
$rule |= ( $this -> getOption ( 'build-frankenphp' ) ? ( BUILD_TARGET_FRANKENPHP | BUILD_TARGET_EMBED ) : BUILD_TARGET_NONE );
2025-09-04 14:05:00 +08:00
$rule |= ( $this -> getOption ( 'build-cgi' ) ? BUILD_TARGET_CGI : BUILD_TARGET_NONE );
2023-12-10 18:28:15 +08:00
$rule |= ( $this -> getOption ( 'build-all' ) ? BUILD_TARGET_ALL : BUILD_TARGET_NONE );
return $rule ;
}
2024-04-07 15:52:24 +08:00
private function getDefinedEnvs () : array
{
$envs = GlobalEnvManager :: getInitializedEnv ();
$final = [];
foreach ( $envs as $env ) {
$exp = explode ( '=' , $env , 2 );
$final [ 'Init var [' . $exp [ 0 ] . ']' ] = $exp [ 1 ];
}
return $final ;
}
private function printFormatInfo ( array $indent_texts , bool $debug = false ) : void
2023-12-10 18:28:15 +08:00
{
// calculate space count for every line
$maxlen = 0 ;
foreach ( $indent_texts as $k => $v ) {
$maxlen = max ( strlen ( $k ), $maxlen );
}
foreach ( $indent_texts as $k => $v ) {
if ( is_string ( $v )) {
/* @phpstan-ignore-next-line */
2024-04-07 15:52:24 +08:00
logger () -> { $debug ? 'debug' : 'info' }( $k . ': ' . str_pad ( '' , $maxlen - strlen ( $k )) . ConsoleColor :: yellow ( $v ));
2023-12-10 18:28:15 +08:00
} elseif ( is_array ( $v ) && ! is_assoc_array ( $v )) {
$first = array_shift ( $v );
/* @phpstan-ignore-next-line */
2024-04-07 15:52:24 +08:00
logger () -> { $debug ? 'debug' : 'info' }( $k . ': ' . str_pad ( '' , $maxlen - strlen ( $k )) . ConsoleColor :: yellow ( $first ));
2023-12-10 18:28:15 +08:00
foreach ( $v as $vs ) {
/* @phpstan-ignore-next-line */
2024-04-07 15:52:24 +08:00
logger () -> { $debug ? 'debug' : 'info' }( str_pad ( '' , $maxlen + 2 ) . ConsoleColor :: yellow ( $vs ));
2023-12-10 18:28:15 +08:00
}
}
}
}
2023-03-18 17:32:21 +08:00
}