diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index c2e01647..4d3683ce 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -14,6 +14,7 @@ use StaticPHP\Attribute\Package\Stage; use StaticPHP\Attribute\Package\Target; use StaticPHP\Attribute\Package\Validate; use StaticPHP\Attribute\PatchDescription; +use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\SPCException; use StaticPHP\Exception\WrongUsageException; @@ -111,7 +112,7 @@ class php } #[ResolveBuild] - public function resolveBuild(TargetPackage $package): array + public function resolveBuild(TargetPackage $package, PackageInstaller $installer): array { // Parse extensions and additional packages for all php-* targets $static_extensions = parse_extension_list($package->getBuildArgument('extensions')); @@ -128,6 +129,7 @@ class php // get instances foreach ($extensions_pkg as $extension) { $extname = substr($extension, 4); + $config = PackageConfig::get($extension, 'php-extension', []); if (!PackageLoader::hasPackage($extension)) { throw new WrongUsageException("Extension [{$extname}] does not exist. Please check your extension name."); } @@ -137,13 +139,25 @@ class php } // set build static/shared if (in_array($extname, $static_extensions)) { + if (($config['build-static'] ?? true) === false) { + throw new WrongUsageException("Extension [{$extname}] cannot be built as static extension."); + } $instance->setBuildStatic(); } if (in_array($extname, $shared_extensions)) { + if (($config['build-shared'] ?? true) === false) { + throw new WrongUsageException("Extension [{$extname}] cannot be built as shared extension, please remove it from --build-shared option."); + } $instance->setBuildShared(); + $instance->setBuildWithPhp($config['build-with-php'] ?? false); } } + // building shared extensions need embed SAPI + if (!empty($shared_extensions) && !$package->getBuildOption('build-embed', false) && $package->getName() === 'php') { + $installer->addBuildPackage('php-embed'); + } + return [...$extensions_pkg, ...$additional_packages]; } @@ -178,14 +192,14 @@ class php return []; } $sapis = array_filter([ - $installer->getBuildPackage('php-cli') ? 'cli' : null, - $installer->getBuildPackage('php-fpm') ? 'fpm' : null, - $installer->getBuildPackage('php-micro') ? 'micro' : null, - $installer->getBuildPackage('php-cgi') ? 'cgi' : null, - $installer->getBuildPackage('php-embed') ? 'embed' : null, - $installer->getBuildPackage('frankenphp') ? 'frankenphp' : null, + $installer->isPackageResolved('php-cli') ? 'cli' : null, + $installer->isPackageResolved('php-fpm') ? 'fpm' : null, + $installer->isPackageResolved('php-micro') ? 'micro' : null, + $installer->isPackageResolved('php-cgi') ? 'cgi' : null, + $installer->isPackageResolved('php-embed') ? 'embed' : null, + $installer->isPackageResolved('frankenphp') ? 'frankenphp' : null, ]); - $static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x->getType() === 'php-extension'); + $static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildStatic()); $shared_extensions = parse_extension_list($package->getBuildOption('build-shared') ?? []); $install_packages = array_filter($installer->getResolvedPackages(), fn ($x) => $x->getType() !== 'php-extension' && $x->getName() !== 'php' && !str_starts_with($x->getName(), 'php-')); return [ @@ -281,15 +295,15 @@ class php $args[] = "--with-config-file-scan-dir={$option}"; } // perform enable cli options - $args[] = $installer->isBuildPackage('php-cli') ? '--enable-cli' : '--disable-cli'; - $args[] = $installer->isBuildPackage('php-fpm') ? '--enable-fpm' : '--disable-fpm'; - $args[] = $installer->isBuildPackage('php-micro') ? match (SystemTarget::getTargetOS()) { + $args[] = $installer->isPackageResolved('php-cli') ? '--enable-cli' : '--disable-cli'; + $args[] = $installer->isPackageResolved('php-fpm') ? '--enable-fpm' : '--disable-fpm'; + $args[] = $installer->isPackageResolved('php-micro') ? match (SystemTarget::getTargetOS()) { 'Linux' => '--enable-micro=all-static', default => '--enable-micro', } : null; - $args[] = $installer->isBuildPackage('php-cgi') ? '--enable-cgi' : '--disable-cgi'; + $args[] = $installer->isPackageResolved('php-cgi') ? '--enable-cgi' : '--disable-cgi'; $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; - $args[] = $installer->isBuildPackage('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed'; + $args[] = $installer->isPackageResolved('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed'; $args[] = getenv('SPC_EXTRA_PHP_VARS') ?: null; $args = implode(' ', array_filter($args)); @@ -311,19 +325,19 @@ class php logger()->info('cleaning up php-src build files'); shell()->cd($package->getSourceDir())->exec('make clean'); - if ($installer->isBuildPackage('php-cli')) { + if ($installer->isPackageResolved('php-cli')) { $package->runStage('unix-make-cli'); } - if ($installer->isBuildPackage('php-cgi')) { + if ($installer->isPackageResolved('php-cgi')) { $package->runStage('unix-make-cgi'); } - if ($installer->isBuildPackage('php-fpm')) { + if ($installer->isPackageResolved('php-fpm')) { $package->runStage('unix-make-fpm'); } - if ($installer->isBuildPackage('php-micro')) { + if ($installer->isPackageResolved('php-micro')) { $package->runStage('unix-make-micro'); } - if ($installer->isBuildPackage('php-embed')) { + if ($installer->isPackageResolved('php-embed')) { $package->runStage('unix-make-embed'); } } @@ -359,6 +373,7 @@ class php } #[Stage('unix-make-micro')] + #[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')] public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { $phar_patched = false; @@ -375,6 +390,8 @@ class php shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$builder->concurrency} micro"); + + $builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', BUILD_BIN_PATH . '/micro.sfx'); } finally { if ($phar_patched) { SourcePatcher::unpatchMicroPhar(); @@ -385,6 +402,7 @@ class php #[Stage('unix-make-embed')] public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make embed')); $shared_exts = array_filter( $installer->getResolvedPackages(), static fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildShared() && $x->isBuildWithPhp() @@ -395,9 +413,11 @@ class php $diff = new DirDiff(BUILD_MODULES_PATH, true); $root = BUILD_ROOT_PATH; + $sed_prefix = SystemTarget::getTargetOS() === 'Darwin' ? 'sed -i ""' : 'sed -i'; + shell()->cd($package->getSourceDir()) ->setEnv($this->makeVars($installer)) - ->exec('sed -i "s|^EXTENSION_DIR = .*|EXTENSION_DIR = /' . basename(BUILD_MODULES_PATH) . '|" Makefile') + ->exec("{$sed_prefix} \"s|^EXTENSION_DIR = .*|EXTENSION_DIR = /" . basename(BUILD_MODULES_PATH) . '|" Makefile') ->exec("make -j{$builder->concurrency} INSTALL_ROOT={$root} install-sapi {$install_modules} install-build install-headers install-programs"); // ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=shared ------------- @@ -417,12 +437,15 @@ class php // process shared extensions that built-with-php $increment_files = $diff->getChangedFiles(); foreach ($increment_files as $increment_file) { - $builder->deployBinary($increment_file, $libphp_so, false); + $builder->deployBinary($increment_file, $increment_file, false); } // ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static ------------- // process libphp.a for static embed + if (!file_exists("{$package->getLibDir()}/libphp.a")) { + return; + } $ar = getenv('AR') ?: 'ar'; $libphp_a = "{$package->getLibDir()}/libphp.a"; shell()->exec("{$ar} -t {$libphp_a} | grep '\\.a$' | xargs -n1 {$ar} d {$libphp_a}"); @@ -432,19 +455,9 @@ class php $package->runStage('patch-embed-scripts'); } - #[BuildFor('Darwin')] - #[BuildFor('Linux')] - public function build(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain): void + #[Stage('unix-build-shared-ext')] + public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain): void { - // virtual target, do nothing - if ($package->getName() !== 'php') { - return; - } - - $package->runStage('unix-buildconf'); - $package->runStage('unix-configure'); - $package->runStage('unix-make'); - // collect shared extensions /** @var PhpExtensionPackage[] $shared_extensions */ $shared_extensions = array_filter( @@ -470,9 +483,10 @@ class php } try { + logger()->debug('Building shared extensions...'); foreach ($shared_extensions as $extension) { - logger()->info('Building shared extensions...'); - $extension->buildSharedExtension(); + InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName())); + $extension->buildShared(); } } finally { // restore php-config @@ -483,6 +497,22 @@ class php } } + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function build(TargetPackage $package): void + { + // virtual target, do nothing + if ($package->getName() !== 'php') { + return; + } + + $package->runStage('unix-buildconf'); + $package->runStage('unix-configure'); + $package->runStage('unix-make'); + + $package->runStage('unix-build-shared-ext'); + } + /** * Patch phpize and php-config if needed */ diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 562d8f8e..667d9688 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -6,8 +6,10 @@ namespace StaticPHP\Package; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; +use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\SPCConfigUtil; /** * Represents a PHP extension package. @@ -41,6 +43,23 @@ class PhpExtensionPackage extends Package parent::__construct($name, $type); } + public function getSourceDir(): string + { + if ($this->getArtifact() === null) { + $path = SOURCE_PATH . '/php-src/ext/' . $this->getExtensionName(); + if (!is_dir($path)) { + throw new ValidationException("Extension source directory not found: {$path}", validation_module: "Extension {$this->getExtensionName()} source"); + } + return $path; + } + return parent::getSourceDir(); + } + + public function getExtensionName(): string + { + return str_replace('ext-', '', $this->getName()); + } + public function addCustomPhpConfigureArgCallback(string $os, callable $fn): void { if ($os === '') { @@ -59,7 +78,7 @@ class PhpExtensionPackage extends Package return ApplicationContext::invoke($callback, ['shared' => $shared, static::class => $this, Package::class => $this]); } $escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH; - $name = str_replace('_', '-', substr($this->getName(), 4)); + $name = str_replace('_', '-', $this->getExtensionName()); $ext_config = PackageConfig::get($name, 'php-extension', []); $arg_type = match (SystemTarget::getTargetOS()) { @@ -81,6 +100,22 @@ class PhpExtensionPackage extends Package public function setBuildShared(bool $build_shared = true): void { $this->build_shared = $build_shared; + // Add build stages for shared build on Unix-like systems + // TODO: Windows shared build support + if ($build_shared && in_array(SystemTarget::getTargetOS(), ['Linux', 'Darwin'])) { + if (!$this->hasStage('build')) { + $this->addBuildFunction(SystemTarget::getTargetOS(), [$this, '_buildSharedUnix']); + } + if (!$this->hasStage('phpize')) { + $this->addStage('phpize', [$this, '_phpize']); + } + if (!$this->hasStage('configure')) { + $this->addStage('configure', [$this, '_configure']); + } + if (!$this->hasStage('make')) { + $this->addStage('make', [$this, '_make']); + } + } } public function setBuildStatic(bool $build_static = true): void @@ -108,8 +143,125 @@ class PhpExtensionPackage extends Package return $this->build_with_php; } - public function buildSharedExtension(): void + public function buildShared(): void { - // TODO: build common shared extensions code here... + if ($this->hasStage('build')) { + $this->runStage('build'); + } else { + throw new WrongUsageException("Extension [{$this->getExtensionName()}] cannot build shared target yet."); + } + } + + /** + * Get shared extension build environment variables for Unix. + * + * @return array{ + * CFLAGS: string, + * CXXFLAGS: string, + * LDFLAGS: string, + * LIBS: string, + * LD_LIBRARY_PATH: string + * } + */ + public function getSharedExtensionEnv(): array + { + $config = (new SPCConfigUtil())->getExtensionConfig($this); + [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); + $preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group '; + $postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group '; + return [ + 'CFLAGS' => $config['cflags'], + 'CXXFLAGS' => $config['cflags'], + 'LDFLAGS' => $config['ldflags'], + 'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"), + 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, + ]; + } + + /** + * @internal + * #[Stage('phpize')] + */ + public function _phpize(array $env, PhpExtensionPackage $package): void + { + shell()->cd($package->getSourceDir())->setEnv($env)->exec(BUILD_BIN_PATH . '/phpize'); + } + + /** + * @internal + * #[Stage('configure')] + */ + public function _configure(array $env, PhpExtensionPackage $package): void + { + $phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + shell()->cd($package->getSourceDir()) + ->setEnv($env) + ->exec( + './configure ' . $this->getPhpConfigureArg(SystemTarget::getCurrentPlatformString(), true) . + ' --with-php-config=' . BUILD_BIN_PATH . '/php-config ' . + "--enable-shared --disable-static {$phpvars}" + ); + } + + /** + * @internal + * #[Stage('make')] + */ + public function _make(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void + { + shell()->cd($package->getSourceDir()) + ->setEnv($env) + ->exec('make clean') + ->exec("make -j{$builder->concurrency}") + ->exec('make install'); + } + + /** + * Build shared extension on Unix-like systems. + * Only for internal calling. For external use, call buildShared() instead. + * @internal + * #[Stage('build')] + */ + public function _buildSharedUnix(PackageBuilder $builder): void + { + $env = $this->getSharedExtensionEnv(); + + $this->runStage('phpize', ['env' => $env]); + $this->runStage('configure', ['env' => $env]); + $this->runStage('make', ['env' => $env]); + + // process *.so file + $soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName() . '.so'; + if (!file_exists($soFile)) { + throw new ValidationException("Extension {$this->getExtensionName()} build failed: {$soFile} not found", validation_module: "Extension {$this->getExtensionName()} build"); + } + $builder->deployBinary($soFile, $soFile, false); + } + + /** + * Splits a given string of library flags into static and shared libraries. + * + * @param string $allLibs A space-separated string of library flags (e.g., -lxyz). + * @return array an array containing two elements: the first is a space-separated string + * of static library flags, and the second is a space-separated string + * of shared library flags + */ + protected function splitLibsIntoStaticAndShared(string $allLibs): array + { + $staticLibString = ''; + $sharedLibString = ''; + $libs = explode(' ', $allLibs); + foreach ($libs as $lib) { + $staticLib = BUILD_LIB_PATH . '/lib' . str_replace('-l', '', $lib) . '.a'; + if (str_starts_with($lib, BUILD_LIB_PATH . '/lib') && str_ends_with($lib, '.a')) { + $staticLib = $lib; + } + if ($lib === '-lphp' || !file_exists($staticLib)) { + $sharedLibString .= " {$lib}"; + } else { + $staticLibString .= " {$lib}"; + } + } + return [trim($staticLibString), trim($sharedLibString)]; } }