mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-08 17:35:36 +08:00
v3 base
This commit is contained in:
188
src/StaticPHP/Package/LibraryPackage.php
Normal file
188
src/StaticPHP/Package/LibraryPackage.php
Normal file
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Package;
|
||||
|
||||
use StaticPHP\Config\PackageConfig;
|
||||
use StaticPHP\Exception\PatchException;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
/**
|
||||
* Represents a library package with platform-specific build functions.
|
||||
*/
|
||||
class LibraryPackage extends Package
|
||||
{
|
||||
/** @var array<string, callable> $build_functions Build functions for different OS binding */
|
||||
protected array $build_functions = [];
|
||||
|
||||
/**
|
||||
* Add a build function for a specific platform.
|
||||
*
|
||||
* @param string $platform PHP_OS_FAMILY
|
||||
* @param callable $func Function to build for the platform
|
||||
*/
|
||||
public function addBuildFunction(string $platform, callable $func): void
|
||||
{
|
||||
$this->build_functions[$platform] = $func;
|
||||
if ($platform === PHP_OS_FAMILY) {
|
||||
$this->addStage('build', $func);
|
||||
}
|
||||
}
|
||||
|
||||
public function isInstalled(): bool
|
||||
{
|
||||
foreach (PackageConfig::get($this->getName(), 'static-libs', []) as $lib) {
|
||||
if (!file_exists("{$this->getLibDir()}/{$lib}")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
foreach (PackageConfig::get($this->getName(), 'headers', []) as $header) {
|
||||
if (!file_exists("{$this->getIncludeDir()}/{$header}")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
foreach (PackageConfig::get($this->getName(), 'pkg-configs', []) as $pc) {
|
||||
if (!file_exists("{$this->getLibDir()}/pkgconfig/{$pc}.pc")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
foreach (PackageConfig::get($this->getName(), 'static-bins', []) as $bin) {
|
||||
if (!file_exists("{$this->getBinDir()}/{$bin}")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function patchLaDependencyPrefix(?array $files = null): void
|
||||
{
|
||||
logger()->info("Patching library {$this->name} la files");
|
||||
$throwOnMissing = true;
|
||||
if ($files === null) {
|
||||
$files = PackageConfig::get($this->getName(), 'static-libs', []);
|
||||
$files = array_map(fn ($name) => str_replace('.a', '.la', $name), $files);
|
||||
$throwOnMissing = false;
|
||||
}
|
||||
foreach ($files as $name) {
|
||||
$realpath = realpath(BUILD_LIB_PATH . '/' . $name);
|
||||
if ($realpath === false) {
|
||||
if ($throwOnMissing) {
|
||||
throw new PatchException('la dependency patcher', "Cannot find library [{$this->name}] la file [{$name}] !");
|
||||
}
|
||||
logger()->warning(message: 'Cannot find library [' . $this->name . '] la file [' . $name . '] !');
|
||||
continue;
|
||||
}
|
||||
logger()->debug('Patching ' . $realpath);
|
||||
// replace prefix
|
||||
$file = FileSystem::readFile($realpath);
|
||||
$file = str_replace(
|
||||
' /lib/',
|
||||
' ' . BUILD_LIB_PATH . '/',
|
||||
$file
|
||||
);
|
||||
$file = preg_replace('/^libdir=.*$/m', "libdir='" . BUILD_LIB_PATH . "'", $file);
|
||||
FileSystem::writeFile($realpath, $file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra CFLAGS for current package.
|
||||
* You need to define the environment variable in the format of {LIBRARY_NAME}_CFLAGS
|
||||
* where {LIBRARY_NAME} is the snake_case name of the library.
|
||||
* For example, for libjpeg, the environment variable should be libjpeg_CFLAGS.
|
||||
*/
|
||||
public function getLibExtraCFlags(): string
|
||||
{
|
||||
// get environment variable
|
||||
$env = getenv($this->getSnakeCaseName() . '_CFLAGS') ?: '';
|
||||
// get default c flags
|
||||
$arch_c_flags = getenv('SPC_DEFAULT_C_FLAGS') ?: '';
|
||||
if (!empty(getenv('SPC_DEFAULT_C_FLAGS')) && !str_contains($env, $arch_c_flags)) {
|
||||
$env .= ' ' . $arch_c_flags;
|
||||
}
|
||||
return trim($env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra CXXFLAGS for current package.
|
||||
* You need to define the environment variable in the format of {LIBRARY_NAME}_CXXFLAGS
|
||||
* where {LIBRARY_NAME} is the snake_case name of the library.
|
||||
* For example, for libjpeg, the environment variable should be libjpeg_CXXFLAGS.
|
||||
*/
|
||||
public function getLibExtraCxxFlags(): string
|
||||
{
|
||||
// get environment variable
|
||||
$env = getenv($this->getSnakeCaseName() . '_CXXFLAGS') ?: '';
|
||||
// get default cxx flags
|
||||
$arch_cxx_flags = getenv('SPC_DEFAULT_CXX_FLAGS') ?: '';
|
||||
if (!empty(getenv('SPC_DEFAULT_CXX_FLAGS')) && !str_contains($env, $arch_cxx_flags)) {
|
||||
$env .= ' ' . $arch_cxx_flags;
|
||||
}
|
||||
return trim($env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra LDFLAGS for current package.
|
||||
* You need to define the environment variable in the format of {LIBRARY_NAME}_LDFLAGS
|
||||
* where {LIBRARY_NAME} is the snake_case name of the library.
|
||||
* For example, for libjpeg, the environment variable should be libjpeg_LDFLAGS.
|
||||
*/
|
||||
public function getLibExtraLdFlags(): string
|
||||
{
|
||||
// get environment variable
|
||||
$env = getenv($this->getSnakeCaseName() . '_LDFLAGS') ?: '';
|
||||
// get default ld flags
|
||||
$arch_ld_flags = getenv('SPC_DEFAULT_LD_FLAGS') ?: '';
|
||||
if (!empty(getenv('SPC_DEFAULT_LD_FLAGS')) && !str_contains($env, $arch_ld_flags)) {
|
||||
$env .= ' ' . $arch_ld_flags;
|
||||
}
|
||||
return trim($env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra LIBS for current package.
|
||||
* You need to define the environment variable in the format of {LIBRARY_NAME}_LIBS
|
||||
* where {LIBRARY_NAME} is the snake_case name of the library.
|
||||
* For example, for libjpeg, the environment variable should be libjpeg_LIBS.
|
||||
*/
|
||||
public function getLibExtraLibs(): string
|
||||
{
|
||||
return getenv($this->getSnakeCaseName() . '_LIBS') ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the build root path for the package.
|
||||
*
|
||||
* TODO: Can be changed to support per-package build root path in the future.
|
||||
*/
|
||||
public function getBuildRootPath(): string
|
||||
{
|
||||
return BUILD_ROOT_PATH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the include directory for the package.
|
||||
*
|
||||
* TODO: Can be changed to support per-package include directory in the future.
|
||||
*/
|
||||
public function getIncludeDir(): string
|
||||
{
|
||||
return BUILD_INCLUDE_PATH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the library directory for the package.
|
||||
*
|
||||
* TODO: Can be changed to support per-package library directory in the future.
|
||||
*/
|
||||
public function getLibDir(): string
|
||||
{
|
||||
return BUILD_LIB_PATH;
|
||||
}
|
||||
|
||||
public function getBinDir(): string
|
||||
{
|
||||
return BUILD_BIN_PATH;
|
||||
}
|
||||
}
|
||||
162
src/StaticPHP/Package/Package.php
Normal file
162
src/StaticPHP/Package/Package.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
|
||||
abstract class Package
|
||||
{
|
||||
use PackageCallbacksTrait;
|
||||
|
||||
/**
|
||||
* @var array<string, callable> $stages Defined stages for the package
|
||||
*/
|
||||
protected array $stages = [];
|
||||
|
||||
/**
|
||||
* @param string $name Name of the package
|
||||
* @param string $type Type of the package
|
||||
*/
|
||||
public function __construct(public readonly string $name, public readonly string $type) {}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function runStage(string $name, array $context = []): mixed
|
||||
{
|
||||
if (!isset($this->stages[$name])) {
|
||||
throw new SPCInternalException("Stage '{$name}' is not defined for package '{$this->name}'.");
|
||||
}
|
||||
|
||||
// Merge package context with provided context
|
||||
/** @noinspection PhpDuplicateArrayKeysInspection */
|
||||
$stageContext = array_merge([
|
||||
Package::class => $this,
|
||||
static::class => $this,
|
||||
], $context);
|
||||
|
||||
// emit BeforeStage
|
||||
$this->emitBeforeStage($name, $stageContext);
|
||||
|
||||
$ret = ApplicationContext::invoke($this->stages[$name], $stageContext);
|
||||
// emit AfterStage
|
||||
$this->emitAfterStage($name, $stageContext, $ret);
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function isInstalled(): bool
|
||||
{
|
||||
// By default, assume package is not installed.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a stage to the package.
|
||||
*
|
||||
* @param string $name Stage name
|
||||
* @param callable $stage Stage callable
|
||||
*/
|
||||
public function addStage(string $name, callable $stage): void
|
||||
{
|
||||
$this->stages[$name] = $stage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the package has a specific stage defined.
|
||||
*
|
||||
* @param string $name Stage name
|
||||
*/
|
||||
public function hasStage(string $name): bool
|
||||
{
|
||||
return isset($this->stages[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the package.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of the package.
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the artifact associated with the package, or null if none is defined.
|
||||
*
|
||||
* @return null|Artifact Artifact instance or null
|
||||
*/
|
||||
public function getArtifact(): ?Artifact
|
||||
{
|
||||
// find config
|
||||
$artifact_name = PackageConfig::get($this->name, 'artifact');
|
||||
return $artifact_name !== null ? ArtifactLoader::getArtifactInstance($artifact_name) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the artifact has source available.
|
||||
*/
|
||||
public function hasSource(): bool
|
||||
{
|
||||
return $this->getArtifact()?->hasSource() ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source directory of the package.
|
||||
* If the source artifact is not available, an exception will be thrown.
|
||||
*/
|
||||
public function getSourceDir(): string
|
||||
{
|
||||
if (($artifact = $this->getArtifact()) && $artifact->hasSource()) {
|
||||
return $artifact->getSourceDir();
|
||||
}
|
||||
throw new SPCInternalException("Source directory for package {$this->name} is not available because the source artifact is missing.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the package has a binary available for current OS and architecture.
|
||||
*/
|
||||
public function hasLocalBinary(): bool
|
||||
{
|
||||
return $this->getArtifact()?->hasPlatformBinary() ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the snake_case name of the package.
|
||||
*/
|
||||
protected function getSnakeCaseName(): string
|
||||
{
|
||||
return str_replace('-', '_', $this->name);
|
||||
}
|
||||
|
||||
private function emitBeforeStage(string $stage, array $stageContext): void
|
||||
{
|
||||
foreach (PackageLoader::getBeforeStageCallbacks($this->getName(), $stage) as $callback) {
|
||||
ApplicationContext::invoke($callback, $stageContext);
|
||||
}
|
||||
}
|
||||
|
||||
private function emitAfterStage(string $stage, array $stageContext, mixed $return_value): void
|
||||
{
|
||||
foreach (PackageLoader::getAfterStageCallbacks($this->getName(), $stage) as $callback) {
|
||||
ApplicationContext::invoke($callback, array_merge($stageContext, ['return_value' => $return_value]));
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/StaticPHP/Package/PackageBuilder.php
Normal file
104
src/StaticPHP/Package/PackageBuilder.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Package;
|
||||
|
||||
use StaticPHP\Config\PackageConfig;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Runtime\Shell\Shell;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
|
||||
class PackageBuilder
|
||||
{
|
||||
/** @var int make jobs count */
|
||||
public readonly int $concurrency;
|
||||
|
||||
/**
|
||||
* @param array $options Builder options
|
||||
*/
|
||||
public function __construct(protected array $options = [])
|
||||
{
|
||||
ApplicationContext::set(PackageBuilder::class, $this);
|
||||
|
||||
// apply build toolchain envs
|
||||
GlobalEnvManager::afterInit();
|
||||
|
||||
$this->concurrency = (int) getenv('SPC_CONCURRENCY') ?: 1;
|
||||
}
|
||||
|
||||
public function buildPackage(Package $package, bool $force = false): int
|
||||
{
|
||||
// init build dirs
|
||||
if (!$package instanceof LibraryPackage) {
|
||||
throw new SPCInternalException('Please, never try to build non-library packages directly.');
|
||||
}
|
||||
FileSystem::createDir($package->getBuildRootPath());
|
||||
FileSystem::createDir($package->getIncludeDir());
|
||||
FileSystem::createDir($package->getBinDir());
|
||||
FileSystem::createDir($package->getLibDir());
|
||||
|
||||
if (!$package->hasStage('build')) {
|
||||
throw new WrongUsageException("Package '{$package->name}' does not have a current platform 'build' stage defined.");
|
||||
}
|
||||
|
||||
// validate package should be built
|
||||
if (!$force) {
|
||||
return $package->isInstalled() ? SPC_STATUS_ALREADY_BUILT : $this->buildPackage($package, true);
|
||||
}
|
||||
// check source is ready
|
||||
if ($package->getType() !== 'virtual-target' && !is_dir($package->getSourceDir())) {
|
||||
throw new WrongUsageException("Source directory for package '{$package->name}' does not exist. Please fetch the source before building.");
|
||||
}
|
||||
Shell::passthruCallback(function () {
|
||||
InteractiveTerm::advance();
|
||||
});
|
||||
|
||||
if ($package->getType() !== 'virtual-target') {
|
||||
// patch before build
|
||||
$package->patchBeforeBuild();
|
||||
}
|
||||
|
||||
// build
|
||||
$package->runStage('build');
|
||||
|
||||
if ($package->getType() !== 'virtual-target') {
|
||||
// install license
|
||||
if (($license = PackageConfig::get($package->getName(), 'license')) !== null) {
|
||||
$this->installLicense($package, $license);
|
||||
}
|
||||
}
|
||||
return SPC_STATUS_BUILT;
|
||||
}
|
||||
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function getOption(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->options[$key] ?? $default;
|
||||
}
|
||||
|
||||
private function installLicense(Package $package, array $license): void
|
||||
{
|
||||
$dir = BUILD_ROOT_PATH . '/source-licenses/' . $package->getName();
|
||||
FileSystem::createDir($dir);
|
||||
if (is_assoc_array($license)) {
|
||||
$license = [$license];
|
||||
}
|
||||
|
||||
foreach ($license as $index => $item) {
|
||||
if ($item['type'] === 'text') {
|
||||
FileSystem::writeFile("{$dir}/{$index}.txt", $item['text']);
|
||||
} elseif ($item['type'] === 'file') {
|
||||
FileSystem::copy("{$package->getSourceDir()}/{$item['path']}", "{$dir}/{$index}.txt");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
src/StaticPHP/Package/PackageCallbacksTrait.php
Normal file
89
src/StaticPHP/Package/PackageCallbacksTrait.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Package;
|
||||
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
/**
|
||||
* Trait for handling package callbacks (info, validate, etc.)
|
||||
*/
|
||||
trait PackageCallbacksTrait
|
||||
{
|
||||
protected mixed $info_callback = null;
|
||||
|
||||
protected mixed $validate_callback = null;
|
||||
|
||||
protected mixed $patch_before_build_callback = null;
|
||||
|
||||
public function setInfoCallback(callable $callback): void
|
||||
{
|
||||
$this->info_callback = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get package info by invoking the info callback.
|
||||
*
|
||||
* @return array<string, mixed> Package information
|
||||
*/
|
||||
public function getPackageInfo(): array
|
||||
{
|
||||
if ($this->info_callback === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Use CallbackInvoker with current package as context
|
||||
$result = ApplicationContext::invoke($this->info_callback, [
|
||||
Package::class => $this,
|
||||
static::class => $this,
|
||||
]);
|
||||
|
||||
return is_array($result) ? $result : [];
|
||||
}
|
||||
|
||||
public function setValidateCallback(callable $callback): void
|
||||
{
|
||||
$this->validate_callback = $callback;
|
||||
}
|
||||
|
||||
public function setPatchBeforeBuildCallback(callable $callback): void
|
||||
{
|
||||
$this->patch_before_build_callback = $callback;
|
||||
}
|
||||
|
||||
public function patchBeforeBuild(): void
|
||||
{
|
||||
if (file_exists("{$this->getSourceDir()}/.spc-patched")) {
|
||||
return;
|
||||
}
|
||||
if ($this->patch_before_build_callback === null) {
|
||||
return;
|
||||
}
|
||||
// Use CallbackInvoker with current package as context
|
||||
$ret = ApplicationContext::invoke($this->patch_before_build_callback, [
|
||||
Package::class => $this,
|
||||
static::class => $this,
|
||||
]);
|
||||
if ($ret === true) {
|
||||
FileSystem::writeFile("{$this->getSourceDir()}/.spc-patched", 'PATCHED!!!');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the package by invoking the validate callback.
|
||||
*/
|
||||
public function validatePackage(): void
|
||||
{
|
||||
if ($this->validate_callback === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use CallbackInvoker with current package as context
|
||||
ApplicationContext::invoke($this->validate_callback, [
|
||||
Package::class => $this,
|
||||
static::class => $this,
|
||||
]);
|
||||
}
|
||||
}
|
||||
534
src/StaticPHP/Package/PackageInstaller.php
Normal file
534
src/StaticPHP/Package/PackageInstaller.php
Normal file
@@ -0,0 +1,534 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Package;
|
||||
|
||||
use StaticPHP\Artifact\Artifact;
|
||||
use StaticPHP\Artifact\ArtifactCache;
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\ArtifactExtractor;
|
||||
use StaticPHP\Artifact\DownloaderOptions;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Util\DependencyResolver;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\V2CompatLayer;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
/**
|
||||
* PackageInstaller is responsible for installing packages within the StaticPHP framework.
|
||||
*/
|
||||
class PackageInstaller
|
||||
{
|
||||
/** @var array<string, Package> Resolved package list */
|
||||
protected array $packages = [];
|
||||
|
||||
/** @var array<string, Package> Packages to be built from source */
|
||||
protected array $build_packages = [];
|
||||
|
||||
/** @var array<string, Package> Packages to be installed */
|
||||
protected array $install_packages = [];
|
||||
|
||||
/** @var array<string, array<string>> Unresolved target additional dependencies defined in #[ResolveBuild] */
|
||||
protected array $target_additional_dependencies = [];
|
||||
|
||||
/** @var bool Whether to download missing sources automatically */
|
||||
protected bool $download = true;
|
||||
|
||||
public function __construct(protected array $options = [])
|
||||
{
|
||||
ApplicationContext::set(PackageInstaller::class, $this);
|
||||
$builder = new PackageBuilder($options);
|
||||
ApplicationContext::set(PackageBuilder::class, $builder);
|
||||
ApplicationContext::set('patch_point', '');
|
||||
|
||||
// Check for no-download option
|
||||
if (!empty($options['no-download'])) {
|
||||
$this->download = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a package to the build list.
|
||||
* This means the package will be built from source.
|
||||
*/
|
||||
public function addBuildPackage(LibraryPackage|string|TargetPackage $package): static
|
||||
{
|
||||
if (is_string($package)) {
|
||||
$package = PackageLoader::getPackage($package);
|
||||
}
|
||||
// special check for php target packages
|
||||
if (in_array($package->getName(), ['php', 'php-cli', 'php-fpm', 'php-micro', 'php-cgi', 'php-embed', 'frankenphp'], true)) {
|
||||
$this->handlePhpTargetPackage($package);
|
||||
return $this;
|
||||
}
|
||||
if (!$package->hasStage('build')) {
|
||||
throw new WrongUsageException("Target package '{$package->getName()}' does not define build process for current OS: " . PHP_OS_FAMILY . '.');
|
||||
}
|
||||
$this->build_packages[$package->getName()] = $package;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Package name
|
||||
* @return null|Package The build package instance or null if not found
|
||||
*/
|
||||
public function getBuildPackage(string $name): ?Package
|
||||
{
|
||||
return $this->build_packages[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a package to the installation list.
|
||||
* This means the package will try to install binary artifacts first.
|
||||
* If no artifacts found, it will fallback to build from source.
|
||||
*/
|
||||
public function addInstallPackage(LibraryPackage|string $package): static
|
||||
{
|
||||
if (is_string($package)) {
|
||||
$package = PackageLoader::getPackage($package);
|
||||
}
|
||||
$this->install_packages[$package->getName()] = $package;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to download packages before installation.
|
||||
*/
|
||||
public function setDownload(bool $download = true): static
|
||||
{
|
||||
$this->download = $download;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the package installation process.
|
||||
*/
|
||||
public function run(bool $interactive = true, bool $disable_delay_msg = false): void
|
||||
{
|
||||
// resolve input, make dependency graph
|
||||
$this->resolvePackages();
|
||||
|
||||
if ($interactive && !$disable_delay_msg) {
|
||||
// show install or build options in terminal with beautiful output
|
||||
$this->printInstallerInfo();
|
||||
|
||||
InteractiveTerm::notice('Build process will start after 2s ...');
|
||||
sleep(2);
|
||||
echo PHP_EOL;
|
||||
}
|
||||
|
||||
// check download
|
||||
if ($this->download) {
|
||||
$downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->options, 'dl');
|
||||
$downloader = new ArtifactDownloader([...$downloaderOptions, 'source-only' => implode(',', array_map(fn ($x) => $x->getName(), $this->build_packages))]);
|
||||
$downloader->addArtifacts($this->getArtifacts())->download($interactive);
|
||||
} else {
|
||||
logger()->notice('Skipping download (--no-download option enabled)');
|
||||
}
|
||||
|
||||
// extract sources
|
||||
$this->extractSourceArtifacts(interactive: $interactive);
|
||||
|
||||
// validate packages
|
||||
foreach ($this->packages as $package) {
|
||||
// 1. call validate package
|
||||
$package->validatePackage();
|
||||
}
|
||||
|
||||
// build/install packages
|
||||
if ($interactive) {
|
||||
InteractiveTerm::notice('Building/Installing packages ...');
|
||||
keyboard_interrupt_register(function () {
|
||||
InteractiveTerm::finish('Build/Install process interrupted by user!', false);
|
||||
exit(130);
|
||||
});
|
||||
}
|
||||
$builder = ApplicationContext::get(PackageBuilder::class);
|
||||
foreach ($this->packages as $package) {
|
||||
if ($this->isBuildPackage($package) || $package instanceof LibraryPackage && $package->hasStage('build')) {
|
||||
if ($interactive) {
|
||||
InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName()));
|
||||
}
|
||||
try {
|
||||
/** @var LibraryPackage $package */
|
||||
$status = $builder->buildPackage($package, $this->isBuildPackage($package));
|
||||
} catch (\Throwable $e) {
|
||||
if ($interactive) {
|
||||
InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false);
|
||||
echo PHP_EOL;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
if ($interactive) {
|
||||
InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : ''));
|
||||
}
|
||||
} elseif ($package instanceof LibraryPackage && $package->getArtifact()->shouldUseBinary()) {
|
||||
// install binary
|
||||
if ($interactive) {
|
||||
InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName()));
|
||||
}
|
||||
try {
|
||||
$status = $this->installBinary($package);
|
||||
} catch (\Throwable $e) {
|
||||
if ($interactive) {
|
||||
InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false);
|
||||
echo PHP_EOL;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
if ($interactive) {
|
||||
InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : ''));
|
||||
}
|
||||
} elseif ($package instanceof LibraryPackage) {
|
||||
throw new WrongUsageException("Package '{$package->getName()}' cannot be installed: no build stage defined and no binary artifact available for current OS.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function isBuildPackage(Package|string $package): bool
|
||||
{
|
||||
return isset($this->build_packages[is_string($package) ? $package : $package->getName()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all resolved packages.
|
||||
*
|
||||
* @return array<string, Package>
|
||||
*/
|
||||
public function getResolvedPackages(): array
|
||||
{
|
||||
return $this->packages;
|
||||
}
|
||||
|
||||
public function isPackageBeingResolved(string $package_name): bool
|
||||
{
|
||||
return isset($this->packages[$package_name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the download status of all artifacts for the resolved packages.
|
||||
*
|
||||
* @return array<string, array{
|
||||
* source-downloaded: bool,
|
||||
* binary-downloaded: bool,
|
||||
* has-source: bool,
|
||||
* has-binary: bool
|
||||
* }> artifact name => [source downloaded, binary downloaded]
|
||||
*/
|
||||
public function getArtifactDownloadStatus(): array
|
||||
{
|
||||
$download_status = [];
|
||||
foreach ($this->getResolvedPackages() as $package) {
|
||||
if (($artifact = $package->getArtifact()) !== null && !isset($download_status[$artifact->getName()])) {
|
||||
// [0: source, 1: binary for current OS]
|
||||
$download_status[$artifact->getName()] = [
|
||||
'source-downloaded' => $artifact->isSourceDownloaded(),
|
||||
'binary-downloaded' => $artifact->isBinaryDownloaded(),
|
||||
'has-source' => $artifact->hasSource(),
|
||||
'has-binary' => $artifact->hasPlatformBinary(),
|
||||
];
|
||||
$download_status[$artifact->getName()] = [$artifact->isSourceDownloaded(), $artifact->isBinaryDownloaded()];
|
||||
}
|
||||
}
|
||||
return $download_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all artifacts from resolved and build packages.
|
||||
*
|
||||
* @return Artifact[]
|
||||
*/
|
||||
public function getArtifacts(): array
|
||||
{
|
||||
$artifacts = [];
|
||||
foreach ($this->getResolvedPackages() as $package) {
|
||||
// Validate package artifacts
|
||||
$this->validatePackageArtifact($package);
|
||||
if (($artifact = $package->getArtifact()) !== null && !in_array($artifact, $artifacts, true)) {
|
||||
$artifacts[] = $artifact;
|
||||
}
|
||||
}
|
||||
// add target artifacts
|
||||
foreach ($this->build_packages as $package) {
|
||||
// Validate package artifacts
|
||||
$this->validatePackageArtifact($package);
|
||||
if (($artifact = $package->getArtifact()) !== null && !in_array($artifact, $artifacts, true)) {
|
||||
$artifacts[] = $artifact;
|
||||
}
|
||||
}
|
||||
return $artifacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all artifacts for resolved packages.
|
||||
*/
|
||||
public function extractSourceArtifacts(bool $interactive = true): void
|
||||
{
|
||||
$packages = array_values($this->packages);
|
||||
|
||||
$cache = ApplicationContext::get(ArtifactCache::class);
|
||||
$extractor = new ArtifactExtractor($cache);
|
||||
|
||||
// Collect all unique artifacts
|
||||
$artifacts = [];
|
||||
$pkg_artifact_map = [];
|
||||
foreach ($packages as $package) {
|
||||
$artifact = $package->getArtifact();
|
||||
if ($artifact !== null && !isset($artifacts[$artifact->getName()]) && (!$artifact->shouldUseBinary() || $this->isBuildPackage($package))) {
|
||||
$pkg_artifact_map[$package->getName()] = $artifact->getName();
|
||||
$artifacts[$artifact->getName()] = $artifact;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: php-src should be extracted first (extensions depend on it)
|
||||
uksort($artifacts, function (string $a, string $b): int {
|
||||
if ($a === 'php-src') {
|
||||
return -1;
|
||||
}
|
||||
if ($b === 'php-src') {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (count($artifacts) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract each artifact
|
||||
if ($interactive) {
|
||||
InteractiveTerm::notice('Extracting source for ' . count($artifacts) . ' artifacts: ' . implode(',', array_map(fn ($x) => ConsoleColor::yellow($x->getName()), $artifacts)) . ' ...');
|
||||
InteractiveTerm::indicateProgress('Extracting artifacts');
|
||||
}
|
||||
|
||||
try {
|
||||
V2CompatLayer::beforeExtsExtractHook();
|
||||
foreach ($artifacts as $artifact) {
|
||||
if ($interactive) {
|
||||
InteractiveTerm::setMessage('Extracting source: ' . ConsoleColor::green($artifact->getName()));
|
||||
}
|
||||
if (($pkg = array_search($artifact->getName(), $pkg_artifact_map, true)) !== false) {
|
||||
V2CompatLayer::beforeLibExtractHook($pkg);
|
||||
}
|
||||
$extractor->extract($artifact, true);
|
||||
if (($pkg = array_search($artifact->getName(), $pkg_artifact_map, true)) !== false) {
|
||||
V2CompatLayer::afterLibExtractHook($pkg);
|
||||
}
|
||||
}
|
||||
V2CompatLayer::afterExtsExtractHook();
|
||||
if ($interactive) {
|
||||
InteractiveTerm::finish('Extracted all sources successfully.');
|
||||
echo PHP_EOL;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
if ($interactive) {
|
||||
InteractiveTerm::finish('Artifact extraction failed!', false);
|
||||
echo PHP_EOL;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function installBinary(Package $package): int
|
||||
{
|
||||
$extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class));
|
||||
$artifact = $package->getArtifact();
|
||||
if ($artifact === null || !$artifact->shouldUseBinary()) {
|
||||
throw new WrongUsageException("Package '{$package->getName()}' does not have a binary artifact to install.");
|
||||
}
|
||||
|
||||
$status = $extractor->extract($artifact);
|
||||
if ($status === SPC_STATUS_ALREADY_EXTRACTED) {
|
||||
return SPC_STATUS_ALREADY_INSTALLED;
|
||||
}
|
||||
|
||||
// perform package after-install actions
|
||||
$this->performAfterInstallActions($package);
|
||||
return SPC_STATUS_INSTALLED;
|
||||
}
|
||||
|
||||
public function getPackage(string $package_name): ?Package
|
||||
{
|
||||
return $this->packages[$package_name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a package has required artifacts.
|
||||
*
|
||||
* @throws WrongUsageException if target/library package has no source or platform binary
|
||||
*/
|
||||
private function validatePackageArtifact(Package $package): void
|
||||
{
|
||||
// target and library must have at least source or platform binary
|
||||
if (in_array($package->getType(), ['library', 'target']) && !$package->getArtifact()?->hasSource() && !$package->getArtifact()?->hasPlatformBinary()) {
|
||||
throw new WrongUsageException("Validation failed: Target package '{$package->getName()}' has no source or platform binary defined.");
|
||||
}
|
||||
}
|
||||
|
||||
private function resolvePackages(): void
|
||||
{
|
||||
$pkgs = [];
|
||||
|
||||
foreach ($this->build_packages as $package) {
|
||||
// call #[ResolveBuild] annotation methods if defined
|
||||
if ($package instanceof TargetPackage && is_array($deps = $package->_emitResolveBuild($this))) {
|
||||
$this->target_additional_dependencies[$package->getName()] = $deps;
|
||||
}
|
||||
$pkgs[] = $package->getName();
|
||||
}
|
||||
|
||||
// gather install packages
|
||||
foreach ($this->install_packages as $package) {
|
||||
$pkgs[] = $package->getName();
|
||||
}
|
||||
|
||||
// resolve dependencies string
|
||||
$resolved_packages = DependencyResolver::resolve(
|
||||
$pkgs,
|
||||
$this->target_additional_dependencies,
|
||||
$this->options['with-suggests'] ?? false
|
||||
);
|
||||
|
||||
foreach ($resolved_packages as $pkg_name) {
|
||||
$this->packages[$pkg_name] = PackageLoader::getPackage($pkg_name);
|
||||
}
|
||||
}
|
||||
|
||||
private function handlePhpTargetPackage(TargetPackage $package): void
|
||||
{
|
||||
// process 'php' target
|
||||
if ($package->getName() === 'php') {
|
||||
logger()->warning("Building 'php' target is deprecated, please use specific targets like 'build:php-cli' instead.");
|
||||
|
||||
$added = false;
|
||||
|
||||
if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cli')) {
|
||||
$cli = PackageLoader::getPackage('php-cli');
|
||||
$this->build_packages[$cli->getName()] = $cli;
|
||||
$added = true;
|
||||
}
|
||||
if ($package->getBuildOption('build-all') || $package->getBuildOption('build-fpm')) {
|
||||
$fpm = PackageLoader::getPackage('php-fpm');
|
||||
$this->build_packages[$fpm->getName()] = $fpm;
|
||||
$added = true;
|
||||
}
|
||||
if ($package->getBuildOption('build-all') || $package->getBuildOption('build-micro')) {
|
||||
$micro = PackageLoader::getPackage('php-micro');
|
||||
$this->build_packages[$micro->getName()] = $micro;
|
||||
$added = true;
|
||||
}
|
||||
if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cgi')) {
|
||||
$cgi = PackageLoader::getPackage('php-cgi');
|
||||
$this->build_packages[$cgi->getName()] = $cgi;
|
||||
$added = true;
|
||||
}
|
||||
if ($package->getBuildOption('build-all') || $package->getBuildOption('build-embed')) {
|
||||
$embed = PackageLoader::getPackage('php-embed');
|
||||
$this->build_packages[$embed->getName()] = $embed;
|
||||
$added = true;
|
||||
}
|
||||
if ($package->getBuildOption('build-all') || $package->getBuildOption('build-frankenphp')) {
|
||||
$frankenphp = PackageLoader::getPackage('frankenphp');
|
||||
$this->build_packages[$frankenphp->getName()] = $frankenphp;
|
||||
$added = true;
|
||||
}
|
||||
$this->build_packages[$package->getName()] = $package;
|
||||
|
||||
if (!$added) {
|
||||
throw new WrongUsageException(
|
||||
"No SAPI target specified to build. Please use '--build-cli', '--build-fpm', '--build-micro', " .
|
||||
"'--build-cgi', '--build-embed', '--build-frankenphp' or '--build-all' options."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// process specific php sapi targets
|
||||
$this->build_packages['php'] = PackageLoader::getPackage('php');
|
||||
$this->build_packages[$package->getName()] = $package;
|
||||
}
|
||||
}
|
||||
|
||||
private function printInstallerInfo(): void
|
||||
{
|
||||
InteractiveTerm::notice('Installation summary:');
|
||||
$summary['Packages to be built'] = implode(',', array_map(fn ($x) => $x->getName(), array_values($this->build_packages)));
|
||||
$summary['Packages to be installed'] = implode(',', array_map(fn ($x) => $x->getName(), array_values($this->packages)));
|
||||
$summary['Artifacts to be downloaded'] = implode(',', array_map(fn ($x) => $x->getName(), $this->getArtifacts()));
|
||||
$this->printArrayInfo(array_filter($summary));
|
||||
echo PHP_EOL;
|
||||
|
||||
foreach ($this->build_packages as $package) {
|
||||
$info = $package->getPackageInfo();
|
||||
if ($info === []) {
|
||||
continue;
|
||||
}
|
||||
InteractiveTerm::notice("{$package->getName()} build options:");
|
||||
// calculate space count for every line
|
||||
$this->printArrayInfo($info);
|
||||
echo PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
private function printArrayInfo(array $info): void
|
||||
{
|
||||
$maxlen = 0;
|
||||
foreach ($info as $k => $v) {
|
||||
$maxlen = max(strlen($k), $maxlen);
|
||||
}
|
||||
foreach ($info as $k => $v) {
|
||||
if (is_string($v)) {
|
||||
InteractiveTerm::plain(" {$k}: " . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($v));
|
||||
} elseif (is_array($v) && !is_assoc_array($v)) {
|
||||
$first = array_shift($v);
|
||||
InteractiveTerm::plain(" {$k}: " . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($first));
|
||||
foreach ($v as $vs) {
|
||||
InteractiveTerm::plain(str_pad('', $maxlen + 4) . ConsoleColor::yellow($vs));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function performAfterInstallActions(Package $package): void
|
||||
{
|
||||
// ----------- perform post-install actions from extracted .package.{pkg_name}.postinstall.json -----------
|
||||
$root_dir = ($package->getArtifact()?->getBinaryDir() ?? '') !== '' ? $package->getArtifact()?->getBinaryDir() : null;
|
||||
if ($root_dir !== null) {
|
||||
$action_json = "{$root_dir}/.package.{$package->getName()}.postinstall.json";
|
||||
if (is_file($action_json)) {
|
||||
$action_json = json_decode(file_get_contents($action_json), true);
|
||||
if (!is_array($action_json)) {
|
||||
throw new WrongUsageException("Invalid post-install action JSON format for package '{$package->getName()}'.");
|
||||
}
|
||||
$placeholders = get_pack_replace();
|
||||
foreach ($action_json as $action) {
|
||||
$action_name = $action['action'] ?? '';
|
||||
switch ($action_name) {
|
||||
// replace-path: => files: [relative_path1, relative_path2]
|
||||
case 'replace-path':
|
||||
$files = $action['files'] ?? [];
|
||||
foreach ($files as $file) {
|
||||
$filepath = $root_dir . "/{$file}";
|
||||
FileSystem::replaceFileStr($filepath, array_values($placeholders), array_keys($placeholders));
|
||||
}
|
||||
break;
|
||||
// replace-to-env: => file: "relative_path", search: "SEARCH_STR", replace-env: "ENV_VAR_NAME"
|
||||
case 'replace-to-env':
|
||||
$file = $action['file'] ?? '';
|
||||
$search = $action['search'] ?? '';
|
||||
$env_var = $action['replace-env'] ?? '';
|
||||
$replace = getenv($env_var) ?: '';
|
||||
$filepath = $root_dir . "/{$file}";
|
||||
FileSystem::replaceFileStr($filepath, $search, $replace);
|
||||
break;
|
||||
default:
|
||||
throw new WrongUsageException("Unknown post-install action '{$action_name}' for package '{$package->getName()}'.");
|
||||
}
|
||||
}
|
||||
// remove the action file after processing
|
||||
unlink($root_dir . "/.package.{$package->getName()}.postinstall.json");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
280
src/StaticPHP/Package/PackageLoader.php
Normal file
280
src/StaticPHP/Package/PackageLoader.php
Normal file
@@ -0,0 +1,280 @@
|
||||
<?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;
|
||||
|
||||
$instance_class = $refClass->newInstance();
|
||||
|
||||
$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.");
|
||||
}
|
||||
$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;
|
||||
}
|
||||
|
||||
// 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->isPackageBeingResolved($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->isPackageBeingResolved($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
|
||||
{
|
||||
if (!$pkg instanceof LibraryPackage) {
|
||||
throw new ValidationException("Class [{$pkg->getName()}] must implement LibraryPackage for BuildFor attribute.");
|
||||
}
|
||||
$pkg->addBuildFunction($attr->os, $fn);
|
||||
}
|
||||
}
|
||||
110
src/StaticPHP/Package/PhpExtensionPackage.php
Normal file
110
src/StaticPHP/Package/PhpExtensionPackage.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Package;
|
||||
|
||||
use StaticPHP\Config\PackageConfig;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
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;
|
||||
$name = str_replace('_', '-', substr($this->getName(), 4));
|
||||
$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;
|
||||
}
|
||||
}
|
||||
142
src/StaticPHP/Package/TargetPackage.php
Normal file
142
src/StaticPHP/Package/TargetPackage.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Package;
|
||||
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Represents a target package in the StaticPHP package management system.
|
||||
*/
|
||||
class TargetPackage extends LibraryPackage
|
||||
{
|
||||
/**
|
||||
* @var array<string, InputOption> $build_options Build options for the target package
|
||||
*/
|
||||
protected array $build_options = [];
|
||||
|
||||
protected array $build_arguments = [];
|
||||
|
||||
protected mixed $resolve_build_callback = null;
|
||||
|
||||
/**
|
||||
* Checks if the target is virtual.
|
||||
*/
|
||||
public function isVirtualTarget(): bool
|
||||
{
|
||||
return $this->type === 'virtual-target';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a build option to the target package.
|
||||
*
|
||||
* @param string $name The name of the build option
|
||||
* @param null|string $shortcut The shortcut for the build option
|
||||
* @param null|int $mode The mode of the build option
|
||||
* @param string $description The description of the build option
|
||||
* @param null|mixed $default The default value of the build option
|
||||
*/
|
||||
public function addBuildOption(string $name, ?string $shortcut = null, ?int $mode = InputOption::VALUE_NONE, string $description = '', mixed $default = null): void
|
||||
{
|
||||
$this->build_options[$name] = new InputOption($name, $shortcut, $mode, $description, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a build argument to the target package.
|
||||
*
|
||||
* @param string $name The name of the build argument
|
||||
* @param null|int $mode The mode of the build argument
|
||||
* @param string $description The description of the build argument
|
||||
* @param null|mixed $default The default value of the build argument
|
||||
*/
|
||||
public function addBuildArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null): void
|
||||
{
|
||||
$this->build_arguments[$name] = new InputArgument($name, $mode, $description, $default);
|
||||
}
|
||||
|
||||
public function setResolveBuildCallback(callable $callback): static
|
||||
{
|
||||
$this->resolve_build_callback = $callback;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a build option value for the target package.
|
||||
*
|
||||
* @param string $key The build option key
|
||||
* @param null|mixed $default The default value if the option is not set
|
||||
* @return mixed The value of the build option
|
||||
*/
|
||||
public function getBuildOption(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$input = ApplicationContext::has(InputInterface::class)
|
||||
? ApplicationContext::get(InputInterface::class)
|
||||
: null;
|
||||
|
||||
if ($input !== null && $input->hasOption($key)) {
|
||||
return $input->getOption($key);
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a build argument value for the target package.
|
||||
*
|
||||
* @param string $key The build argument key
|
||||
* @return mixed The value of the build argument
|
||||
*/
|
||||
public function getBuildArgument(string $key): mixed
|
||||
{
|
||||
$input = ApplicationContext::has(InputInterface::class)
|
||||
? ApplicationContext::get(InputInterface::class)
|
||||
: null;
|
||||
|
||||
if ($input !== null && $input->hasArgument($key)) {
|
||||
return $input->getArgument($key);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all build options for the target package.
|
||||
*
|
||||
* @internal
|
||||
* @return InputOption[] Get all build options for the target package
|
||||
*/
|
||||
public function _exportBuildOptions(): array
|
||||
{
|
||||
return $this->build_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all build arguments for the target package.
|
||||
*
|
||||
* @internal
|
||||
* @return InputArgument[] Get all build arguments for the target package
|
||||
*/
|
||||
public function _exportBuildArguments(): array
|
||||
{
|
||||
return $this->build_arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the init build callback to prepare its dependencies.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function _emitResolveBuild(PackageInstaller $installer): mixed
|
||||
{
|
||||
if (!is_callable($this->resolve_build_callback)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ApplicationContext::invoke($this->resolve_build_callback, [
|
||||
TargetPackage::class => $this,
|
||||
PackageInstaller::class => $installer,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user