Files
static-php-cli/src/StaticPHP/Package/PhpExtensionPackage.php

279 lines
9.9 KiB
PHP
Raw Normal View History

2025-11-30 15:35:04 +08:00
<?php
declare(strict_types=1);
namespace StaticPHP\Package;
use StaticPHP\Attribute\Package\Stage;
2025-11-30 15:35:04 +08:00
use StaticPHP\Config\PackageConfig;
use StaticPHP\DI\ApplicationContext;
2025-12-08 17:01:58 +08:00
use StaticPHP\Exception\ValidationException;
2025-11-30 15:35:04 +08:00
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Runtime\SystemTarget;
2025-12-08 17:01:58 +08:00
use StaticPHP\Util\SPCConfigUtil;
2025-11-30 15:35:04 +08:00
/**
* Represents a PHP extension package.
*/
class PhpExtensionPackage extends Package
{
/**
* @var array <string, callable> Callbacks for custom PHP configure arguments per OS
*/
protected array $custom_php_configure_arg_callbacks = [];
protected bool $build_shared = false;
protected bool $build_static = false;
protected bool $build_with_php = false;
/**
* @param string $name Name of the php extension
* @param string $type Type of the package, defaults to 'php-extension'
*/
public function __construct(string $name, string $type = 'php-extension', protected array $extension_config = [])
{
// Ensure the package name starts with 'ext-'
if (!str_starts_with($name, 'ext-')) {
$name = "ext-{$name}";
}
if ($this->extension_config === []) {
$this->extension_config = PackageConfig::get($name, 'php-extension', []);
}
parent::__construct($name, $type);
}
2025-12-08 17:01:58 +08:00
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());
}
2025-11-30 15:35:04 +08:00
public function addCustomPhpConfigureArgCallback(string $os, callable $fn): void
{
if ($os === '') {
foreach (['Linux', 'Windows', 'Darwin'] as $supported_os) {
$this->custom_php_configure_arg_callbacks[$supported_os] = $fn;
}
} else {
$this->custom_php_configure_arg_callbacks[$os] = $fn;
}
}
public function getPhpConfigureArg(string $os, bool $shared): string
{
if (isset($this->custom_php_configure_arg_callbacks[$os])) {
$callback = $this->custom_php_configure_arg_callbacks[$os];
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;
2025-12-08 17:01:58 +08:00
$name = str_replace('_', '-', $this->getExtensionName());
2025-11-30 15:35:04 +08:00
$ext_config = PackageConfig::get($name, 'php-extension', []);
$arg_type = match (SystemTarget::getTargetOS()) {
'Windows' => $ext_config['arg-type@windows'] ?? $ext_config['arg-type'] ?? 'enable',
'Darwin' => $ext_config['arg-type@macos'] ?? $ext_config['arg-type@unix'] ?? $ext_config['arg-type'] ?? 'enable',
'Linux' => $ext_config['arg-type@linux'] ?? $ext_config['arg-type@unix'] ?? $ext_config['arg-type'] ?? 'enable',
default => $ext_config['arg-type'] ?? 'enable',
};
return match ($arg_type) {
'enable' => $shared ? "--enable-{$name}=shared" : "--enable-{$name}",
'enable-path' => $shared ? "--enable-{$name}=shared,{$escapedPath}" : "--enable-{$name}={$escapedPath}",
'with' => $shared ? "--with-{$name}=shared" : "--with-{$name}",
'with-path' => $shared ? "--with-{$name}=shared,{$escapedPath}" : "--with-{$name}={$escapedPath}",
default => throw new WrongUsageException("Unknown argument type '{$arg_type}' for PHP extension '{$name}'"),
};
}
public function setBuildShared(bool $build_shared = true): void
{
$this->build_shared = $build_shared;
}
public function setBuildStatic(bool $build_static = true): void
{
$this->build_static = $build_static;
}
public function setBuildWithPhp(bool $build_with_php = true): void
{
$this->build_with_php = $build_with_php;
}
public function isBuildShared(): bool
{
return $this->build_shared;
}
public function isBuildStatic(): bool
{
return $this->build_static;
}
public function isBuildWithPhp(): bool
{
return $this->build_with_php;
}
2025-12-08 17:01:58 +08:00
public function buildShared(): void
{
2025-12-08 17:01:58 +08:00
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]
public function phpizeForUnix(array $env, PhpExtensionPackage $package): void
2025-12-08 17:01:58 +08:00
{
shell()->cd($package->getSourceDir())->setEnv($env)->exec(BUILD_BIN_PATH . '/phpize');
}
/**
* @internal
*/
#[Stage]
public function configureForUnix(array $env, PhpExtensionPackage $package): void
2025-12-08 17:01:58 +08:00
{
$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]
public function makeForUnix(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void
2025-12-08 17:01:58 +08:00
{
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 buildSharedForUnix(PackageBuilder $builder): void
2025-12-08 17:01:58 +08:00
{
$env = $this->getSharedExtensionEnv();
$this->runStage('phpizeForUnix', ['env' => $env]);
$this->runStage('configureForUnix', ['env' => $env]);
$this->runStage('makeForUnix', ['env' => $env]);
2025-12-08 17:01:58 +08:00
// 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);
}
/**
* Register default stages if not already defined by attributes.
* This is called after all attributes have been loaded.
*
* @internal Called by PackageLoader after loading attributes
*/
public function registerDefaultStages(): void
{
// Add build stages for shared build on Unix-like systems
// TODO: Windows shared build support
if ($this->build_shared && in_array(SystemTarget::getTargetOS(), ['Linux', 'Darwin'])) {
if (!$this->hasStage('build')) {
$this->addBuildFunction(SystemTarget::getTargetOS(), [$this, 'buildSharedForUnix']);
}
if (!$this->hasStage('phpizeForUnix')) {
$this->addStage('phpizeForUnix', [$this, 'phpizeForUnix']);
}
if (!$this->hasStage('configureForUnix')) {
$this->addStage('configureForUnix', [$this, 'configureForUnix']);
}
if (!$this->hasStage('makeForUnix')) {
$this->addStage('makeForUnix', [$this, 'makeForUnix']);
}
}
}
2025-12-08 17:01:58 +08:00
/**
* 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)];
}
2025-11-30 15:35:04 +08:00
}