Improve PECL package inspector

pull/224/head
Michele Locati 2020-12-15 09:30:48 +01:00
parent c2d8c3d37b
commit 01464a72d4
No known key found for this signature in database
GPG Key ID: 98B7CE2E7234E28B
2 changed files with 210 additions and 89 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/scripts/tmp/
/vendor/ /vendor/
/.php_cs.cache /.php_cs.cache
/composer.lock /composer.lock

View File

@ -21,35 +21,51 @@ try {
echo "Syntax: {$argv[0]} <extension>\n"; echo "Syntax: {$argv[0]} <extension>\n";
exit(in_array('-h', $argv, true) || in_array('--help', $argv, true) ? 0 : 1); 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... '; echo 'Retrieving package versions... ';
$versions = listPackageVersions($argv[1]); $versions = listPackageVersions($argv[1]);
$numVersions = count($versions); $numVersions = count($versions);
echo "done.\n"; echo "done.\n";
$data = []; $versionInfos = [];
$lastData = null;
$versionCount = 0; $versionCount = 0;
foreach ($versions as $version) { foreach ($versions as $version) {
$versionCount++; $versionCount++;
echo "Inspecting version {$version} [{$versionCount}/{$numVersions}]... "; echo "Inspecting version {$version} [{$versionCount}/{$numVersions}]... ";
$versionData = inspectPackageVersion($argv[1], $version); $versionInfos[] = inspectPackageVersion($argv[1], $version);
if ($lastData !== null && $lastData->isCompatibleWith($versionData)) {
$lastData->addVersion($versionData->getVersion());
} else {
$data[] = $lastData = new PackageVersionCombined($versionData);
}
echo "done.\n"; echo "done.\n";
} }
foreach ($data as $info) { echo "# PHP VERSION COMPATIBILITY LIST\n";
echo '## Versions: ', implode(', ', $info->getVersions()), "\n"; $instance = null;
echo 'Min PHP version: ', $info->getMinPHPVersion(), "\n"; foreach ($versionInfos as $versionInfo) {
echo 'Max PHP version: ', $info->getMaxPHPVersion(), "\n"; if ($instance === null || !$instance->isCompatibleWith($versionInfo)) {
foreach ($info->getConfigureOptions() as $configureOptionIndex => $configureOption) { if ($instance !== null) {
echo 'Option ', $configureOptionIndex + 1, ': ', $configureOption->getName(), ' "', $configureOption->getPrompt(), '"'; echo $instance, "\n";
if ($configureOption->getDefault() !== null) {
echo ' [', $configureOption->getDefault(), ']';
} }
echo "\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) { } catch (RuntimeException $x) {
echo $x->getMessage(), "\n"; echo $x->getMessage(), "\n";
@ -61,6 +77,12 @@ try {
*/ */
function listPackageVersions(string $package): array 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 { try {
$xml = file_get_contents("https://pecl.php.net/rest/r/{$package}/allreleases.xml"); $xml = file_get_contents("https://pecl.php.net/rest/r/{$package}/allreleases.xml");
} catch (RuntimeException $x) { } catch (RuntimeException $x) {
@ -70,6 +92,17 @@ function listPackageVersions(string $package): array
throw $x; 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(); $dom = new DOMDocument();
if (!$dom->loadXML($xml)) { if (!$dom->loadXML($xml)) {
throw new RuntimeException('Failed to parse the downloaded XML data.'); throw new RuntimeException('Failed to parse the downloaded XML data.');
@ -89,8 +122,13 @@ function listPackageVersions(string $package): array
return $versions; return $versions;
} }
function inspectPackageVersion(string $package, string $version): PackageVersion 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'); $tgzFile = tempnam(sys_get_temp_dir(), 'dpx');
try { try {
@ -112,33 +150,11 @@ function inspectPackageVersion(string $package, string $version): PackageVersion
} }
try { try {
$dom = new DOMDocument(); $info = inspectPackageVersionXml($package, $version, "{$extractedDir}/package.xml");
if (!$dom->loadXML(file_get_contents("{$extractedDir}/package.xml"))) { if (!is_dir($cachedDir)) {
throw new RuntimeException('Failed to parse the downloaded package.xml file.'); mkdir($cachedDir, 0777, true);
}
$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 PackageVersion(
$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
));
} }
copy("{$extractedDir}/package.xml", $cachedFile);
} finally { } finally {
unlink("{$extractedDir}/package.xml"); unlink("{$extractedDir}/package.xml");
} }
@ -155,18 +171,58 @@ function inspectPackageVersion(string $package, string $version): PackageVersion
return $info; return $info;
} }
abstract class PackageInfo 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 $minPHPVersion;
private string $maxPHPVersion; private string $maxPHPVersion;
private array $configureOptions = []; private array $configureOptions = [];
protected function __construct(string $minPHPVersion, string $maxPHPVersion = '') public function __construct(string $version, string $minPHPVersion, string $maxPHPVersion = '')
{ {
$this->version = $version;
$this->minPHPVersion = $minPHPVersion; $this->minPHPVersion = $minPHPVersion;
$this->maxPHPVersion = $maxPHPVersion; $this->maxPHPVersion = $maxPHPVersion;
} }
public function getVersion(): string
{
return $this->version;
}
public function getMinPHPVersion(): string public function getMinPHPVersion(): string
{ {
return $this->minPHPVersion; return $this->minPHPVersion;
@ -187,18 +243,6 @@ abstract class PackageInfo
return $this; return $this;
} }
/**
* @param PackageConfigureOption[] $value
*
* @return $this
*/
public function setConfigureOptions(array $value): self
{
$this->configureOptions = $value;
return $this;
}
/** /**
* @return PackageConfigureOption[] * @return PackageConfigureOption[]
*/ */
@ -208,31 +252,76 @@ abstract class PackageInfo
} }
} }
class PackageVersion extends PackageInfo class PackageConfigureOption
{ {
private string $version; private string $name;
private string $prompt;
private ?string $default;
public function __construct(string $version, string $minPHPVersion, string $maxPHPVersion = '') public function __construct(string $name, string $prompt, ?string $default = null)
{ {
parent::__construct($minPHPVersion, $maxPHPVersion); $this->name = $name;
$this->version = $version; $this->prompt = $prompt;
$this->default = $default;
} }
public function getVersion(): string public function __toString(): string
{ {
return $this->version; $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 PackageVersionCombined extends PackageInfo class CompatiblePHPVersions
{ {
private string $minPHPVersion;
private string $maxPHPVersion;
private array $versions = []; private array $versions = [];
public function __construct(PackageInfo $info) public function __construct(string $minPHPVersion, string $maxPHPVersion)
{ {
parent::__construct($info->getMinPHPVersion(), $info->getMaxPHPVersion()); $this->minPHPVersion = $minPHPVersion;
$this->setConfigureOptions($info->getConfigureOptions()); $this->maxPHPVersion = $maxPHPVersion;
$this->addVersion($info->getVersion()); }
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;
} }
/** /**
@ -253,40 +342,71 @@ class PackageVersionCombined extends PackageInfo
return $this->versions; return $this->versions;
} }
public function isCompatibleWith(PackageInfo $info): bool public function isCompatibleWith(PackageVersionInfo $info): bool
{ {
return $this->getMinPHPVersion() === $info->getMinPHPVersion() return $this->getMinPHPVersion() === $info->getMinPHPVersion() && $this->getMaxPHPVersion() === $info->getMaxPHPVersion();
&& $this->getMaxPHPVersion() === $info->getMaxPHPVersion()
&& $this->getConfigureOptions() == $info->getConfigureOptions()
;
} }
} }
class PackageConfigureOption class CompatibleConfigurationOptions
{ {
private string $name; /**
private string $prompt; * @var PackageConfigureOption[]
private ?string $default; */
private array $configureOptions = [];
private array $versions = [];
public function __construct(string $name, string $prompt, ?string $default = null) public function __construct(array $configureOptions)
{ {
$this->name = $name; $this->configureOptions = $configureOptions;
$this->prompt = $prompt;
$this->default = $default;
} }
public function getName(): string public function __toString(): string
{ {
return $this->name; $versions = $this->getVersions();
$lines = [
"- Package versions: {$versions[0]}" . (isset($versions[1]) ? ' -> ' . $versions[count($versions) - 1] : ''),
];
$options = $this->getConfigureOptions();
if ($options === []) {
$lines[] = ' <no options>';
} else {
foreach ($options as $index => $option) {
$lines[] = $index + 1 . ') ' . (string) $option;
}
}
return implode("\n", $lines);
} }
public function getPrompt(): string /**
* @return PackageConfigureOption[]
*/
public function getConfigureOptions(): array
{ {
return $this->prompt; return $this->configureOptions;
} }
public function getDefault(): ?string /**
* @return $this
*/
public function addVersion(string $value): self
{ {
return $this->default; $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();
} }
} }