addArgument('extensions', InputArgument::REQUIRED, 'The extensions will be compiled, comma separated'); $this->addOption('with-libs', null, InputOption::VALUE_REQUIRED, 'add additional libraries, comma separated', ''); $this->addOption('build-shared', 'D', InputOption::VALUE_REQUIRED, 'Shared extensions to build, comma separated', ''); $this->addOption('build-micro', null, null, 'Build micro SAPI'); $this->addOption('build-cli', null, null, 'Build cli SAPI'); $this->addOption('build-fpm', null, null, 'Build fpm SAPI (not available on Windows)'); $this->addOption('build-embed', null, null, 'Build embed SAPI (not available on Windows)'); $this->addOption('build-frankenphp', null, null, 'Build FrankenPHP SAPI (not available on Windows)'); $this->addOption('build-cgi', null, null, 'Build cgi SAPI'); $this->addOption('build-all', null, null, 'Build all SAPI'); $this->addOption('no-strip', null, null, 'build without strip, keep symbols to debug'); $this->addOption('disable-opcache-jit', null, null, 'disable opcache jit'); $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'); $this->addOption('with-hardcoded-ini', 'I', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Patch PHP source code, inject hardcoded INI'); $this->addOption('with-micro-fake-cli', null, null, 'Let phpmicro\'s PHP_SAPI use "cli" instead of "micro"'); $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'); $this->addOption('with-added-patch', 'P', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Inject patch script outside'); $this->addOption('without-micro-ext-test', null, null, 'Disable phpmicro with extension test code'); $this->addOption('with-upx-pack', null, null, 'Compress / pack binary using UPX tool (linux/windows only)'); $this->addOption('with-micro-logo', null, InputOption::VALUE_REQUIRED, 'Use custom .ico for micro.sfx (windows only)'); $this->addOption('enable-micro-win32', null, null, 'Enable win32 mode for phpmicro (Windows only)'); $this->addOption('with-frankenphp-app', null, InputOption::VALUE_REQUIRED, 'Path to a folder to be embedded in FrankenPHP'); $this->addOption('with-php', null, InputOption::VALUE_REQUIRED, 'PHP version to build (e.g., 8.2, 8.3, 8.4). Uses php-src-X.Y if available, otherwise php-src'); } public function handle(): int { // transform string to array $libraries = array_map('trim', array_filter(explode(',', $this->getOption('with-libs')))); // transform string to array $shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared')))); // transform string to array $static_extensions = $this->parseExtensionList($this->getArgument('extensions')); // parse rule with options $rule = $this->parseRules($shared_extensions); // check dynamic extension build env // linux must build with glibc if (!empty($shared_extensions) && SPCTarget::isStatic()) { $this->output->writeln('Linux does not support dynamic extension loading with fully static builds, please build with a shared C runtime target!'); return static::FAILURE; } $static_and_shared = array_intersect($static_extensions, $shared_extensions); if (!empty($static_and_shared)) { $this->output->writeln('Building extensions [' . implode(',', $static_and_shared) . '] as both static and shared, tests may not be accurate or fail.'); } if ($rule === BUILD_TARGET_NONE) { $this->output->writeln('Please add at least one build SAPI!'); $this->output->writeln("\t--build-cli\t\tBuild php-cli SAPI"); $this->output->writeln("\t--build-micro\t\tBuild phpmicro SAPI"); $this->output->writeln("\t--build-fpm\t\tBuild php-fpm SAPI"); $this->output->writeln("\t--build-embed\t\tBuild embed SAPI/libphp"); $this->output->writeln("\t--build-frankenphp\tBuild FrankenPHP SAPI/libphp"); $this->output->writeln("\t--build-all\t\tBuild all SAPI: cli, micro, fpm, embed, frankenphp"); return static::FAILURE; } if ($rule === BUILD_TARGET_ALL) { logger()->warning('--build-all option makes `--no-strip` always true, be aware!'); } 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; } } // 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!'); } 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!'); } } // Determine which php-src to use based on --with-php option $php_version = $this->getOption('with-php'); if ($php_version !== null) { // Check if version-specific php-src exists in lock file $version_specific_name = "php-src-{$php_version}"; $lock_file_path = DOWNLOAD_PATH . '/.lock.json'; if (file_exists($lock_file_path)) { $lock_content = json_decode(file_get_contents($lock_file_path), true); if (isset($lock_content[$version_specific_name])) { // Use version-specific php-src f_putenv("SPC_PHP_SRC_NAME={$version_specific_name}"); logger()->info("Building with PHP {$php_version} (using {$version_specific_name})"); } else { logger()->error('No php-src found in downloads. Please run download command first.'); return static::FAILURE; } } else { logger()->error('Lock file not found. Please download sources first.'); return static::FAILURE; } } else { f_putenv('SPC_PHP_SRC_NAME=php-src'); } // 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'])); // 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)); // 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'; } $ver = $builder->getPHPVersionFromArchive() ?: $builder->getPHPVersion(false); $indent_texts['PHP Version'] = $ver; if (!empty($not_included)) { $indent_texts['Extra Exts (' . count($not_included) . ')'] = implode(', ', $not_included); } $this->printFormatInfo($this->getDefinedEnvs(), true); $this->printFormatInfo($indent_texts); // bind extra info to exception handler ExceptionHandler::bindBuildPhpExtraInfo($indent_texts); logger()->notice('Build will start after 2s ...'); sleep(2); // compile libraries $builder->proveLibs($libraries); // check extensions $builder->proveExts($static_extensions, $shared_extensions); // validate libs and extensions $builder->validateLibsAndExts(); // check some things before building all the things $builder->checkBeforeBuildPHP($rule); // 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); } // build or install libraries $builder->setupLibs(); // 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); } // add static-php-cli.version to main.c, in order to debug php failure more easily SourcePatcher::patchSPCVersionToPHP($this->getApplication()->getVersion()); // clean old modules that may conflict with the new php build FileSystem::removeDir(BUILD_MODULES_PATH); // start to build $builder->buildPHP($rule); $builder->testPHP($rule); // compile stopwatch :P $time = round(microtime(true) - START_TIME, 3); logger()->info(''); logger()->info(' Build complete, used ' . $time . ' s !'); logger()->info(''); // ---------- When using bin/spc-alpine-docker, the build root path is different from the host system ---------- $build_root_path = BUILD_ROOT_PATH; $fixed = ''; $build_root_path = get_display_path($build_root_path); 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}"); } elseif (Config::getExt($ext, 'type') !== 'addon') { logger()->warning("Shared extension [{$ext}] not found, please check!"); } } } // 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; } /** * Parse build options to rule int. */ private function parseRules(array $shared_extensions = []): int { $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); $rule |= $this->getOption('build-embed') || !empty($shared_extensions) ? BUILD_TARGET_EMBED : BUILD_TARGET_NONE; $rule |= ($this->getOption('build-frankenphp') ? (BUILD_TARGET_FRANKENPHP | BUILD_TARGET_EMBED) : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-cgi') ? BUILD_TARGET_CGI : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-all') ? BUILD_TARGET_ALL : BUILD_TARGET_NONE); return $rule; } 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 { // 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 */ logger()->{$debug ? 'debug' : 'info'}($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($v)); } elseif (is_array($v) && !is_assoc_array($v)) { $first = array_shift($v); /* @phpstan-ignore-next-line */ logger()->{$debug ? 'debug' : 'info'}($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($first)); foreach ($v as $vs) { /* @phpstan-ignore-next-line */ logger()->{$debug ? 'debug' : 'info'}(str_pad('', $maxlen + 2) . ConsoleColor::yellow($vs)); } } } } }