mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-08 09:25:35 +08:00
Refactor package stage handling and update class structures for improved flexibility
This commit is contained in:
@@ -5,10 +5,11 @@ declare(strict_types=1);
|
||||
namespace StaticPHP\Package;
|
||||
|
||||
use StaticPHP\Artifact\Artifact;
|
||||
use StaticPHP\Artifact\ArtifactLoader;
|
||||
use StaticPHP\Config\PackageConfig;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use StaticPHP\Registry\ArtifactLoader;
|
||||
use StaticPHP\Registry\PackageLoader;
|
||||
|
||||
abstract class Package
|
||||
{
|
||||
@@ -32,15 +33,25 @@ abstract class Package
|
||||
* Run a defined stage of the package.
|
||||
* If the stage is not defined, an exception should be thrown.
|
||||
*
|
||||
* @param string $name Name of the stage to run
|
||||
* @param array $context Additional context to pass to the stage callback
|
||||
* @return mixed Based on the stage definition, return the result of the stage
|
||||
* @param array|callable|string $name Name of the stage to run (can be callable)
|
||||
* @param array $context Additional context to pass to the stage callback
|
||||
* @return mixed Based on the stage definition, return the result of the stage
|
||||
*/
|
||||
public function runStage(string $name, array $context = []): mixed
|
||||
public function runStage(mixed $name, array $context = []): mixed
|
||||
{
|
||||
if (!isset($this->stages[$name])) {
|
||||
if (!$this->hasStage($name)) {
|
||||
$name = match (true) {
|
||||
is_string($name) => $name,
|
||||
is_array($name) && count($name) === 2 => $name[1], // use function name
|
||||
default => '{' . gettype($name) . '}',
|
||||
};
|
||||
throw new SPCInternalException("Stage '{$name}' is not defined for package '{$this->name}'.");
|
||||
}
|
||||
$name = match (true) {
|
||||
is_string($name) => $name,
|
||||
is_array($name) && count($name) === 2 => $name[1], // use function name
|
||||
default => throw new SPCInternalException('Invalid stage name type: ' . gettype($name)),
|
||||
};
|
||||
|
||||
// Merge package context with provided context
|
||||
/** @noinspection PhpDuplicateArrayKeysInspection */
|
||||
@@ -80,9 +91,6 @@ abstract class Package
|
||||
|
||||
/**
|
||||
* Add a stage to the package.
|
||||
*
|
||||
* @param string $name Stage name
|
||||
* @param callable $stage Stage callable
|
||||
*/
|
||||
public function addStage(string $name, callable $stage): void
|
||||
{
|
||||
@@ -92,11 +100,17 @@ abstract class Package
|
||||
/**
|
||||
* Check if the package has a specific stage defined.
|
||||
*
|
||||
* @param string $name Stage name
|
||||
* @param mixed $name Stage name
|
||||
*/
|
||||
public function hasStage(string $name): bool
|
||||
public function hasStage(mixed $name): bool
|
||||
{
|
||||
return isset($this->stages[$name]);
|
||||
if (is_array($name) && count($name) === 2) {
|
||||
return isset($this->stages[$name[1]]); // use function name
|
||||
}
|
||||
if (is_string($name)) {
|
||||
return isset($this->stages[$name]); // use defined name
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ use StaticPHP\Artifact\ArtifactExtractor;
|
||||
use StaticPHP\Artifact\DownloaderOptions;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Registry\PackageLoader;
|
||||
use StaticPHP\Util\DependencyResolver;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Package;
|
||||
|
||||
use StaticPHP\Attribute\Package\AfterStage;
|
||||
use StaticPHP\Attribute\Package\BeforeStage;
|
||||
use StaticPHP\Attribute\Package\BuildFor;
|
||||
use StaticPHP\Attribute\Package\CustomPhpConfigureArg;
|
||||
use StaticPHP\Attribute\Package\Extension;
|
||||
use StaticPHP\Attribute\Package\Info;
|
||||
use StaticPHP\Attribute\Package\InitPackage;
|
||||
use StaticPHP\Attribute\Package\Library;
|
||||
use StaticPHP\Attribute\Package\PatchBeforeBuild;
|
||||
use StaticPHP\Attribute\Package\ResolveBuild;
|
||||
use StaticPHP\Attribute\Package\Stage;
|
||||
use StaticPHP\Attribute\Package\Target;
|
||||
use StaticPHP\Attribute\Package\Validate;
|
||||
use StaticPHP\Config\PackageConfig;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\ValidationException;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
class PackageLoader
|
||||
{
|
||||
/** @var array<string, Package> */
|
||||
private static ?array $packages = null;
|
||||
|
||||
private static array $before_stages = [];
|
||||
|
||||
private static array $after_stage = [];
|
||||
|
||||
private static array $patch_before_builds = [];
|
||||
|
||||
/** @var array<string, true> Track loaded classes to prevent duplicates */
|
||||
private static array $loaded_classes = [];
|
||||
|
||||
public static function initPackageInstances(): void
|
||||
{
|
||||
if (self::$packages !== null) {
|
||||
return;
|
||||
}
|
||||
// init packages instance from config
|
||||
foreach (PackageConfig::getAll() as $name => $item) {
|
||||
$pkg = match ($item['type']) {
|
||||
'target', 'virtual-target' => new TargetPackage($name, $item['type']),
|
||||
'library' => new LibraryPackage($name, $item['type']),
|
||||
'php-extension' => new PhpExtensionPackage($name, $item['type']),
|
||||
default => null,
|
||||
};
|
||||
if ($pkg !== null) {
|
||||
self::$packages[$name] = $pkg;
|
||||
} else {
|
||||
throw new WrongUsageException("Package [{$name}] has unknown type [{$item['type']}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load package definitions from PSR-4 directory.
|
||||
*
|
||||
* @param string $dir Directory path
|
||||
* @param string $base_namespace Base namespace for dir's PSR-4 mapping
|
||||
* @param bool $auto_require Whether to auto-require PHP files (for external plugins not in autoload)
|
||||
*/
|
||||
public static function loadFromPsr4Dir(string $dir, string $base_namespace, bool $auto_require = false): void
|
||||
{
|
||||
self::initPackageInstances();
|
||||
$classes = FileSystem::getClassesPsr4($dir, $base_namespace, auto_require: $auto_require);
|
||||
foreach ($classes as $class) {
|
||||
self::loadFromClass($class);
|
||||
}
|
||||
}
|
||||
|
||||
public static function hasPackage(string $name): bool
|
||||
{
|
||||
return isset(self::$packages[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Package instance by its name.
|
||||
*
|
||||
* @param string $name The name of the package
|
||||
* @return Package Returns the Package instance if found, otherwise null
|
||||
*/
|
||||
public static function getPackage(string $name): Package
|
||||
{
|
||||
if (!isset(self::$packages[$name])) {
|
||||
throw new WrongUsageException("Package [{$name}] not found.");
|
||||
}
|
||||
return self::$packages[$name];
|
||||
}
|
||||
|
||||
public static function getTargetPackage(string $name): TargetPackage
|
||||
{
|
||||
$pkg = self::getPackage($name);
|
||||
if ($pkg instanceof TargetPackage) {
|
||||
return $pkg;
|
||||
}
|
||||
throw new WrongUsageException("Package [{$name}] is not a TargetPackage.");
|
||||
}
|
||||
|
||||
public static function getLibraryPackage(string $name): LibraryPackage
|
||||
{
|
||||
$pkg = self::getPackage($name);
|
||||
if ($pkg instanceof LibraryPackage) {
|
||||
return $pkg;
|
||||
}
|
||||
throw new WrongUsageException("Package [{$name}] is not a LibraryPackage.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all loaded Package instances.
|
||||
*/
|
||||
public static function getPackages(array|string|null $type_filter = null): iterable
|
||||
{
|
||||
foreach (self::$packages as $name => $package) {
|
||||
if ($type_filter === null) {
|
||||
yield $name => $package;
|
||||
} elseif ($package->getType() === $type_filter) {
|
||||
yield $name => $package;
|
||||
} elseif (is_array($type_filter) && in_array($package->getType(), $type_filter, true)) {
|
||||
yield $name => $package;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init package instance from defined classes and attributes.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function loadFromClass(mixed $class): void
|
||||
{
|
||||
$refClass = new \ReflectionClass($class);
|
||||
$class_name = $refClass->getName();
|
||||
|
||||
// Skip if already loaded to prevent duplicate registrations
|
||||
if (isset(self::$loaded_classes[$class_name])) {
|
||||
return;
|
||||
}
|
||||
self::$loaded_classes[$class_name] = true;
|
||||
|
||||
$attributes = $refClass->getAttributes();
|
||||
foreach ($attributes as $attribute) {
|
||||
$pkg = null;
|
||||
|
||||
$attribute_instance = $attribute->newInstance();
|
||||
if ($attribute_instance instanceof Target === false &&
|
||||
$attribute_instance instanceof Library === false &&
|
||||
$attribute_instance instanceof Extension === false) {
|
||||
// not a package attribute
|
||||
continue;
|
||||
}
|
||||
$package_type = PackageConfig::get($attribute_instance->name, 'type');
|
||||
if ($package_type === null) {
|
||||
throw new WrongUsageException("Package [{$attribute_instance->name}] not defined in config, please check your config files.");
|
||||
}
|
||||
|
||||
// if class has parent class and matches the attribute instance, use custom class
|
||||
if ($refClass->getParentClass() !== false) {
|
||||
if (is_a($class_name, Package::class, true)) {
|
||||
self::$packages[$attribute_instance->name] = new $class_name($attribute_instance->name, $package_type);
|
||||
$instance_class = self::$packages[$attribute_instance->name];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($instance_class)) {
|
||||
$instance_class = $refClass->newInstance();
|
||||
}
|
||||
|
||||
$pkg = self::$packages[$attribute_instance->name];
|
||||
|
||||
// validate package type matches
|
||||
$pkg_type_attr = match ($attribute->getName()) {
|
||||
Target::class => ['target', 'virtual-target'],
|
||||
Library::class => ['library'],
|
||||
Extension::class => ['php-extension'],
|
||||
default => null,
|
||||
};
|
||||
if (!in_array($package_type, $pkg_type_attr, true)) {
|
||||
throw new ValidationException("Package [{$attribute_instance->name}] type mismatch: config type is [{$package_type}], but attribute type is [" . implode('|', $pkg_type_attr) . '].');
|
||||
}
|
||||
if ($pkg !== null && !PackageConfig::isPackageExists($pkg->getName())) {
|
||||
throw new ValidationException("Package [{$pkg->getName()}] config not found for class {$class}");
|
||||
}
|
||||
|
||||
// init method attributes
|
||||
$methods = $refClass->getMethods(\ReflectionMethod::IS_PUBLIC);
|
||||
foreach ($methods as $method) {
|
||||
$method_attributes = $method->getAttributes();
|
||||
foreach ($method_attributes as $method_attribute) {
|
||||
$method_instance = $method_attribute->newInstance();
|
||||
match ($method_attribute->getName()) {
|
||||
// #[BuildFor(PHP_OS_FAMILY)]
|
||||
BuildFor::class => self::addBuildFunction($pkg, $method_instance, [$instance_class, $method->getName()]),
|
||||
// #[CustomPhpConfigureArg(PHP_OS_FAMILY)]
|
||||
CustomPhpConfigureArg::class => self::bindCustomPhpConfigureArg($pkg, $method_attribute->newInstance(), [$instance_class, $method->getName()]),
|
||||
// #[Stage('stage_name')]
|
||||
Stage::class => $pkg->addStage($method_attribute->newInstance()->name, [$instance_class, $method->getName()]),
|
||||
// #[InitPackage] (run now with package context)
|
||||
InitPackage::class => ApplicationContext::invoke([$instance_class, $method->getName()], [
|
||||
Package::class => $pkg,
|
||||
$pkg::class => $pkg,
|
||||
]),
|
||||
// #[InitBuild]
|
||||
ResolveBuild::class => $pkg instanceof TargetPackage ? $pkg->setResolveBuildCallback([$instance_class, $method->getName()]) : null,
|
||||
// #[Info]
|
||||
Info::class => $pkg->setInfoCallback([$instance_class, $method->getName()]),
|
||||
// #[Validate]
|
||||
Validate::class => $pkg->setValidateCallback([$instance_class, $method->getName()]),
|
||||
// #[PatchBeforeBuild]
|
||||
PatchBeforeBuild::class => $pkg->setPatchBeforeBuildCallback([$instance_class, $method->getName()]),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
// register package
|
||||
self::$packages[$pkg->getName()] = $pkg;
|
||||
}
|
||||
|
||||
if (!isset($instance_class)) {
|
||||
$instance_class = $refClass->newInstance();
|
||||
}
|
||||
|
||||
// parse non-package available attributes
|
||||
foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
|
||||
$method_attributes = $method->getAttributes();
|
||||
foreach ($method_attributes as $method_attribute) {
|
||||
$method_instance = $method_attribute->newInstance();
|
||||
match ($method_attribute->getName()) {
|
||||
// #[BeforeStage('package_name', 'stage')] and #[AfterStage('package_name', 'stage')]
|
||||
BeforeStage::class => self::$before_stages[$method_instance->package_name][$method_instance->stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved],
|
||||
AfterStage::class => self::$after_stage[$method_instance->package_name][$method_instance->stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved],
|
||||
// #[PatchBeforeBuild()
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function getBeforeStageCallbacks(string $package_name, string $stage): iterable
|
||||
{
|
||||
// match condition
|
||||
$installer = ApplicationContext::get(PackageInstaller::class);
|
||||
$stages = self::$before_stages[$package_name][$stage] ?? [];
|
||||
foreach ($stages as [$callback, $only_when_package_resolved]) {
|
||||
if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) {
|
||||
continue;
|
||||
}
|
||||
yield $callback;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getAfterStageCallbacks(string $package_name, string $stage): array
|
||||
{
|
||||
// match condition
|
||||
$installer = ApplicationContext::get(PackageInstaller::class);
|
||||
$stages = self::$after_stage[$package_name][$stage] ?? [];
|
||||
$result = [];
|
||||
foreach ($stages as [$callback, $only_when_package_resolved]) {
|
||||
if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) {
|
||||
continue;
|
||||
}
|
||||
$result[] = $callback;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function getPatchBeforeBuildCallbacks(string $package_name): array
|
||||
{
|
||||
return self::$patch_before_builds[$package_name] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a custom PHP configure argument callback to a php-extension package.
|
||||
*/
|
||||
private static function bindCustomPhpConfigureArg(Package $pkg, object $attr, callable $fn): void
|
||||
{
|
||||
if (!$pkg instanceof PhpExtensionPackage) {
|
||||
throw new ValidationException("Class [{$pkg->getName()}] must implement PhpExtensionPackage for CustomPhpConfigureArg attribute.");
|
||||
}
|
||||
$pkg->addCustomPhpConfigureArgCallback($attr->os, $fn);
|
||||
}
|
||||
|
||||
private static function addBuildFunction(Package $pkg, object $attr, callable $fn): void
|
||||
{
|
||||
$pkg->addBuildFunction($attr->os, $fn);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Package;
|
||||
|
||||
use StaticPHP\Attribute\Package\Stage;
|
||||
use StaticPHP\Config\PackageConfig;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\ValidationException;
|
||||
@@ -100,22 +101,6 @@ 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
|
||||
@@ -180,18 +165,18 @@ class PhpExtensionPackage extends Package
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* #[Stage('phpize')]
|
||||
*/
|
||||
public function _phpize(array $env, PhpExtensionPackage $package): void
|
||||
#[Stage]
|
||||
public function phpizeForUnix(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
|
||||
#[Stage]
|
||||
public function configureForUnix(array $env, PhpExtensionPackage $package): void
|
||||
{
|
||||
$phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: '';
|
||||
shell()->cd($package->getSourceDir())
|
||||
@@ -205,9 +190,9 @@ class PhpExtensionPackage extends Package
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* #[Stage('make')]
|
||||
*/
|
||||
public function _make(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void
|
||||
#[Stage]
|
||||
public function makeForUnix(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void
|
||||
{
|
||||
shell()->cd($package->getSourceDir())
|
||||
->setEnv($env)
|
||||
@@ -222,13 +207,13 @@ class PhpExtensionPackage extends Package
|
||||
* @internal
|
||||
* #[Stage('build')]
|
||||
*/
|
||||
public function _buildSharedUnix(PackageBuilder $builder): void
|
||||
public function buildSharedForUnix(PackageBuilder $builder): void
|
||||
{
|
||||
$env = $this->getSharedExtensionEnv();
|
||||
|
||||
$this->runStage('phpize', ['env' => $env]);
|
||||
$this->runStage('configure', ['env' => $env]);
|
||||
$this->runStage('make', ['env' => $env]);
|
||||
$this->runStage('phpizeForUnix', ['env' => $env]);
|
||||
$this->runStage('configureForUnix', ['env' => $env]);
|
||||
$this->runStage('makeForUnix', ['env' => $env]);
|
||||
|
||||
// process *.so file
|
||||
$soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName() . '.so';
|
||||
@@ -238,6 +223,32 @@ class PhpExtensionPackage extends Package
|
||||
$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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a given string of library flags into static and shared libraries.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user