#!/usr/bin/env php \n"; exit(in_array('-h', $argv, true) || in_array('--help', $argv, true) ? 0 : 1); } define('TMP_DIR', __DIR__ . '/tmp'); if (!is_dir(TMP_DIR)) { mkdir(TMP_DIR); } echo "# FETCHING DATA\n"; echo 'Retrieving package versions... '; $versions = listPackageVersions($argv[1]); $numVersions = count($versions); echo "done.\n"; $versionInfos = []; $versionCount = 0; foreach ($versions as $version) { $versionCount++; echo "Inspecting version {$version} [{$versionCount}/{$numVersions}]... "; $versionInfos[] = inspectPackageVersion($argv[1], $version); echo "done.\n"; } echo "# PHP VERSION COMPATIBILITY LIST\n"; $instance = null; foreach ($versionInfos as $versionInfo) { if ($instance === null || !$instance->isCompatibleWith($versionInfo)) { if ($instance !== null) { echo $instance, "\n"; } $instance = new CompatiblePHPVersions($versionInfo->getMinPHPVersion(), $versionInfo->getMaxPHPVersion()); } $instance->addVersion($versionInfo->getVersion()); } if ($instance !== null) { echo $instance, "\n"; } echo "# CONFIGURATION OPTIONS\n"; $instance = null; foreach ($versionInfos as $versionInfo) { if ($instance === null || !$instance->isCompatibleWith($versionInfo)) { if ($instance !== null) { echo $instance, "\n"; } $instance = new CompatibleConfigurationOptions($versionInfo->getConfigureOptions()); } $instance->addVersion($versionInfo->getVersion()); } if ($instance !== null) { echo $instance, "\n"; } } catch (RuntimeException $x) { echo $x->getMessage(), "\n"; exit(1); } /** * @return string[] */ function listPackageVersions(string $package): array { $cachedDir = TMP_DIR; $cachedFile = "{$cachedDir}/{$package}.xml"; if (is_file($cachedFile) && filemtime($cachedFile) > (time() - 1800)) { return listPackageVersionsXml($package, file_get_contents($cachedFile)); } try { $xml = file_get_contents("https://pecl.php.net/rest/r/{$package}/allreleases.xml"); } catch (RuntimeException $x) { if (strpos($x->getMessage(), '404 Not Found') !== false) { throw new RuntimeException("Invalid PECL package name or no versions available for {$package}"); } throw $x; } $versions = listPackageVersionsXml($package, $xml); if (!is_dir($cachedDir)) { mkdir($cachedDir, 0777, true); } file_put_contents($cachedFile, $xml); return $versions; } function listPackageVersionsXml(string $package, string $xml): array { $dom = new DOMDocument(); if (!$dom->loadXML($xml)) { throw new RuntimeException('Failed to parse the downloaded XML data.'); } $xpath = new DOMXPath($dom); $xpath->registerNamespace('v', 'http://pear.php.net/dtd/rest.allreleases'); $versionNodes = $xpath->query('/v:a/v:r/v:v'); if ($versionNodes->count() === 0) { throw new RuntimeException("No versions available for {$package}"); } $versions = []; foreach ($versionNodes as $versionNode) { $versions[] = $versionNode->nodeValue; } usort($versions, 'version_compare'); return $versions; } function inspectPackageVersion(string $package, string $version): PackageVersionInfo { $cachedDir = TMP_DIR . "/{$package}"; $cachedFile = "{$cachedDir}/{$version}.xml"; if (is_file($cachedFile)) { return inspectPackageVersionXml($package, $version, $cachedFile); } $tgzFile = tempnam(sys_get_temp_dir(), 'dpx'); try { file_put_contents($tgzFile, file_get_contents("https://pecl.php.net/get/{$package}-{$version}.tgz")); $archive = new PharData($tgzFile); $archive->decompress('tar'); $tarFile = preg_replace('/\.\w+$/', '.tar', $tgzFile); try { $extractedDir = preg_replace('/\.tar$/', '.decompressed', $tarFile); mkdir($extractedDir); try { try { $archive->extractTo($extractedDir, 'package2.xml'); rename("{$extractedDir}/package2.xml", "{$extractedDir}/package.xml"); } catch (PharException $x) { $archive->extractTo($extractedDir, 'package.xml'); } try { $info = inspectPackageVersionXml($package, $version, "{$extractedDir}/package.xml"); if (!is_dir($cachedDir)) { mkdir($cachedDir, 0777, true); } copy("{$extractedDir}/package.xml", $cachedFile); } finally { unlink("{$extractedDir}/package.xml"); } } finally { rmdir($extractedDir); } } finally { unlink($tarFile); } } finally { unlink($tgzFile); } return $info; } function inspectPackageVersionXml(string $package, string $version, string $xmlFile): PackageVersionInfo { $dom = new DOMDocument(); if (!$dom->loadXML(file_get_contents($xmlFile))) { throw new RuntimeException("Failed to parse the downloaded package.xml file located at {$xmlFile}."); } $xpath = new DOMXPath($dom); $xpath->registerNamespace('v2', 'http://pear.php.net/dtd/package-2.0'); if ($xpath->query('/v2:package/v2:dependencies')->count() === 1) { $ns = 'v2:'; } elseif ($xpath->query('/package/release/version')->count() === 1) { $ns = ''; } else { throw new RuntimeException('Unsupported namespace'); } $minPHPVersionNodes = $xpath->query("/{$ns}package/{$ns}dependencies/{$ns}required/{$ns}php/{$ns}min"); $maxPHPVersionNodes = $xpath->query("/{$ns}package/{$ns}dependencies/{$ns}required/{$ns}php/{$ns}max"); $info = new PackageVersionInfo( $version, $minPHPVersionNodes->count() === 0 ? '' : $minPHPVersionNodes[0]->nodeValue, $maxPHPVersionNodes->count() === 0 ? '' : $maxPHPVersionNodes[0]->nodeValue ); foreach ($xpath->query("/{$ns}package/{$ns}extsrcrelease/{$ns}configureoption") as $configureOptionNode) { $info->addConfigureOption(new PackageConfigureOption( $configureOptionNode->getAttribute('name'), $configureOptionNode->getAttribute('prompt'), $configureOptionNode->hasAttribute('default') ? $configureOptionNode->getAttribute('default') : null )); } return $info; } class PackageVersionInfo { private string $version; private string $minPHPVersion; private string $maxPHPVersion; private array $configureOptions = []; public function __construct(string $version, string $minPHPVersion, string $maxPHPVersion = '') { $this->version = $version; $this->minPHPVersion = $minPHPVersion; $this->maxPHPVersion = $maxPHPVersion; } public function getVersion(): string { return $this->version; } public function getMinPHPVersion(): string { return $this->minPHPVersion; } public function getMaxPHPVersion(): string { return $this->maxPHPVersion; } /** * @return $this */ public function addConfigureOption(PackageConfigureOption $value): self { $this->configureOptions[] = $value; return $this; } /** * @return PackageConfigureOption[] */ public function getConfigureOptions(): array { return $this->configureOptions; } } class PackageConfigureOption { private string $name; private string $prompt; private ?string $default; public function __construct(string $name, string $prompt, ?string $default = null) { $this->name = $name; $this->prompt = $prompt; $this->default = $default; } public function __toString(): string { $result = "{$this->getName()} \"{$this->getPrompt()}\""; if ($this->getDefault() !== null) { $result .= " [{$this->getDefault()}]"; } return $result; } public function getName(): string { return $this->name; } public function getPrompt(): string { return $this->prompt; } public function getDefault(): ?string { return $this->default; } } class CompatiblePHPVersions { private string $minPHPVersion; private string $maxPHPVersion; private array $versions = []; public function __construct(string $minPHPVersion, string $maxPHPVersion) { $this->minPHPVersion = $minPHPVersion; $this->maxPHPVersion = $maxPHPVersion; } public function __toString(): string { $versions = $this->getVersions(); return implode("\n", [ "- Package versions: {$versions[0]}" . (isset($versions[1]) ? ' -> ' . $versions[count($versions) - 1] : ''), " - Min PHP version: {$this->getMinPHPVersion()}", " - Max PHP version: {$this->getMaxPHPVersion()}", ]); } public function getMinPHPVersion(): string { return $this->minPHPVersion; } public function getMaxPHPVersion(): string { return $this->maxPHPVersion; } /** * @return $this */ public function addVersion(string $value): self { $this->versions[] = $value; return $this; } /** * @return string[] */ public function getVersions(): array { return $this->versions; } public function isCompatibleWith(PackageVersionInfo $info): bool { return $this->getMinPHPVersion() === $info->getMinPHPVersion() && $this->getMaxPHPVersion() === $info->getMaxPHPVersion(); } } class CompatibleConfigurationOptions { /** * @var PackageConfigureOption[] */ private array $configureOptions = []; private array $versions = []; public function __construct(array $configureOptions) { $this->configureOptions = $configureOptions; } public function __toString(): string { $versions = $this->getVersions(); $lines = [ "- Package versions: {$versions[0]}" . (isset($versions[1]) ? ' -> ' . $versions[count($versions) - 1] : ''), ]; $options = $this->getConfigureOptions(); if ($options === []) { $lines[] = ' '; } else { foreach ($options as $index => $option) { $lines[] = $index + 1 . ') ' . (string) $option; } } return implode("\n", $lines); } /** * @return PackageConfigureOption[] */ public function getConfigureOptions(): array { return $this->configureOptions; } /** * @return $this */ public function addVersion(string $value): self { $this->versions[] = $value; return $this; } /** * @return string[] */ public function getVersions(): array { return $this->versions; } public function isCompatibleWith(PackageVersionInfo $info): bool { return $this->getConfigureOptions() == $info->getConfigureOptions(); } }