2025-11-30 15:35:04 +08:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace StaticPHP\Package;
|
|
|
|
|
|
2025-12-09 14:58:11 +08:00
|
|
|
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-04 10:53:49 +08:00
|
|
|
|
2025-12-08 17:01:58 +08:00
|
|
|
public function buildShared(): void
|
2025-12-04 10:53:49 +08:00
|
|
|
{
|
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
|
|
|
|
|
*/
|
2025-12-09 14:58:11 +08:00
|
|
|
#[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
|
|
|
|
|
*/
|
2025-12-09 14:58:11 +08:00
|
|
|
#[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
|
|
|
|
|
*/
|
2025-12-09 14:58:11 +08:00
|
|
|
#[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')]
|
|
|
|
|
*/
|
2025-12-09 14:58:11 +08:00
|
|
|
public function buildSharedForUnix(PackageBuilder $builder): void
|
2025-12-08 17:01:58 +08:00
|
|
|
{
|
|
|
|
|
$env = $this->getSharedExtensionEnv();
|
|
|
|
|
|
2025-12-09 14:58:11 +08:00
|
|
|
$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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 14:58:11 +08:00
|
|
|
/**
|
|
|
|
|
* 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-12-04 10:53:49 +08:00
|
|
|
}
|
2025-11-30 15:35:04 +08:00
|
|
|
}
|