diff --git a/.gitignore b/.gitignore index d33eae53..2a351fcd 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ log/ # spc.phar spc.phar spc.exe + +# dumped files from StaticPHP v3 +/dump-*.json diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index dc602538..6dc35ad5 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -237,6 +237,39 @@ class Artifact 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 { return $this->config[$type] ?? null; diff --git a/src/StaticPHP/Command/Dev/PackageInfoCommand.php b/src/StaticPHP/Command/Dev/PackageInfoCommand.php new file mode 100644 index 00000000..7c869199 --- /dev/null +++ b/src/StaticPHP/Command/Dev/PackageInfoCommand.php @@ -0,0 +1,193 @@ +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("Package '{$packageName}' not found."); + 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("Package: {$name} Type: {$type} Registry: {$registry}"); + $this->output->writeln("Config file: {$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('── Package Config ──'); + $this->printYamlBlock($pkgFields, 0); + $this->output->writeln(''); + } + + // Artifact config + if ($artifactConfig !== null) { + $artifactFile = $artifactInfo ? $this->toRelativePath($artifactInfo['config']) : 'unknown'; + $this->output->writeln("── Artifact Config ── file: {$artifactFile}"); + + // Check if artifact config is inline (embedded in pkg config) or separate + $inlineArtifact = $pkgConfig['artifact'] ?? null; + if (is_array($inlineArtifact)) { + $this->output->writeln(' (inline in package config)'); + } + + $split = $this->splitArtifactConfig($artifactConfig); + + foreach ($split as $section => $value) { + $this->output->writeln(''); + $this->output->writeln(" [{$section}]"); + $this->printYamlBlock($value, 4); + } + $this->output->writeln(''); + } else { + $this->output->writeln('── Artifact Config ── (none)'); + $this->output->writeln(''); + } + + return static::SUCCESS; + } + + /** + * Split artifact config into logical sections for cleaner display. + * + * @return array + */ + 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 . "{$k}:"); + $this->printYamlBlock($v, $indent + 2); + } else { + $this->output->writeln($pad . "{$k}: " . $this->colorScalar($v)); + } + } + } + } + + private function colorScalar(mixed $v): string + { + if (is_bool($v)) { + return '' . ($v ? 'true' : 'false') . ''; + } + if (is_int($v) || is_float($v)) { + return '' . $v . ''; + } + if ($v === null) { + return 'null'; + } + // Strings that look like URLs + if (is_string($v) && (str_starts_with($v, 'http://') || str_starts_with($v, 'https://'))) { + return '' . $v . ''; + } + return '' . $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; + } +} diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index fd59e98b..1ec1a503 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -138,6 +138,16 @@ abstract class Package 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. * diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 582216d7..07bc6abd 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -280,6 +280,27 @@ class PhpExtensionPackage extends Package $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 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. * This is called after all attributes have been loaded.