mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-07 00:35:41 +08:00
Merge remote-tracking branch 'origin/v3-refactor/libs' into v3-refactor/libs
# Conflicts: # config/artifact.yml # config/pkg.lib.yml # spc.registry.json
This commit is contained in:
@@ -164,7 +164,14 @@ class Artifact
|
||||
|
||||
// For selective mode, cannot reliably check extraction status
|
||||
if ($mode === 'selective') {
|
||||
return false;
|
||||
// check files existence
|
||||
foreach ($extract_config['files'] as $target_file) {
|
||||
$target_file = FileSystem::replacePathVariable($target_file);
|
||||
if (!file_exists($target_file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// For standalone mode, check directory or file and hash
|
||||
@@ -268,6 +275,19 @@ class Artifact
|
||||
return FileSystem::convertPath(SOURCE_PATH . '/' . $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source build root directory.
|
||||
* It's only worked when 'source-root' is defined in artifact config.
|
||||
* Normally it's equal to source dir.
|
||||
*/
|
||||
public function getSourceRoot(): string
|
||||
{
|
||||
if (isset($this->config['metadata']['source-root'])) {
|
||||
return $this->getSourceDir() . '/' . ltrim($this->config['metadata']['source-root'], '/');
|
||||
}
|
||||
return $this->getSourceDir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get binary extraction directory and mode.
|
||||
*
|
||||
|
||||
@@ -312,7 +312,7 @@ class ArtifactDownloader
|
||||
FileSystem::createDir(DOWNLOAD_PATH);
|
||||
}
|
||||
logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $this->artifacts)) . " with concurrency {$this->parallel} ...");
|
||||
// Download artifacts parallely
|
||||
// Download artifacts parallelly
|
||||
if ($this->parallel > 1) {
|
||||
$this->downloadWithConcurrency();
|
||||
} else {
|
||||
@@ -438,7 +438,7 @@ class ArtifactDownloader
|
||||
break;
|
||||
}
|
||||
}
|
||||
$vvv = ApplicationContext::isDebug() ? "\nIf the problem persists, consider using `-vvv` to enable verbose mode, and disable parallel downloading for more details." : '';
|
||||
$vvv = !ApplicationContext::isDebug() ? "\nIf the problem persists, consider using `-vvv` to enable verbose mode, or disable parallel downloading for more details." : '';
|
||||
throw new DownloaderException("Download artifact '{$artifact->getName()}' failed. Please check your internet connection and try again.{$vvv}");
|
||||
}
|
||||
|
||||
|
||||
@@ -247,6 +247,7 @@ class ArtifactExtractor
|
||||
$artifact->emitAfterBinaryExtract($target_path, $platform);
|
||||
logger()->debug("Emitted after-binary-extract hooks for [{$name}]");
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
if ($hash !== null && $cache_info['cache_type'] !== 'file') {
|
||||
FileSystem::writeFile("{$target_path}/.spc-hash", $hash);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ class FileList implements DownloadTypeInterface
|
||||
throw new DownloaderException("Failed to get {$name} file list from {$config['url']}");
|
||||
}
|
||||
$versions = [];
|
||||
logger()->debug('Matched ' . count($matches['version']) . " versions for {$name}");
|
||||
foreach ($matches['version'] as $i => $version) {
|
||||
$lower = strtolower($version);
|
||||
foreach (['alpha', 'beta', 'rc', 'pre', 'nightly', 'snapshot', 'dev'] as $beta) {
|
||||
|
||||
8
src/StaticPHP/Attribute/Package/PatchBeforeBuild.php
Normal file
8
src/StaticPHP/Attribute/Package/PatchBeforeBuild.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Package;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
class PatchBeforeBuild {}
|
||||
@@ -4,22 +4,32 @@ declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command;
|
||||
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Util\V2CompatLayer;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
#[AsCommand('build:libs')]
|
||||
#[AsCommand('build:libs', 'Build specified library packages')]
|
||||
class BuildLibsCommand extends BaseCommand
|
||||
{
|
||||
public function configure()
|
||||
public function configure(): void
|
||||
{
|
||||
$this->addArgument('libraries', InputArgument::REQUIRED, 'The library packages will be compiled, comma separated');
|
||||
// Builder options
|
||||
$this->getDefinition()->addOptions([
|
||||
new InputOption('with-suggests', ['L', 'E'], null, 'Resolve and install suggested packages as well'),
|
||||
new InputOption('with-packages', null, InputOption::VALUE_REQUIRED, 'add additional packages to install/build, comma separated', ''),
|
||||
new InputOption('no-download', null, null, 'Skip downloading artifacts (use existing cached files)'),
|
||||
...V2CompatLayer::getLegacyBuildOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$libs = parse_comma_list($this->input->getArgument('libraries'));
|
||||
|
||||
$installer = new \StaticPHP\Package\PackageInstaller($this->input->getOptions());
|
||||
$installer = new PackageInstaller($this->input->getOptions());
|
||||
foreach ($libs as $lib) {
|
||||
$installer->addBuildPackage($lib);
|
||||
}
|
||||
|
||||
121
src/StaticPHP/Command/Dev/LintConfigCommand.php
Normal file
121
src/StaticPHP/Command/Dev/LintConfigCommand.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command\Dev;
|
||||
|
||||
use StaticPHP\Command\BaseCommand;
|
||||
use StaticPHP\Registry\Registry;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
#[AsCommand('dev:lint-config', 'Lint configuration file format', ['dev:sort-config'])]
|
||||
class LintConfigCommand extends BaseCommand
|
||||
{
|
||||
public function handle(): int
|
||||
{
|
||||
// get loaded configs
|
||||
$loded_configs = Registry::getLoadedArtifactConfigs();
|
||||
foreach ($loded_configs as $file) {
|
||||
$this->sortConfigFile($file, 'artifact');
|
||||
}
|
||||
$loaded_pkg_configs = Registry::getLoadedPackageConfigs();
|
||||
foreach ($loaded_pkg_configs as $file) {
|
||||
$this->sortConfigFile($file, 'package');
|
||||
}
|
||||
return static::SUCCESS;
|
||||
}
|
||||
|
||||
public function artifactSortKey(string $a, string $b): int
|
||||
{
|
||||
// sort by predefined order, other not matching keys go to the end alphabetically
|
||||
$order = ['source', 'source-mirror', 'binary', 'binary-mirror', 'metadata'];
|
||||
|
||||
$pos_a = array_search($a, $order, true);
|
||||
$pos_b = array_search($b, $order, true);
|
||||
|
||||
// Both in order list
|
||||
if ($pos_a !== false && $pos_b !== false) {
|
||||
return $pos_a <=> $pos_b;
|
||||
}
|
||||
|
||||
// Only $a in order list
|
||||
if ($pos_a !== false) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Only $b in order list
|
||||
if ($pos_b !== false) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Neither in order list, sort alphabetically
|
||||
return $a <=> $b;
|
||||
}
|
||||
|
||||
public function packageSortKey(string $a, string $b): int
|
||||
{
|
||||
// sort by predefined order, other not matching keys go to the end alphabetically
|
||||
$order = ['type', 'artifact', 'depends', 'suggests', 'frameworks'];
|
||||
|
||||
// Handle suffix patterns (e.g., 'depends@unix', 'static-libs@windows')
|
||||
$base_a = preg_replace('/@(unix|windows|macos|linux|freebsd|bsd)$/', '', $a);
|
||||
$base_b = preg_replace('/@(unix|windows|macos|linux|freebsd|bsd)$/', '', $b);
|
||||
|
||||
$pos_a = array_search($base_a, $order, true);
|
||||
$pos_b = array_search($base_b, $order, true);
|
||||
|
||||
// Both in order list
|
||||
if ($pos_a !== false && $pos_b !== false) {
|
||||
if ($pos_a === $pos_b) {
|
||||
// Same base field, sort by suffix
|
||||
return $a <=> $b;
|
||||
}
|
||||
return $pos_a <=> $pos_b;
|
||||
}
|
||||
|
||||
// Only $a in order list
|
||||
if ($pos_a !== false) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Only $b in order list
|
||||
if ($pos_b !== false) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Neither in order list, sort alphabetically
|
||||
return $a <=> $b;
|
||||
}
|
||||
|
||||
private function sortConfigFile(mixed $file, string $config_type): void
|
||||
{
|
||||
// read file content with different extensions
|
||||
$content = file_get_contents($file);
|
||||
if ($content === false) {
|
||||
$this->output->writeln("Failed to read artifact config file: {$file}");
|
||||
return;
|
||||
}
|
||||
$data = match (pathinfo($file, PATHINFO_EXTENSION)) {
|
||||
'json' => json_decode($content, true),
|
||||
'yml', 'yaml' => Yaml::parse($content), // skip yaml files for now
|
||||
default => null,
|
||||
};
|
||||
if (!is_array($data)) {
|
||||
$this->output->writeln("Invalid JSON format in artifact config file: {$file}");
|
||||
return;
|
||||
}
|
||||
ksort($data);
|
||||
foreach ($data as $artifact_name => &$config) {
|
||||
uksort($config, $config_type === 'artifact' ? [$this, 'artifactSortKey'] : [$this, 'packageSortKey']);
|
||||
}
|
||||
unset($config);
|
||||
$new_content = match (pathinfo($file, PATHINFO_EXTENSION)) {
|
||||
'json' => json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n",
|
||||
'yml', 'yaml' => Yaml::dump($data, 4, 2),
|
||||
default => null,
|
||||
};
|
||||
file_put_contents($file, $new_content);
|
||||
$this->output->writeln("Sorted artifact config file: {$file}");
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command\Dev;
|
||||
|
||||
use StaticPHP\Command\BaseCommand;
|
||||
use StaticPHP\Registry\Registry;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
#[AsCommand('dev:sort-config', 'Sort artifact configuration files alphabetically')]
|
||||
class SortConfigCommand extends BaseCommand
|
||||
{
|
||||
public function handle(): int
|
||||
{
|
||||
// get loaded configs
|
||||
$loded_configs = Registry::getLoadedArtifactConfigs();
|
||||
foreach ($loded_configs as $file) {
|
||||
$this->sortConfigFile($file);
|
||||
}
|
||||
$loaded_pkg_configs = Registry::getLoadedPackageConfigs();
|
||||
foreach ($loaded_pkg_configs as $file) {
|
||||
$this->sortConfigFile($file);
|
||||
}
|
||||
return static::SUCCESS;
|
||||
}
|
||||
|
||||
private function sortConfigFile(mixed $file): void
|
||||
{
|
||||
$content = file_get_contents($file);
|
||||
if ($content === false) {
|
||||
$this->output->writeln("Failed to read artifact config file: {$file}");
|
||||
return;
|
||||
}
|
||||
$data = json_decode($content, true);
|
||||
if (!is_array($data)) {
|
||||
$this->output->writeln("Invalid JSON format in artifact config file: {$file}");
|
||||
return;
|
||||
}
|
||||
ksort($data);
|
||||
foreach ($data as $artifact_name => &$config) {
|
||||
ksort($config);
|
||||
}
|
||||
unset($config);
|
||||
$new_content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
file_put_contents($file, $new_content);
|
||||
$this->output->writeln("Sorted artifact config file: {$file}");
|
||||
}
|
||||
}
|
||||
@@ -77,4 +77,19 @@ class ArtifactConfig
|
||||
{
|
||||
return self::$artifact_configs[$artifact_name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an inline artifact configuration.
|
||||
* Used when artifact is defined inline within a package configuration.
|
||||
*
|
||||
* @param string $artifact_name Artifact name (usually same as package name)
|
||||
* @param array $config Artifact configuration
|
||||
* @param string $registry_name Registry name
|
||||
* @param string $source_info Source info for debugging
|
||||
*/
|
||||
public static function registerInlineArtifact(string $artifact_name, array $config, string $registry_name, string $source_info = 'inline'): void
|
||||
{
|
||||
self::$artifact_configs[$artifact_name] = $config;
|
||||
Registry::_bindArtifactConfigFile($artifact_name, $registry_name, $source_info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class ConfigValidator
|
||||
'type' => ConfigType::STRING,
|
||||
'depends' => ConfigType::LIST_ARRAY, // @
|
||||
'suggests' => ConfigType::LIST_ARRAY, // @
|
||||
'artifact' => ConfigType::STRING,
|
||||
'artifact' => [self::class, 'validateArtifactField'], // STRING or OBJECT
|
||||
'license' => [ConfigType::class, 'validateLicenseField'],
|
||||
'lang' => ConfigType::STRING,
|
||||
'frameworks' => ConfigType::LIST_ARRAY, // @
|
||||
@@ -102,7 +102,14 @@ class ConfigValidator
|
||||
if (!is_array($data)) {
|
||||
throw new ValidationException("{$config_file_name} is broken");
|
||||
}
|
||||
|
||||
// Define allowed artifact fields
|
||||
$allowed_artifact_fields = ['source', 'source-mirror', 'binary', 'binary-mirror', 'metadata'];
|
||||
|
||||
foreach ($data as $name => $artifact) {
|
||||
// First pass: validate unknown fields
|
||||
self::validateNoInvalidFields('artifact', $name, $artifact, $allowed_artifact_fields);
|
||||
|
||||
foreach ($artifact as $k => $v) {
|
||||
// check source field
|
||||
if ($k === 'source' || $k === 'source-mirror') {
|
||||
@@ -202,6 +209,11 @@ class ConfigValidator
|
||||
throw new ValidationException("Package [{$name}] in {$config_file_name} of type '{$pkg['type']}' must have an 'artifact' field");
|
||||
}
|
||||
|
||||
// validate and lint inline artifact object if present
|
||||
if (isset($pkg['artifact']) && is_array($pkg['artifact'])) {
|
||||
self::validateAndLintInlineArtifact($name, $data[$name]['artifact']);
|
||||
}
|
||||
|
||||
// check if "php-extension" package has php-extension specific fields and validate inside
|
||||
if ($pkg['type'] === 'php-extension') {
|
||||
self::validatePhpExtensionFields($name, $pkg);
|
||||
@@ -234,6 +246,19 @@ class ConfigValidator
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate artifact field - can be string (reference) or object (inline).
|
||||
*
|
||||
* @param mixed $value Field value
|
||||
*/
|
||||
public static function validateArtifactField(mixed $value): bool
|
||||
{
|
||||
if (!is_string($value) && !is_assoc_array($value)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an artifact download object field.
|
||||
*
|
||||
@@ -373,4 +398,19 @@ class ConfigValidator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and lint inline artifact object structure.
|
||||
*
|
||||
* @param string $pkg_name Package name
|
||||
* @param array $artifact Inline artifact configuration (passed by reference to apply linting)
|
||||
*/
|
||||
private static function validateAndLintInlineArtifact(string $pkg_name, array &$artifact): void
|
||||
{
|
||||
// Validate and lint as if it's a standalone artifact
|
||||
$temp_data = [$pkg_name => $artifact];
|
||||
self::validateAndLintArtifacts("inline artifact in package '{$pkg_name}'", $temp_data);
|
||||
// Write back the linted artifact configuration
|
||||
$artifact = $temp_data[$pkg_name];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class PackageConfig
|
||||
throw new WrongUsageException("Directory {$dir} does not exist, cannot load pkg.json config.");
|
||||
}
|
||||
$loaded = [];
|
||||
$files = glob("{$dir}/pkg.*.json");
|
||||
$files = glob("{$dir}/*");
|
||||
if (is_array($files)) {
|
||||
foreach ($files as $file) {
|
||||
self::loadFromFile($file, $registry_name);
|
||||
@@ -58,10 +58,39 @@ class PackageConfig
|
||||
foreach ($data as $pkg_name => $config) {
|
||||
self::$package_configs[$pkg_name] = $config;
|
||||
Registry::_bindPackageConfigFile($pkg_name, $registry_name, $file);
|
||||
|
||||
// Register inline artifact if present
|
||||
if (isset($config['artifact']) && is_array($config['artifact'])) {
|
||||
ArtifactConfig::registerInlineArtifact(
|
||||
$pkg_name,
|
||||
$config['artifact'],
|
||||
$registry_name,
|
||||
"inline in {$file}"
|
||||
);
|
||||
}
|
||||
}
|
||||
return $file;
|
||||
}
|
||||
|
||||
public static function loadFromArray(array $data, string $registry_name): void
|
||||
{
|
||||
ConfigValidator::validateAndLintPackages('array_input', $data);
|
||||
foreach ($data as $pkg_name => $config) {
|
||||
self::$package_configs[$pkg_name] = $config;
|
||||
Registry::_bindPackageConfigFile($pkg_name, $registry_name, 'array_input');
|
||||
|
||||
// Register inline artifact if present
|
||||
if (isset($config['artifact']) && is_array($config['artifact'])) {
|
||||
ArtifactConfig::registerInlineArtifact(
|
||||
$pkg_name,
|
||||
$config['artifact'],
|
||||
$registry_name,
|
||||
'inline in array_input'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a package configuration exists.
|
||||
*/
|
||||
|
||||
@@ -8,8 +8,8 @@ use StaticPHP\Command\BuildLibsCommand;
|
||||
use StaticPHP\Command\BuildTargetCommand;
|
||||
use StaticPHP\Command\Dev\EnvCommand;
|
||||
use StaticPHP\Command\Dev\IsInstalledCommand;
|
||||
use StaticPHP\Command\Dev\LintConfigCommand;
|
||||
use StaticPHP\Command\Dev\ShellCommand;
|
||||
use StaticPHP\Command\Dev\SortConfigCommand;
|
||||
use StaticPHP\Command\DoctorCommand;
|
||||
use StaticPHP\Command\DownloadCommand;
|
||||
use StaticPHP\Command\DumpLicenseCommand;
|
||||
@@ -62,7 +62,7 @@ class ConsoleApplication extends Application
|
||||
new ShellCommand(),
|
||||
new IsInstalledCommand(),
|
||||
new EnvCommand(),
|
||||
new SortConfigCommand(),
|
||||
new LintConfigCommand(),
|
||||
]);
|
||||
|
||||
// add additional commands from registries
|
||||
|
||||
@@ -146,7 +146,8 @@ readonly class Doctor
|
||||
foreach (DoctorLoader::getDoctorItems() as [$item, $optional]) {
|
||||
/* @var CheckItem $item */
|
||||
// optional check
|
||||
if ($optional !== null && !call_user_func($optional)) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
if (is_callable($optional) && !call_user_func($optional)) {
|
||||
continue; // skip this when the optional check is false
|
||||
}
|
||||
// limit_os check
|
||||
|
||||
@@ -27,25 +27,25 @@ class LinuxMuslCheck
|
||||
public static function optionalCheck(): bool
|
||||
{
|
||||
$toolchain = ApplicationContext::get(ToolchainInterface::class);
|
||||
return $toolchain instanceof MuslToolchain || $toolchain instanceof ZigToolchain && !LinuxUtil::isMuslDist();
|
||||
return $toolchain instanceof MuslToolchain || $toolchain instanceof ZigToolchain && !LinuxUtil::isMuslDist() && !str_contains(getenv('SPC_TARGET') ?: '', 'gnu');
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
#[CheckItem('if musl-wrapper is installed', limit_os: 'Linux', level: 800)]
|
||||
public function checkMusl(): CheckResult
|
||||
public function checkMusl(): ?CheckResult
|
||||
{
|
||||
$musl_wrapper_lib = sprintf('/lib/ld-musl-%s.so.1', php_uname('m'));
|
||||
if (file_exists($musl_wrapper_lib) && (file_exists('/usr/local/musl/lib/libc.a') || getenv('SPC_TOOLCHAIN') === ZigToolchain::class)) {
|
||||
return CheckResult::ok();
|
||||
return null;
|
||||
}
|
||||
return CheckResult::fail('musl-wrapper is not installed on your system', 'fix-musl-wrapper');
|
||||
}
|
||||
|
||||
#[CheckItem('if musl-cross-make is installed', limit_os: 'Linux', level: 799)]
|
||||
public function checkMuslCrossMake(): CheckResult
|
||||
public function checkMuslCrossMake(): ?CheckResult
|
||||
{
|
||||
if (getenv('SPC_TOOLCHAIN') === ZigToolchain::class && !LinuxUtil::isMuslDist()) {
|
||||
return CheckResult::ok();
|
||||
return null;
|
||||
}
|
||||
$arch = arch2gnu(php_uname('m'));
|
||||
$cross_compile_lib = "/usr/local/musl/{$arch}-linux-musl/lib/libc.a";
|
||||
|
||||
@@ -19,7 +19,6 @@ class LinuxToolCheck
|
||||
'bzip2', 'cmake', 'gcc',
|
||||
'g++', 'patch', 'binutils-gold',
|
||||
'libtoolize', 'which',
|
||||
'patchelf',
|
||||
];
|
||||
|
||||
public const TOOLS_DEBIAN = [
|
||||
@@ -28,7 +27,6 @@ class LinuxToolCheck
|
||||
'tar', 'unzip', 'gzip', 'gcc', 'g++',
|
||||
'bzip2', 'cmake', 'patch',
|
||||
'xz', 'libtoolize', 'which',
|
||||
'patchelf',
|
||||
];
|
||||
|
||||
public const TOOLS_RHEL = [
|
||||
@@ -36,8 +34,7 @@ class LinuxToolCheck
|
||||
'git', 'autoconf', 'automake',
|
||||
'tar', 'unzip', 'gzip', 'gcc', 'g++',
|
||||
'bzip2', 'cmake', 'patch', 'which',
|
||||
'xz', 'libtool', 'gettext-devel',
|
||||
'patchelf', 'file',
|
||||
'xz', 'libtool', 'gettext-devel', 'file',
|
||||
];
|
||||
|
||||
public const TOOLS_ARCH = [
|
||||
|
||||
@@ -175,8 +175,21 @@ abstract class Package
|
||||
public function getArtifact(): ?Artifact
|
||||
{
|
||||
// find config
|
||||
$artifact_name = PackageConfig::get($this->name, 'artifact');
|
||||
return $artifact_name !== null ? ArtifactLoader::getArtifactInstance($artifact_name) : null;
|
||||
$artifact_field = PackageConfig::get($this->name, 'artifact');
|
||||
|
||||
if ($artifact_field === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_string($artifact_field)) {
|
||||
return ArtifactLoader::getArtifactInstance($artifact_field);
|
||||
}
|
||||
|
||||
if (is_array($artifact_field)) {
|
||||
return ArtifactLoader::getArtifactInstance($this->name);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,6 +212,19 @@ abstract class Package
|
||||
throw new SPCInternalException("Source directory for package {$this->name} is not available because the source artifact is missing.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source build root directory.
|
||||
* It's only worked when 'source-root' is defined in artifact config.
|
||||
* Normally it's equal to source dir.
|
||||
*/
|
||||
public function getSourceRoot(): string
|
||||
{
|
||||
if (($artifact = $this->getArtifact()) && $artifact->hasSource()) {
|
||||
return $artifact->getSourceRoot();
|
||||
}
|
||||
throw new SPCInternalException("Source root 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.
|
||||
*/
|
||||
|
||||
@@ -16,7 +16,7 @@ trait PackageCallbacksTrait
|
||||
|
||||
protected mixed $validate_callback = null;
|
||||
|
||||
protected mixed $patch_before_build_callback = null;
|
||||
protected mixed $patch_before_build_callbacks = null;
|
||||
|
||||
public function setInfoCallback(callable $callback): void
|
||||
{
|
||||
@@ -48,9 +48,9 @@ trait PackageCallbacksTrait
|
||||
$this->validate_callback = $callback;
|
||||
}
|
||||
|
||||
public function setPatchBeforeBuildCallback(callable $callback): void
|
||||
public function addPatchBeforeBuildCallback(callable $callback): void
|
||||
{
|
||||
$this->patch_before_build_callback = $callback;
|
||||
$this->patch_before_build_callbacks[] = $callback;
|
||||
}
|
||||
|
||||
public function patchBeforeBuild(): void
|
||||
@@ -58,16 +58,18 @@ trait PackageCallbacksTrait
|
||||
if (file_exists("{$this->getSourceDir()}/.spc-patched")) {
|
||||
return;
|
||||
}
|
||||
if ($this->patch_before_build_callback === null) {
|
||||
if ($this->patch_before_build_callbacks === 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!!!');
|
||||
foreach ($this->patch_before_build_callbacks as $callback) {
|
||||
$ret = ApplicationContext::invoke($callback, [
|
||||
Package::class => $this,
|
||||
static::class => $this,
|
||||
]);
|
||||
if ($ret === true) {
|
||||
FileSystem::writeFile("{$this->getSourceDir()}/.spc-patched", 'PATCHED!!!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ use StaticPHP\Util\DependencyResolver;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\LicenseDumper;
|
||||
use StaticPHP\Util\V2CompatLayer;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
@@ -208,6 +209,11 @@ class PackageInstaller
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->dumpLicenseFiles($this->packages);
|
||||
if ($interactive) {
|
||||
InteractiveTerm::success('Exported package licenses', true);
|
||||
}
|
||||
}
|
||||
|
||||
public function isBuildPackage(Package|string $package): bool
|
||||
@@ -253,7 +259,7 @@ class PackageInstaller
|
||||
if ($this->isBuildPackage($package)) {
|
||||
return $package->isInstalled();
|
||||
}
|
||||
if ($package instanceof LibraryPackage && $package->getArtifact()->shouldUseBinary()) {
|
||||
if ($package->getArtifact() !== null && $package->getArtifact()->shouldUseBinary()) {
|
||||
$artifact = $package->getArtifact();
|
||||
return $artifact->isBinaryExtracted();
|
||||
}
|
||||
@@ -460,6 +466,21 @@ class PackageInstaller
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Package[] $packages
|
||||
*/
|
||||
private function dumpLicenseFiles(array $packages): void
|
||||
{
|
||||
$dumper = new LicenseDumper();
|
||||
foreach ($packages as $package) {
|
||||
$artifact = $package->getArtifact();
|
||||
if ($artifact !== null) {
|
||||
$dumper->addArtifacts([$artifact->getName()]);
|
||||
}
|
||||
}
|
||||
$dumper->dump(BUILD_ROOT_PATH . '/license');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a package has required artifacts.
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,7 @@ 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;
|
||||
@@ -168,7 +169,7 @@ class PackageLoader
|
||||
}
|
||||
}
|
||||
|
||||
$pkg = self::$packages[$attribute_instance->name];
|
||||
$pkg = self::$packages[$attribute_instance->name] ?? null;
|
||||
|
||||
// Use the package instance if it's a Package subclass, otherwise create a new instance
|
||||
$instance_class = is_a($class_name, Package::class, true) ? $pkg : $refClass->newInstance();
|
||||
@@ -183,7 +184,7 @@ class PackageLoader
|
||||
if (!in_array($package_type, $pkg_type_attr, true)) {
|
||||
throw new RegistryException("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())) {
|
||||
if ($pkg instanceof Package && !PackageConfig::isPackageExists($pkg->getName())) {
|
||||
throw new RegistryException("Package [{$pkg->getName()}] config not found for class {$class}");
|
||||
}
|
||||
|
||||
@@ -196,6 +197,8 @@ class PackageLoader
|
||||
match ($method_attribute->getName()) {
|
||||
// #[BuildFor(PHP_OS_FAMILY)]
|
||||
BuildFor::class => self::addBuildFunction($pkg, $method_instance, [$instance_class, $method->getName()]),
|
||||
// #[BeforeBuild]
|
||||
PatchBeforeBuild::class => self::addPatchBeforeBuildFunction($pkg, [$instance_class, $method->getName()]),
|
||||
// #[CustomPhpConfigureArg(PHP_OS_FAMILY)]
|
||||
CustomPhpConfigureArg::class => self::bindCustomPhpConfigureArg($pkg, $method_attribute->newInstance(), [$instance_class, $method->getName()]),
|
||||
// #[Stage('stage_name')]
|
||||
@@ -332,6 +335,11 @@ class PackageLoader
|
||||
$pkg->addBuildFunction($attr->os, $fn);
|
||||
}
|
||||
|
||||
private static function addPatchBeforeBuildFunction(Package $pkg, callable $fn): void
|
||||
{
|
||||
$pkg->addPatchBeforeBuildCallback($fn);
|
||||
}
|
||||
|
||||
private static function addStage(\ReflectionMethod $method, Package $pkg, object $instance_class, object $method_instance): void
|
||||
{
|
||||
$name = $method_instance->function;
|
||||
@@ -347,7 +355,7 @@ class PackageLoader
|
||||
$stage = $method_instance->stage;
|
||||
$stage = match (true) {
|
||||
is_string($stage) => $stage,
|
||||
is_array($stage) && count($stage) === 2 => $stage[1],
|
||||
count($stage) === 2 => $stage[1],
|
||||
default => throw new RegistryException('Invalid stage definition in BeforeStage attribute.'),
|
||||
};
|
||||
if ($method_instance->package_name === '' && $pkg === null) {
|
||||
|
||||
@@ -13,6 +13,8 @@ use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Registry
|
||||
{
|
||||
private static ?string $current_registry_name = null;
|
||||
|
||||
/** @var string[] List of loaded registries */
|
||||
private static array $loaded_registries = [];
|
||||
|
||||
@@ -35,7 +37,7 @@ class Registry
|
||||
public static function getRegistryConfig(?string $registry_name = null): array
|
||||
{
|
||||
if ($registry_name === null && spc_mode(SPC_MODE_SOURCE)) {
|
||||
return self::$registry_configs['internal'];
|
||||
return self::$registry_configs['core'];
|
||||
}
|
||||
if ($registry_name !== null && isset(self::$registry_configs[$registry_name])) {
|
||||
return self::$registry_configs[$registry_name];
|
||||
@@ -83,6 +85,8 @@ class Registry
|
||||
|
||||
logger()->debug("Loading registry '{$registry_name}' from file: {$registry_file}");
|
||||
|
||||
self::$current_registry_name = $registry_name;
|
||||
|
||||
// Load composer autoload if specified (for external registries with their own dependencies)
|
||||
if (isset($data['autoload']) && is_string($data['autoload'])) {
|
||||
$autoload_path = FileSystem::fullpath($data['autoload'], dirname($registry_file));
|
||||
@@ -94,24 +98,6 @@ class Registry
|
||||
}
|
||||
}
|
||||
|
||||
// load doctor items from PSR-4 directories
|
||||
if (isset($data['doctor']['psr-4']) && is_assoc_array($data['doctor']['psr-4'])) {
|
||||
foreach ($data['doctor']['psr-4'] as $namespace => $path) {
|
||||
$path = FileSystem::fullpath($path, dirname($registry_file));
|
||||
DoctorLoader::loadFromPsr4Dir($path, $namespace, $auto_require);
|
||||
}
|
||||
}
|
||||
|
||||
// load doctor items from specific classes
|
||||
// Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"}
|
||||
if (isset($data['doctor']['classes']) && is_array($data['doctor']['classes'])) {
|
||||
foreach ($data['doctor']['classes'] as $key => $value) {
|
||||
[$class, $file] = self::parseClassEntry($key, $value);
|
||||
self::requireClassFile($class, $file, dirname($registry_file), $auto_require);
|
||||
DoctorLoader::loadFromClass($class);
|
||||
}
|
||||
}
|
||||
|
||||
// load package configs
|
||||
if (isset($data['package']['config']) && is_array($data['package']['config'])) {
|
||||
foreach ($data['package']['config'] as $path) {
|
||||
@@ -136,6 +122,24 @@ class Registry
|
||||
}
|
||||
}
|
||||
|
||||
// load doctor items from PSR-4 directories
|
||||
if (isset($data['doctor']['psr-4']) && is_assoc_array($data['doctor']['psr-4'])) {
|
||||
foreach ($data['doctor']['psr-4'] as $namespace => $path) {
|
||||
$path = FileSystem::fullpath($path, dirname($registry_file));
|
||||
DoctorLoader::loadFromPsr4Dir($path, $namespace, $auto_require);
|
||||
}
|
||||
}
|
||||
|
||||
// load doctor items from specific classes
|
||||
// Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"}
|
||||
if (isset($data['doctor']['classes']) && is_array($data['doctor']['classes'])) {
|
||||
foreach ($data['doctor']['classes'] as $key => $value) {
|
||||
[$class, $file] = self::parseClassEntry($key, $value);
|
||||
self::requireClassFile($class, $file, dirname($registry_file), $auto_require);
|
||||
DoctorLoader::loadFromClass($class);
|
||||
}
|
||||
}
|
||||
|
||||
// load packages from PSR-4 directories
|
||||
if (isset($data['package']['psr-4']) && is_assoc_array($data['package']['psr-4'])) {
|
||||
foreach ($data['package']['psr-4'] as $namespace => $path) {
|
||||
@@ -193,6 +197,7 @@ class Registry
|
||||
}
|
||||
ConsoleApplication::_addAdditionalCommands($instances);
|
||||
}
|
||||
self::$current_registry_name = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,6 +305,11 @@ class Registry
|
||||
return self::$loaded_artifact_configs;
|
||||
}
|
||||
|
||||
public static function getCurrentRegistryName(): ?string
|
||||
{
|
||||
return self::$current_registry_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a class entry from the classes array.
|
||||
* Supports two formats:
|
||||
|
||||
@@ -169,7 +169,7 @@ class UnixAutoconfExecutor extends Executor
|
||||
*/
|
||||
private function initShell(): void
|
||||
{
|
||||
$this->shell = shell()->cd($this->package->getSourceDir())->initializeEnv($this->package)->appendEnv([
|
||||
$this->shell = shell()->cd($this->package->getSourceRoot())->initializeEnv($this->package)->appendEnv([
|
||||
'CFLAGS' => "-I{$this->package->getIncludeDir()}",
|
||||
'CXXFLAGS' => "-I{$this->package->getIncludeDir()}",
|
||||
'LDFLAGS' => "-L{$this->package->getLibDir()}",
|
||||
@@ -185,12 +185,12 @@ class UnixAutoconfExecutor extends Executor
|
||||
$callable();
|
||||
return $this;
|
||||
} catch (SPCException $e) {
|
||||
if (file_exists("{$this->package->getSourceDir()}/config.log")) {
|
||||
logger()->debug("Config log file found: {$this->package->getSourceDir()}/config.log");
|
||||
if (file_exists("{$this->package->getSourceRoot()}/config.log")) {
|
||||
logger()->debug("Config log file found: {$this->package->getSourceRoot()}/config.log");
|
||||
$log_file = "lib.{$this->package->getName()}.console.log";
|
||||
logger()->debug('Saved config log file to: ' . SPC_LOGS_DIR . "/{$log_file}");
|
||||
$e->addExtraLogFile("{$this->package->getName()} library config.log", $log_file);
|
||||
copy("{$this->package->getSourceDir()}/config.log", SPC_LOGS_DIR . "/{$log_file}");
|
||||
copy("{$this->package->getSourceRoot()}/config.log", SPC_LOGS_DIR . "/{$log_file}");
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ class UnixCMakeExecutor extends Executor
|
||||
private function initBuildDir(): void
|
||||
{
|
||||
if ($this->build_dir === null) {
|
||||
$this->build_dir = "{$this->package->getSourceDir()}/build";
|
||||
$this->build_dir = "{$this->package->getSourceRoot()}/build";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +295,7 @@ CMAKE;
|
||||
*/
|
||||
private function initShell(): void
|
||||
{
|
||||
$this->shell = shell()->cd($this->package->getSourceDir())->initializeEnv($this->package)->appendEnv([
|
||||
$this->shell = shell()->cd($this->package->getSourceRoot())->initializeEnv($this->package)->appendEnv([
|
||||
'CFLAGS' => "-I{$this->package->getIncludeDir()}",
|
||||
'CXXFLAGS' => "-I{$this->package->getIncludeDir()}",
|
||||
'LDFLAGS' => "-L{$this->package->getLibDir()}",
|
||||
|
||||
@@ -18,10 +18,10 @@ class ClangNativeToolchain implements UnixToolchainInterface
|
||||
{
|
||||
public function initEnv(): void
|
||||
{
|
||||
GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_CC=clang');
|
||||
GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_CXX=clang++');
|
||||
GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_AR=ar');
|
||||
GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_LD=ld');
|
||||
GlobalEnvManager::putenv('SPC_DEFAULT_CC=clang');
|
||||
GlobalEnvManager::putenv('SPC_DEFAULT_CXX=clang++');
|
||||
GlobalEnvManager::putenv('SPC_DEFAULT_AR=ar');
|
||||
GlobalEnvManager::putenv('SPC_DEFAULT_LD=ld');
|
||||
}
|
||||
|
||||
public function afterInit(): void
|
||||
@@ -52,6 +52,6 @@ class ClangNativeToolchain implements UnixToolchainInterface
|
||||
|
||||
public function isStatic(): bool
|
||||
{
|
||||
return PHP_OS_FAMILY === 'Linux' && LinuxUtil::isMuslDist();
|
||||
return PHP_OS_FAMILY === 'Linux' && LinuxUtil::isMuslDist() && !getenv('SPC_MUSL_DYNAMIC');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ class GccNativeToolchain implements UnixToolchainInterface
|
||||
{
|
||||
public function initEnv(): void
|
||||
{
|
||||
GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_CC=gcc');
|
||||
GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_CXX=g++');
|
||||
GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_AR=ar');
|
||||
GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_LD=ld');
|
||||
GlobalEnvManager::putenv('SPC_DEFAULT_CC=gcc');
|
||||
GlobalEnvManager::putenv('SPC_DEFAULT_CXX=g++');
|
||||
GlobalEnvManager::putenv('SPC_DEFAULT_AR=ar');
|
||||
GlobalEnvManager::putenv('SPC_DEFAULT_LD=ld');
|
||||
}
|
||||
|
||||
public function afterInit(): void
|
||||
@@ -49,6 +49,6 @@ class GccNativeToolchain implements UnixToolchainInterface
|
||||
|
||||
public function isStatic(): bool
|
||||
{
|
||||
return PHP_OS_FAMILY === 'Linux' && LinuxUtil::isMuslDist();
|
||||
return PHP_OS_FAMILY === 'Linux' && LinuxUtil::isMuslDist() && !getenv('SPC_MUSL_DYNAMIC');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ class MuslToolchain implements UnixToolchainInterface
|
||||
{
|
||||
$arch = getenv('GNU_ARCH');
|
||||
// Set environment variables for musl toolchain
|
||||
GlobalEnvManager::putenv("SPC_LINUX_DEFAULT_CC={$arch}-linux-musl-gcc");
|
||||
GlobalEnvManager::putenv("SPC_LINUX_DEFAULT_CXX={$arch}-linux-musl-g++");
|
||||
GlobalEnvManager::putenv("SPC_LINUX_DEFAULT_AR={$arch}-linux-musl-ar");
|
||||
GlobalEnvManager::putenv("SPC_LINUX_DEFAULT_LD={$arch}-linux-musl-ld");
|
||||
GlobalEnvManager::putenv("SPC_DEFAULT_CC={$arch}-linux-musl-gcc");
|
||||
GlobalEnvManager::putenv("SPC_DEFAULT_CXX={$arch}-linux-musl-g++");
|
||||
GlobalEnvManager::putenv("SPC_DEFAULT_AR={$arch}-linux-musl-ar");
|
||||
GlobalEnvManager::putenv("SPC_DEFAULT_LD={$arch}-linux-musl-ld");
|
||||
GlobalEnvManager::addPathIfNotExists('/usr/local/musl/bin');
|
||||
GlobalEnvManager::addPathIfNotExists("/usr/local/musl/{$arch}-linux-musl/bin");
|
||||
|
||||
@@ -40,7 +40,7 @@ class MuslToolchain implements UnixToolchainInterface
|
||||
|
||||
public function getCompilerInfo(): ?string
|
||||
{
|
||||
$compiler = getenv('CC') ?: getenv('SPC_LINUX_DEFAULT_CC');
|
||||
$compiler = getenv('CC') ?: getenv('SPC_DEFAULT_CC');
|
||||
$version = shell(false)->execWithResult("{$compiler} --version", false);
|
||||
$head = pathinfo($compiler, PATHINFO_BASENAME);
|
||||
if ($version[0] === 0 && preg_match('/linux-musl-cc.*(\d+.\d+.\d+)/', $version[1][0], $match)) {
|
||||
|
||||
@@ -55,6 +55,58 @@ class DependencyResolver
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all dependencies of a specific package within a resolved package set.
|
||||
* This is useful when you need to get build flags for a specific library and its deps.
|
||||
*
|
||||
* The method will only include dependencies that exist in the resolved set,
|
||||
* which properly handles optional dependencies (suggests) - only those that
|
||||
* were actually resolved will be included.
|
||||
*
|
||||
* @param string $package_name The package to get dependencies for
|
||||
* @param string[] $resolved_packages The resolved package list (from resolve())
|
||||
* @param bool $include_suggests Whether to include suggests that are in resolved set
|
||||
* @return string[] Dependencies of the package (NOT including itself), ordered for building
|
||||
*/
|
||||
public static function getSubDependencies(string $package_name, array $resolved_packages, bool $include_suggests = false): array
|
||||
{
|
||||
// Create a lookup set for O(1) membership check
|
||||
$resolved_set = array_flip($resolved_packages);
|
||||
|
||||
// Verify the target package is in the resolved set
|
||||
if (!isset($resolved_set[$package_name])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Build dependency map from config
|
||||
$dep_map = [];
|
||||
foreach ($resolved_packages as $pkg) {
|
||||
$dep_map[$pkg] = [
|
||||
'depends' => PackageConfig::get($pkg, 'depends', []),
|
||||
'suggests' => PackageConfig::get($pkg, 'suggests', []),
|
||||
];
|
||||
}
|
||||
|
||||
// Collect all sub-dependencies recursively (excluding the package itself)
|
||||
$visited = [];
|
||||
$sorted = [];
|
||||
|
||||
// Get dependencies to process for the target package
|
||||
$deps = $dep_map[$package_name]['depends'] ?? [];
|
||||
if ($include_suggests) {
|
||||
$deps = array_merge($deps, $dep_map[$package_name]['suggests'] ?? []);
|
||||
}
|
||||
|
||||
// Only visit dependencies that are in the resolved set
|
||||
foreach ($deps as $dep) {
|
||||
if (isset($resolved_set[$dep])) {
|
||||
self::visitSubDeps($dep, $dep_map, $resolved_set, $include_suggests, $visited, $sorted);
|
||||
}
|
||||
}
|
||||
|
||||
return $sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a reverse dependency map for the resolved packages.
|
||||
* For each package that is depended upon, list which packages depend on it.
|
||||
@@ -89,6 +141,39 @@ class DependencyResolver
|
||||
return $why;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive helper for getSubDependencies.
|
||||
* Visits dependencies in topological order (dependencies first).
|
||||
*/
|
||||
private static function visitSubDeps(
|
||||
string $pkg_name,
|
||||
array $dep_map,
|
||||
array $resolved_set,
|
||||
bool $include_suggests,
|
||||
array &$visited,
|
||||
array &$sorted
|
||||
): void {
|
||||
if (isset($visited[$pkg_name])) {
|
||||
return;
|
||||
}
|
||||
$visited[$pkg_name] = true;
|
||||
|
||||
// Get dependencies to process
|
||||
$deps = $dep_map[$pkg_name]['depends'] ?? [];
|
||||
if ($include_suggests) {
|
||||
$deps = array_merge($deps, $dep_map[$pkg_name]['suggests'] ?? []);
|
||||
}
|
||||
|
||||
// Only visit dependencies that are in the resolved set
|
||||
foreach ($deps as $dep) {
|
||||
if (isset($resolved_set[$dep])) {
|
||||
self::visitSubDeps($dep, $dep_map, $resolved_set, $include_suggests, $visited, $sorted);
|
||||
}
|
||||
}
|
||||
|
||||
$sorted[] = $pkg_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor pattern implementation for dependency resolution.
|
||||
*
|
||||
|
||||
@@ -75,7 +75,7 @@ class LicenseDumper
|
||||
*
|
||||
* @param Artifact $artifact Artifact instance
|
||||
* @param string $target_dir Target directory
|
||||
* @param array<string, array> &$license_summary Summary data to populate
|
||||
* @param array &$license_summary Summary data to populate
|
||||
* @return bool True if dumped
|
||||
* @throws SPCInternalException
|
||||
*/
|
||||
@@ -163,18 +163,26 @@ class LicenseDumper
|
||||
{
|
||||
$artifact_name = $artifact->getName();
|
||||
|
||||
// Try source directory first (if extracted)
|
||||
if ($artifact->isSourceExtracted()) {
|
||||
$source_dir = $artifact->getSourceDir();
|
||||
$full_path = "{$source_dir}/{$license_file_path}";
|
||||
// replace
|
||||
if (str_starts_with($license_file_path, '@/')) {
|
||||
$license_file_path = str_replace('@/', ROOT_DIR . '/src/globals/licenses/', $license_file_path);
|
||||
}
|
||||
|
||||
$source_dir = $artifact->getSourceDir();
|
||||
if (FileSystem::isRelativePath($license_file_path)) {
|
||||
$full_path = "{$source_dir}/{$license_file_path}";
|
||||
} else {
|
||||
$full_path = $license_file_path;
|
||||
}
|
||||
// Try source directory first (if extracted)
|
||||
if ($artifact->isSourceExtracted() || file_exists($full_path)) {
|
||||
logger()->debug("Checking license file: {$full_path}");
|
||||
if (file_exists($full_path)) {
|
||||
logger()->info("Reading license from source: {$full_path}");
|
||||
return file_get_contents($full_path);
|
||||
}
|
||||
} else {
|
||||
logger()->debug("Artifact source not extracted: {$artifact_name}");
|
||||
logger()->warning("Artifact source not extracted: {$artifact_name}");
|
||||
}
|
||||
|
||||
// Fallback: try SOURCE_PATH directly
|
||||
|
||||
@@ -28,7 +28,12 @@ class PkgConfigUtil
|
||||
];
|
||||
$found = null;
|
||||
foreach ($find_list as $file) {
|
||||
if (file_exists($file) && is_executable($file)) {
|
||||
$exists = file_exists($file);
|
||||
$executable = is_executable($file);
|
||||
if (!$exists) {
|
||||
continue;
|
||||
}
|
||||
if (!$executable && chmod($file, 0755) || $executable) {
|
||||
$found = $file;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -149,6 +149,117 @@ class SPCConfigUtil
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get build configuration for a package and its sub-dependencies within a resolved set.
|
||||
*
|
||||
* This is useful when you need to statically link something against a specific
|
||||
* library and all its transitive dependencies. It properly handles optional
|
||||
* dependencies by only including those that were actually resolved.
|
||||
*
|
||||
* @param string $package_name The package to get config for
|
||||
* @param string[] $resolved_packages The full resolved package list
|
||||
* @param bool $include_suggests Whether to include resolved suggests
|
||||
* @return array{
|
||||
* cflags: string,
|
||||
* ldflags: string,
|
||||
* libs: string
|
||||
* }
|
||||
*/
|
||||
public function getPackageDepsConfig(string $package_name, array $resolved_packages, bool $include_suggests = false): array
|
||||
{
|
||||
// Get sub-dependencies within the resolved set
|
||||
$sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, $include_suggests);
|
||||
|
||||
if (empty($sub_deps)) {
|
||||
return [
|
||||
'cflags' => '',
|
||||
'ldflags' => '',
|
||||
'libs' => '',
|
||||
];
|
||||
}
|
||||
|
||||
// Use libs_only_deps mode and no_php for library linking
|
||||
$save_no_php = $this->no_php;
|
||||
$save_libs_only_deps = $this->libs_only_deps;
|
||||
$this->no_php = true;
|
||||
$this->libs_only_deps = true;
|
||||
|
||||
$ret = $this->configWithResolvedPackages($sub_deps);
|
||||
|
||||
$this->no_php = $save_no_php;
|
||||
$this->libs_only_deps = $save_libs_only_deps;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration using already-resolved packages (skip dependency resolution).
|
||||
*
|
||||
* @param string[] $resolved_packages Already resolved package names in build order
|
||||
* @return array{
|
||||
* cflags: string,
|
||||
* ldflags: string,
|
||||
* libs: string
|
||||
* }
|
||||
*/
|
||||
public function configWithResolvedPackages(array $resolved_packages): array
|
||||
{
|
||||
$ldflags = $this->getLdflagsString();
|
||||
$cflags = $this->getIncludesString($resolved_packages);
|
||||
$libs = $this->getLibsString($resolved_packages, !$this->absolute_libs);
|
||||
|
||||
// additional OS-specific libraries (e.g. macOS -lresolv)
|
||||
if ($extra_libs = SystemTarget::getRuntimeLibs()) {
|
||||
$libs .= " {$extra_libs}";
|
||||
}
|
||||
|
||||
$extra_env = getenv('SPC_EXTRA_LIBS');
|
||||
if (is_string($extra_env) && !empty($extra_env)) {
|
||||
$libs .= " {$extra_env}";
|
||||
}
|
||||
|
||||
// package frameworks
|
||||
if (SystemTarget::getTargetOS() === 'Darwin') {
|
||||
$libs .= " {$this->getFrameworksString($resolved_packages)}";
|
||||
}
|
||||
|
||||
// C++
|
||||
if ($this->hasCpp($resolved_packages)) {
|
||||
$libcpp = SystemTarget::getTargetOS() === 'Darwin' ? '-lc++' : '-lstdc++';
|
||||
$libs = str_replace($libcpp, '', $libs) . " {$libcpp}";
|
||||
}
|
||||
|
||||
if ($this->libs_only_deps) {
|
||||
// mimalloc must come first
|
||||
if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) {
|
||||
$libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs);
|
||||
}
|
||||
return [
|
||||
'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags),
|
||||
'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags),
|
||||
'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs),
|
||||
];
|
||||
}
|
||||
|
||||
// embed
|
||||
if (!$this->no_php) {
|
||||
$libs = "-lphp {$libs} -lc";
|
||||
}
|
||||
|
||||
$allLibs = getenv('LIBS') . ' ' . $libs;
|
||||
|
||||
// mimalloc must come first
|
||||
if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) {
|
||||
$allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs);
|
||||
}
|
||||
|
||||
return [
|
||||
'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags),
|
||||
'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags),
|
||||
'libs' => clean_spaces($allLibs),
|
||||
];
|
||||
}
|
||||
|
||||
private function hasCpp(array $packages): bool
|
||||
{
|
||||
foreach ($packages as $package) {
|
||||
|
||||
@@ -44,7 +44,7 @@ abstract class UnixUtil
|
||||
continue;
|
||||
}
|
||||
$name = preg_replace('/@.*$/', '', $name);
|
||||
if ($name !== '' && $name !== false) {
|
||||
if (!empty($name)) {
|
||||
$defined[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user