static-php-cli/src/SPC/builder/LibraryBase.php

418 lines
13 KiB
PHP
Raw Normal View History

2023-03-18 17:32:21 +08:00
<?php
declare(strict_types=1);
namespace SPC\builder;
use SPC\exception\SPCException;
use SPC\exception\SPCInternalException;
use SPC\exception\WrongUsageException;
2023-03-18 17:32:21 +08:00
use SPC\store\Config;
use SPC\store\Downloader;
2024-07-07 20:45:18 +08:00
use SPC\store\FileSystem;
use SPC\store\LockFile;
use SPC\store\SourceManager;
2025-06-12 10:58:58 +08:00
use SPC\util\GlobalValueTrait;
2023-03-18 17:32:21 +08:00
abstract class LibraryBase
{
2025-06-12 10:58:58 +08:00
use GlobalValueTrait;
/** @var string */
2023-03-18 17:32:21 +08:00
public const NAME = 'unknown';
protected string $source_dir;
protected array $dependencies = [];
2023-10-14 14:05:33 +08:00
protected bool $patched = false;
2023-03-18 17:32:21 +08:00
public function __construct(?string $source_dir = null)
{
if (static::NAME === 'unknown') {
throw new SPCInternalException('Please set the NAME constant in ' . static::class);
2023-03-18 17:32:21 +08:00
}
$this->source_dir = $source_dir ?? (SOURCE_PATH . DIRECTORY_SEPARATOR . Config::getLib(static::NAME, 'source'));
2023-03-18 17:32:21 +08:00
}
2024-07-07 20:45:18 +08:00
/**
* Try to install or build this library.
* @param bool $force If true, force install or build
2024-07-07 20:45:18 +08:00
*/
public function setup(bool $force = false): int
{
$source = Config::getLib(static::NAME, 'source');
// if source is locked as pre-built, we just tryInstall it
$pre_built_name = Downloader::getPreBuiltLockName($source);
if (($lock = LockFile::get($pre_built_name)) && $lock['lock_as'] === SPC_DOWNLOAD_PRE_BUILT) {
return $this->tryInstall($lock, $force);
2024-07-07 20:45:18 +08:00
}
return $this->tryBuild($force);
}
/**
* Get library name.
*/
public function getName(): string
{
return static::NAME;
}
2023-03-18 17:32:21 +08:00
/**
* Get current lib source root dir.
2023-03-18 17:32:21 +08:00
*/
public function getSourceDir(): string
{
return $this->source_dir;
}
/**
* Get current lib dependencies.
2023-03-18 17:32:21 +08:00
*
* @return array<string, LibraryBase>
2023-03-18 17:32:21 +08:00
*/
public function getDependencies(bool $recursive = false): array
{
// 非递归情况下直接返回通过 addLibraryDependency 方法添加的依赖
if (!$recursive) {
return $this->dependencies;
}
$deps = [];
$added = 1;
while ($added !== 0) {
$added = 0;
foreach ($this->dependencies as $depName => $dep) {
foreach ($dep->getDependencies(true) as $depdepName => $depdep) {
if (!in_array($depdepName, array_keys($deps), true)) {
$deps[$depdepName] = $depdep;
++$added;
}
}
if (!in_array($depName, array_keys($deps), true)) {
$deps[$depName] = $dep;
}
}
}
return $deps;
}
/**
* Calculate dependencies for current library.
2023-03-18 17:32:21 +08:00
*/
public function calcDependency(): void
{
// Add dependencies from the configuration file. Here, choose different metadata based on the operating system.
2023-03-18 17:32:21 +08:00
/*
Rules:
If it is a Windows system, try the following dependencies in order: lib-depends-windows, lib-depends-win, lib-depends.
2025-03-11 05:39:38 +01:00
If it is a macOS system, try the following dependencies in order: lib-depends-macos, lib-depends-unix, lib-depends.
If it is a Linux system, try the following dependencies in order: lib-depends-linux, lib-depends-unix, lib-depends.
*/
2023-03-18 17:32:21 +08:00
foreach (Config::getLib(static::NAME, 'lib-depends', []) as $dep_name) {
$this->addLibraryDependency($dep_name);
}
foreach (Config::getLib(static::NAME, 'lib-suggests', []) as $dep_name) {
$this->addLibraryDependency($dep_name, true);
}
}
/**
* Get config static libs.
2023-03-18 17:32:21 +08:00
*/
public function getStaticLibs(): array
{
return Config::getLib(static::NAME, 'static-libs', []);
}
/**
* Get config headers.
2023-03-18 17:32:21 +08:00
*/
public function getHeaders(): array
{
return Config::getLib(static::NAME, 'headers', []);
}
/**
* Get binary files.
*/
public function getBinaryFiles(): array
{
return Config::getLib(static::NAME, 'bin', []);
}
public function tryInstall(array $lock, bool $force_install = false): int
2024-07-07 20:45:18 +08:00
{
$install_file = $lock['filename'];
2024-07-07 20:45:18 +08:00
if ($force_install) {
logger()->info('Installing required library [' . static::NAME . '] from pre-built binaries');
// Extract files
try {
FileSystem::extractPackage($install_file, $lock['source_type'], DOWNLOAD_PATH . '/' . $install_file, BUILD_ROOT_PATH);
2024-07-07 20:45:18 +08:00
$this->install();
return LIB_STATUS_OK;
} catch (SPCException $e) {
2024-07-07 20:45:18 +08:00
logger()->error('Failed to extract pre-built library [' . static::NAME . ']: ' . $e->getMessage());
return LIB_STATUS_INSTALL_FAILED;
}
}
2025-07-22 17:23:13 +08:00
if (!$this->isLibraryInstalled()) {
2025-07-22 21:13:35 +08:00
return $this->tryInstall($lock, true);
2024-07-07 20:45:18 +08:00
}
return LIB_STATUS_ALREADY;
}
2023-03-18 17:32:21 +08:00
/**
* Try to build this library, before build, we check first.
*
* BUILD_STATUS_OK if build success
* BUILD_STATUS_ALREADY if already built
* BUILD_STATUS_FAILED if build failed
2023-03-18 17:32:21 +08:00
*/
public function tryBuild(bool $force_build = false): int
{
2023-10-14 14:05:33 +08:00
if (file_exists($this->source_dir . '/.spc.patched')) {
$this->patched = true;
}
// force means just build
2023-03-18 17:32:21 +08:00
if ($force_build) {
$type = Config::getLib(static::NAME, 'type', 'lib');
logger()->info('Building required ' . $type . ' [' . static::NAME . ']');
// extract first if not exists
if (!is_dir($this->source_dir)) {
$this->getBuilder()->emitPatchPoint('before-library[' . static::NAME . ']-extract');
SourceManager::initSource(libs: [static::NAME], source_only: true);
$this->getBuilder()->emitPatchPoint('after-library[' . static::NAME . ']-extract');
}
2023-10-14 14:05:33 +08:00
if (!$this->patched && $this->patchBeforeBuild()) {
file_put_contents($this->source_dir . '/.spc.patched', 'PATCHED!!!');
}
$this->getBuilder()->emitPatchPoint('before-library[' . static::NAME . ']-build');
2023-03-18 17:32:21 +08:00
$this->build();
2024-07-08 12:55:41 +08:00
$this->installLicense();
$this->getBuilder()->emitPatchPoint('after-library[' . static::NAME . ']-build');
2024-07-07 20:45:18 +08:00
return LIB_STATUS_OK;
2023-03-18 17:32:21 +08:00
}
if (!$this->isLibraryInstalled()) {
return $this->tryBuild(true);
}
// if all the files exist at this point, skip the compilation process
2024-07-07 20:45:18 +08:00
return LIB_STATUS_ALREADY;
2023-03-18 17:32:21 +08:00
}
public function validate(): void
{
// do nothing, just throw wrong usage exception if not valid
}
/**
* Get current lib version
*
* @return null|string Version string or null
*/
public function getLibVersion(): ?string
{
return null;
}
/**
* Get current builder object.
2023-03-18 17:32:21 +08:00
*/
abstract public function getBuilder(): BuilderBase;
2024-07-07 20:45:18 +08:00
public function beforePack(): void
{
// do something before pack, default do nothing. overwrite this method to do something (e.g. modify pkg-config file)
}
2025-03-12 20:05:05 +07:00
/**
* Patch code before build
* If you need to patch some code, overwrite this
* return true if you patched something, false if not
*/
public function patchBeforeBuild(): bool
{
return false;
}
2025-03-11 07:44:31 +01:00
/**
* Patch code before ./buildconf
* If you need to patch some code, overwrite this
2025-03-12 20:05:05 +07:00
* return true if you patched something, false if not
2025-03-11 07:44:31 +01:00
*/
public function patchBeforeBuildconf(): bool
{
return false;
}
/**
* Patch code before ./configure
* If you need to patch some code, overwrite this
* return true if you patched something, false if not
*/
2025-03-10 10:25:35 +01:00
public function patchBeforeConfigure(): bool
{
return false;
}
/**
* Patch code before windows configure.bat
* If you need to patch some code, overwrite this
* return true if you patched something, false if not
*/
public function patchBeforeWindowsConfigure(): bool
{
return false;
}
2025-03-11 07:44:31 +01:00
/**
* Patch code before make
* If you need to patch some code, overwrite this
* return true if you patched something, false if not
*/
public function patchBeforeMake(): bool
{
return false;
}
2025-07-10 12:59:27 +08:00
/**
* Patch php-config after embed was built
* Example: imap requires -lcrypt
*/
public function patchPhpConfig(): bool
{
return false;
}
2023-03-18 17:32:21 +08:00
/**
* Build this library.
2023-03-18 17:32:21 +08:00
*/
abstract protected function build();
2024-07-07 20:45:18 +08:00
protected function install(): void
{
2025-07-23 11:52:25 +08:00
// replace placeholders if BUILD_ROOT_PATH/.spc-extract-placeholder.json exists
2025-07-23 15:33:26 +08:00
$replace_item_file = BUILD_ROOT_PATH . '/.spc-extract-placeholder.json';
if (!file_exists($replace_item_file)) {
2025-07-23 11:52:25 +08:00
return;
}
2025-07-23 15:33:26 +08:00
$replace_items = json_decode(file_get_contents($replace_item_file), true);
if (!is_array($replace_items)) {
throw new SPCInternalException("Invalid placeholder file: {$replace_item_file}");
2025-07-23 11:52:25 +08:00
}
2025-07-23 15:33:26 +08:00
$placeholders = get_pack_replace();
2025-07-23 11:52:25 +08:00
// replace placeholders in BUILD_ROOT_PATH
2025-07-23 15:33:26 +08:00
foreach ($replace_items as $item) {
2025-07-23 11:52:25 +08:00
$filepath = BUILD_ROOT_PATH . "/{$item}";
FileSystem::replaceFileStr(
$filepath,
2025-07-23 15:33:26 +08:00
array_values($placeholders),
array_keys($placeholders),
2025-07-23 11:52:25 +08:00
);
}
// remove placeholder file
2025-07-23 15:33:26 +08:00
unlink($replace_item_file);
2024-07-07 20:45:18 +08:00
}
2023-03-18 17:32:21 +08:00
/**
* Add lib dependency
2023-03-18 17:32:21 +08:00
*/
protected function addLibraryDependency(string $name, bool $optional = false): void
{
$dep_lib = $this->getBuilder()->getLib($name);
if ($dep_lib) {
2023-03-18 17:32:21 +08:00
$this->dependencies[$name] = $dep_lib;
return;
}
if (!$optional) {
throw new WrongUsageException(static::NAME . " requires library {$name} but it is not included");
2023-03-18 17:32:21 +08:00
}
logger()->debug('enabling ' . static::NAME . " without {$name}");
2023-03-18 17:32:21 +08:00
}
2024-04-07 15:52:24 +08:00
protected function getSnakeCaseName(): string
{
return str_replace('-', '_', static::NAME);
}
2024-07-08 12:55:41 +08:00
/**
* Install license files in buildroot directory
*/
protected function installLicense(): void
{
$source = Config::getLib($this->getName(), 'source');
FileSystem::createDir(BUILD_ROOT_PATH . "/source-licenses/{$source}");
2024-07-08 12:55:41 +08:00
$license_files = Config::getSource($source)['license'] ?? [];
if (is_assoc_array($license_files)) {
$license_files = [$license_files];
}
foreach ($license_files as $index => $license) {
if ($license['type'] === 'text') {
FileSystem::writeFile(BUILD_ROOT_PATH . "/source-licenses/{$source}/{$index}.txt", $license['text']);
2024-07-08 12:55:41 +08:00
continue;
}
if ($license['type'] === 'file') {
copy($this->source_dir . '/' . $license['path'], BUILD_ROOT_PATH . "/source-licenses/{$source}/{$index}.txt");
2024-07-08 12:55:41 +08:00
}
}
}
2025-07-22 17:23:13 +08:00
protected function isLibraryInstalled(): bool
{
if ($pkg_configs = Config::getLib(static::NAME, 'pkg-configs', [])) {
$pkg_config_path = getenv('PKG_CONFIG_PATH') ?: '';
$search_paths = array_unique(array_filter(explode(is_unix() ? ':' : ';', $pkg_config_path)));
foreach ($pkg_configs as $name) {
$found = false;
foreach ($search_paths as $path) {
if (file_exists($path . "/{$name}.pc")) {
$found = true;
break;
}
}
if (!$found) {
return false;
}
}
return true; // allow using system dependencies if pkg_config_path is explicitly defined
}
2025-07-22 17:23:13 +08:00
foreach (Config::getLib(static::NAME, 'static-libs', []) as $name) {
if (!file_exists(BUILD_LIB_PATH . "/{$name}")) {
return false;
}
}
foreach (Config::getLib(static::NAME, 'headers', []) as $name) {
if (!file_exists(BUILD_INCLUDE_PATH . "/{$name}")) {
return false;
}
}
$pkg_config_path = getenv('PKG_CONFIG_PATH') ?: '';
$search_paths = array_filter(explode(is_unix() ? ':' : ';', $pkg_config_path));
2025-07-22 17:23:13 +08:00
foreach (Config::getLib(static::NAME, 'pkg-configs', []) as $name) {
$found = false;
foreach ($search_paths as $path) {
if (file_exists($path . "/{$name}.pc")) {
$found = true;
break;
}
}
if (!$found) {
2025-07-22 17:23:13 +08:00
return false;
}
}
foreach (Config::getLib(static::NAME, 'bin', []) as $name) {
if (!file_exists(BUILD_BIN_PATH . "/{$name}")) {
return false;
}
}
return true;
}
2023-03-18 17:32:21 +08:00
}