mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-03-18 04:44:53 +08:00
Add PackageInfoCommand to display package configuration information and support status
This commit is contained in:
parent
cfce177070
commit
28c82b811b
3
.gitignore
vendored
3
.gitignore
vendored
@ -61,3 +61,6 @@ log/
|
|||||||
# spc.phar
|
# spc.phar
|
||||||
spc.phar
|
spc.phar
|
||||||
spc.exe
|
spc.exe
|
||||||
|
|
||||||
|
# dumped files from StaticPHP v3
|
||||||
|
/dump-*.json
|
||||||
|
|||||||
@ -237,6 +237,39 @@ class Artifact
|
|||||||
return isset($this->config['binary'][$target]) || isset($this->custom_binary_callbacks[$target]);
|
return isset($this->config['binary'][$target]) || isset($this->custom_binary_callbacks[$target]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all platform strings for which a binary is declared (config or custom callback).
|
||||||
|
*
|
||||||
|
* For platforms where the binary type is "custom", a registered custom_binary_callback
|
||||||
|
* is required to consider it truly installable.
|
||||||
|
*
|
||||||
|
* @return string[] e.g. ['linux-x86_64', 'linux-aarch64', 'macos-aarch64']
|
||||||
|
*/
|
||||||
|
public function getBinaryPlatforms(): array
|
||||||
|
{
|
||||||
|
$platforms = [];
|
||||||
|
if (isset($this->config['binary']) && is_array($this->config['binary'])) {
|
||||||
|
foreach ($this->config['binary'] as $platform => $platformConfig) {
|
||||||
|
$type = is_array($platformConfig) ? ($platformConfig['type'] ?? '') : '';
|
||||||
|
if ($type === 'custom') {
|
||||||
|
// Only installable if a custom callback has been registered
|
||||||
|
if (isset($this->custom_binary_callbacks[$platform])) {
|
||||||
|
$platforms[] = $platform;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$platforms[] = $platform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Include custom callbacks for platforms not listed in config at all
|
||||||
|
foreach (array_keys($this->custom_binary_callbacks) as $platform) {
|
||||||
|
if (!in_array($platform, $platforms, true)) {
|
||||||
|
$platforms[] = $platform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $platforms;
|
||||||
|
}
|
||||||
|
|
||||||
public function getDownloadConfig(string $type): mixed
|
public function getDownloadConfig(string $type): mixed
|
||||||
{
|
{
|
||||||
return $this->config[$type] ?? null;
|
return $this->config[$type] ?? null;
|
||||||
|
|||||||
193
src/StaticPHP/Command/Dev/PackageInfoCommand.php
Normal file
193
src/StaticPHP/Command/Dev/PackageInfoCommand.php
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace StaticPHP\Command\Dev;
|
||||||
|
|
||||||
|
use StaticPHP\Command\BaseCommand;
|
||||||
|
use StaticPHP\Config\ArtifactConfig;
|
||||||
|
use StaticPHP\Config\PackageConfig;
|
||||||
|
use StaticPHP\Registry\Registry;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
|
#[AsCommand('dev:info', 'Display configuration information for a package')]
|
||||||
|
class PackageInfoCommand extends BaseCommand
|
||||||
|
{
|
||||||
|
protected bool $no_motd = true;
|
||||||
|
|
||||||
|
public function configure(): void
|
||||||
|
{
|
||||||
|
$this->addArgument('package', InputArgument::REQUIRED, 'Package name to inspect');
|
||||||
|
$this->addOption('json', null, InputOption::VALUE_NONE, 'Output as JSON instead of colored terminal display');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$packageName = $this->getArgument('package');
|
||||||
|
|
||||||
|
if (!PackageConfig::isPackageExists($packageName)) {
|
||||||
|
$this->output->writeln("<error>Package '{$packageName}' not found.</error>");
|
||||||
|
return static::USER_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pkgConfig = PackageConfig::get($packageName);
|
||||||
|
$artifactConfig = ArtifactConfig::get($packageName);
|
||||||
|
$pkgInfo = Registry::getPackageConfigInfo($packageName);
|
||||||
|
$artifactInfo = Registry::getArtifactConfigInfo($packageName);
|
||||||
|
|
||||||
|
if ($this->getOption('json')) {
|
||||||
|
return $this->outputJson($packageName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->outputTerminal($packageName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function outputJson(string $name, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo): int
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'name' => $name,
|
||||||
|
'registry' => $pkgInfo['registry'] ?? null,
|
||||||
|
'package_config_file' => $pkgInfo ? $this->toRelativePath($pkgInfo['config']) : null,
|
||||||
|
'package' => $pkgConfig,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($artifactConfig !== null) {
|
||||||
|
$data['artifact_config_file'] = $artifactInfo ? $this->toRelativePath($artifactInfo['config']) : null;
|
||||||
|
$data['artifact'] = $this->splitArtifactConfig($artifactConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->output->writeln(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
||||||
|
return static::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function outputTerminal(string $name, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo): int
|
||||||
|
{
|
||||||
|
$type = $pkgConfig['type'] ?? 'unknown';
|
||||||
|
$registry = $pkgInfo['registry'] ?? 'unknown';
|
||||||
|
$pkgFile = $pkgInfo ? $this->toRelativePath($pkgInfo['config']) : 'unknown';
|
||||||
|
|
||||||
|
// Header
|
||||||
|
$this->output->writeln('');
|
||||||
|
$this->output->writeln("<info>Package:</info> <comment>{$name}</comment> <info>Type:</info> <comment>{$type}</comment> <info>Registry:</info> <comment>{$registry}</comment>");
|
||||||
|
$this->output->writeln("<info>Config file:</info> {$pkgFile}");
|
||||||
|
$this->output->writeln('');
|
||||||
|
|
||||||
|
// Package config fields (excluding type and artifact which are shown separately)
|
||||||
|
$pkgFields = array_diff_key($pkgConfig, array_flip(['type', 'artifact']));
|
||||||
|
if (!empty($pkgFields)) {
|
||||||
|
$this->output->writeln('<comment>── Package Config ──</comment>');
|
||||||
|
$this->printYamlBlock($pkgFields, 0);
|
||||||
|
$this->output->writeln('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Artifact config
|
||||||
|
if ($artifactConfig !== null) {
|
||||||
|
$artifactFile = $artifactInfo ? $this->toRelativePath($artifactInfo['config']) : 'unknown';
|
||||||
|
$this->output->writeln("<comment>── Artifact Config ──</comment> <info>file:</info> {$artifactFile}");
|
||||||
|
|
||||||
|
// Check if artifact config is inline (embedded in pkg config) or separate
|
||||||
|
$inlineArtifact = $pkgConfig['artifact'] ?? null;
|
||||||
|
if (is_array($inlineArtifact)) {
|
||||||
|
$this->output->writeln('<info> (inline in package config)</info>');
|
||||||
|
}
|
||||||
|
|
||||||
|
$split = $this->splitArtifactConfig($artifactConfig);
|
||||||
|
|
||||||
|
foreach ($split as $section => $value) {
|
||||||
|
$this->output->writeln('');
|
||||||
|
$this->output->writeln(" <info>[{$section}]</info>");
|
||||||
|
$this->printYamlBlock($value, 4);
|
||||||
|
}
|
||||||
|
$this->output->writeln('');
|
||||||
|
} else {
|
||||||
|
$this->output->writeln('<comment>── Artifact Config ──</comment> <fg=gray>(none)</>');
|
||||||
|
$this->output->writeln('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return static::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split artifact config into logical sections for cleaner display.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function splitArtifactConfig(array $config): array
|
||||||
|
{
|
||||||
|
$sections = [];
|
||||||
|
$sectionOrder = ['source', 'source-mirror', 'binary', 'binary-mirror', 'metadata'];
|
||||||
|
foreach ($sectionOrder as $key) {
|
||||||
|
if (array_key_exists($key, $config)) {
|
||||||
|
$sections[$key] = $config[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Any remaining unknown keys
|
||||||
|
foreach ($config as $k => $v) {
|
||||||
|
if (!array_key_exists($k, $sections)) {
|
||||||
|
$sections[$k] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print a value as indented YAML-style output with Symfony Console color tags.
|
||||||
|
*/
|
||||||
|
private function printYamlBlock(mixed $value, int $indent): void
|
||||||
|
{
|
||||||
|
$pad = str_repeat(' ', $indent);
|
||||||
|
if (!is_array($value)) {
|
||||||
|
$this->output->writeln($pad . $this->colorScalar($value));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$isList = array_is_list($value);
|
||||||
|
foreach ($value as $k => $v) {
|
||||||
|
if ($isList) {
|
||||||
|
if (is_array($v)) {
|
||||||
|
$this->output->writeln($pad . '- ');
|
||||||
|
$this->printYamlBlock($v, $indent + 2);
|
||||||
|
} else {
|
||||||
|
$this->output->writeln($pad . '- ' . $this->colorScalar($v));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (is_array($v)) {
|
||||||
|
$this->output->writeln($pad . "<fg=cyan>{$k}</>:");
|
||||||
|
$this->printYamlBlock($v, $indent + 2);
|
||||||
|
} else {
|
||||||
|
$this->output->writeln($pad . "<fg=cyan>{$k}</>: " . $this->colorScalar($v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function colorScalar(mixed $v): string
|
||||||
|
{
|
||||||
|
if (is_bool($v)) {
|
||||||
|
return '<fg=yellow>' . ($v ? 'true' : 'false') . '</>';
|
||||||
|
}
|
||||||
|
if (is_int($v) || is_float($v)) {
|
||||||
|
return '<fg=yellow>' . $v . '</>';
|
||||||
|
}
|
||||||
|
if ($v === null) {
|
||||||
|
return '<fg=gray>null</>';
|
||||||
|
}
|
||||||
|
// Strings that look like URLs
|
||||||
|
if (is_string($v) && (str_starts_with($v, 'http://') || str_starts_with($v, 'https://'))) {
|
||||||
|
return '<fg=blue>' . $v . '</>';
|
||||||
|
}
|
||||||
|
return '<fg=green>' . $v . '</>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function toRelativePath(string $absolutePath): string
|
||||||
|
{
|
||||||
|
$normalized = realpath($absolutePath) ?: $absolutePath;
|
||||||
|
$root = rtrim(ROOT_DIR, '/') . '/';
|
||||||
|
if (str_starts_with($normalized, $root)) {
|
||||||
|
return substr($normalized, strlen($root));
|
||||||
|
}
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -138,6 +138,16 @@ abstract class Package
|
|||||||
return $this->stages;
|
return $this->stages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of OS families that have a registered build function (via #[BuildFor]).
|
||||||
|
*
|
||||||
|
* @return string[] e.g. ['Linux', 'Darwin']
|
||||||
|
*/
|
||||||
|
public function getBuildForOSList(): array
|
||||||
|
{
|
||||||
|
return array_keys($this->build_functions);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the package has a specific stage defined.
|
* Check if the package has a specific stage defined.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -280,6 +280,27 @@ class PhpExtensionPackage extends Package
|
|||||||
$builder->deployBinary($soFile, $soFile, false);
|
$builder->deployBinary($soFile, $soFile, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get per-OS build support status for this php-extension.
|
||||||
|
*
|
||||||
|
* Rules (same as v2):
|
||||||
|
* - OS not listed in 'support' config => 'yes' (fully supported)
|
||||||
|
* - OS listed with 'wip' => 'wip'
|
||||||
|
* - OS listed with 'partial' => 'partial'
|
||||||
|
* - OS listed with 'no' => 'no'
|
||||||
|
*
|
||||||
|
* @return array<string, string> e.g. ['Linux' => 'yes', 'Darwin' => 'partial', 'Windows' => 'no']
|
||||||
|
*/
|
||||||
|
public function getBuildSupportStatus(): array
|
||||||
|
{
|
||||||
|
$exceptions = $this->extension_config['support'] ?? [];
|
||||||
|
$result = [];
|
||||||
|
foreach (['Linux', 'Darwin', 'Windows'] as $os) {
|
||||||
|
$result[$os] = $exceptions[$os] ?? 'yes';
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register default stages if not already defined by attributes.
|
* Register default stages if not already defined by attributes.
|
||||||
* This is called after all attributes have been loaded.
|
* This is called after all attributes have been loaded.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user