1
0
mirror of https://github.com/php/pie.git synced 2026-03-23 23:12:17 +01:00

435: added service to pre-scan for missing dependencies before installing an extension

This commit is contained in:
James Titcumb
2026-03-03 12:24:48 +00:00
parent a41a1ab678
commit 299903fd9d
6 changed files with 242 additions and 0 deletions

View File

@@ -11,6 +11,7 @@ use Php\Pie\ComposerIntegration\PieComposerFactory;
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ComposerIntegration\PieOperation;
use Php\Pie\DependencyResolver\BundledPhpExtensionRefusal;
use Php\Pie\DependencyResolver\DependencyInstaller\PrescanSystemDependencies;
use Php\Pie\DependencyResolver\DependencyResolver;
use Php\Pie\DependencyResolver\InvalidPackageName;
use Php\Pie\DependencyResolver\UnableToResolveRequirement;
@@ -23,6 +24,7 @@ use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use function sprintf;
@@ -35,6 +37,7 @@ final class InstallCommand extends Command
public function __construct(
private readonly ContainerInterface $container,
private readonly DependencyResolver $dependencyResolver,
private readonly PrescanSystemDependencies $prescanSystemDependencies,
private readonly ComposerIntegrationHandler $composerIntegrationHandler,
private readonly InvokeSubCommand $invokeSubCommand,
private readonly FindMatchingPackages $findMatchingPackages,
@@ -102,6 +105,16 @@ final class InstallCommand extends Command
),
);
// @todo flag to disable this check
try {
($this->prescanSystemDependencies)($composer, $targetPlatform, $requestedNameAndVersion);
} catch (Throwable $anything) {
$this->io->writeError(
'<comment>Skipping system dependency pre-scan due to exception:</comment> ' . $anything->getMessage(),
verbosity: IOInterface::VERBOSE,
);
}
try {
$package = ($this->dependencyResolver)(
$composer,

View File

@@ -146,6 +146,10 @@ class PhpBinaryPathBasedPlatformRepository extends PlatformRepository
$this->addPackage($lib);
}
/**
* Instructions for PIE to install these libraries, if they are missing, should be added
* into {@see \Php\Pie\DependencyResolver\DependencyInstaller\PrescanSystemDependencies}
*/
private function addLibrariesUsingPkgConfig(): void
{
$this->detectLibraryWithPkgConfig('curl', 'libcurl');

View File

@@ -39,6 +39,7 @@ use Php\Pie\Installing\Uninstall;
use Php\Pie\Installing\UninstallUsingUnlink;
use Php\Pie\Installing\UnixInstall;
use Php\Pie\Installing\WindowsInstall;
use Php\Pie\Platform\PackageManager;
use Php\Pie\SelfManage\BuildTools\CheckAllBuildTools;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\ConsoleEvents;
@@ -209,6 +210,13 @@ final class Container
$container->alias(Ini\RemoveIniEntryWithFileGetContents::class, Ini\RemoveIniEntry::class);
$container->singleton(
PackageManager::class,
static function (): PackageManager|null {
return PackageManager::detect();
},
);
return $container;
}

View File

@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace Php\Pie\DependencyResolver\DependencyInstaller;
use Composer\Composer;
use Composer\IO\IOInterface;
use Php\Pie\DependencyResolver\DependencyResolver;
use Php\Pie\DependencyResolver\DependencyStatus;
use Php\Pie\DependencyResolver\FetchDependencyStatuses;
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
use Php\Pie\Platform\PackageManager;
use Php\Pie\Platform\TargetPlatform;
use Throwable;
use function array_filter;
use function array_key_exists;
use function array_map;
use function array_unique;
use function array_values;
use function count;
use function implode;
use function sprintf;
use function str_replace;
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
class PrescanSystemDependencies
{
/** @var array<non-empty-string, array<non-empty-string, non-empty-string>> */
private readonly array $libraries;
public function __construct(
private readonly DependencyResolver $dependencyResolver,
private readonly FetchDependencyStatuses $fetchDependencyStatuses,
private readonly IOInterface $io,
private readonly PackageManager|null $packageManager,
) {
/**
* Checks for the existence of these libraries should be added into
* {@see \Php\Pie\ComposerIntegration\PhpBinaryPathBasedPlatformRepository::addLibrariesUsingPkgConfig()}
*/
$this->libraries = [
'sodium' => [
PackageManager::Apt->value => 'libsodium-dev',
PackageManager::Apk->value => 'libsodium-dev',
PackageManager::Dnf->value => 'pkgconfig(libsodium)',
PackageManager::Yum->value => 'pkgconfig(libsodium)',
],
'jpeg' => [
PackageManager::Apt->value => 'libjpeg-dev',
PackageManager::Apk->value => 'libjpeg-turbo-dev',
PackageManager::Dnf->value => 'pkgconfig(libjpeg)',
PackageManager::Yum->value => 'pkgconfig(libjpeg)',
],
];
}
public function __invoke(Composer $composer, TargetPlatform $targetPlatform, RequestedPackageAndVersion $requestedNameAndVersion): void
{
if ($this->packageManager === null) {
$this->io->writeError('<comment>Skipping pre-scan of system dependencies, as a supported package manager could not be detected.</comment>', verbosity: IOInterface::VERBOSE);
return;
}
$this->io->write(sprintf('Checking system dependencies are present for extension %s', $requestedNameAndVersion->prettyNameAndVersion()), verbosity: IOInterface::VERBOSE);
$package = ($this->dependencyResolver)(
$composer,
$targetPlatform,
$requestedNameAndVersion,
true,
);
$unmetDependencies = array_filter(
($this->fetchDependencyStatuses)($targetPlatform, $composer, $package->composerPackage()),
static function (DependencyStatus $dependencyStatus): bool {
return ! $dependencyStatus->satisfied();
},
);
if (! count($unmetDependencies)) {
$this->io->write('All system dependencies are already installed.', verbosity: IOInterface::VERBOSE);
return;
}
$this->io->write(
sprintf('Extension %s has unmet dependencies: %s', $requestedNameAndVersion->prettyNameAndVersion(), implode(', ', array_map(static fn (DependencyStatus $status): string => $status->name, $unmetDependencies))),
verbosity: IOInterface::VERBOSE,
);
$packageManagerPackages = array_values(array_unique(array_filter(array_map(
fn (DependencyStatus $unmetDependency): string|null => $this->packageManagerPackageForDependency($unmetDependency, $this->packageManager),
$unmetDependencies,
))));
if (! count($packageManagerPackages)) {
$this->io->writeError('No system dependencies could be installed automatically by PIE.', verbosity: IOInterface::VERBOSE);
return;
}
$proposedInstallCommand = implode(' ', $this->packageManager->installCommand($packageManagerPackages));
$this->io->write(sprintf('<info>Installing missing system dependencies:</info> %s', $proposedInstallCommand));
try {
$this->packageManager->install($packageManagerPackages);
} catch (Throwable $anything) {
$this->io->writeError(sprintf('<info>Failed to install missing system dependencies:</info> %s', $anything->getMessage()));
}
}
private function packageManagerPackageForDependency(DependencyStatus $unmetDependency, PackageManager $packageManager): string|null
{
$depName = str_replace('lib-', '', $unmetDependency->name);
if (! array_key_exists($depName, $this->libraries)) {
$this->io->writeError(
sprintf('Could not automatically install %s, as PIE does not have the package manager definition.', $unmetDependency->name),
verbosity: IOInterface::VERBOSE,
);
return null;
}
if (! array_key_exists($packageManager->value, $this->libraries[$depName])) {
$this->io->writeError(
sprintf('Could not automatically install "%s", as PIE does not have a definition for "%s"', $unmetDependency->name, $packageManager->value),
verbosity: IOInterface::VERBOSE,
);
return null;
}
$packageManagerPackage = $this->libraries[$depName][$packageManager->value];
// Note: ideally, we should also parse the version constraint. This initial iteration will ignore that, to be improved later.
$this->io->write(
sprintf('Adding %s package %s to be installed for %s', $packageManager->value, $packageManagerPackage, $unmetDependency->name),
verbosity: IOInterface::VERBOSE,
);
return $packageManagerPackage;
}
}

View File

@@ -31,3 +31,30 @@ RUN apt-get remove --allow-remove-essential -y apt
USER linuxbrew
RUN pie install --auto-install-build-tools -v asgrim/example-pie-extension
RUN pie show
FROM ubuntu AS test_pie_installs_system_deps_on_ubuntu
RUN apt-get update && apt install -y unzip curl wget gcc make autoconf libtool bison re2c pkg-config libzip-dev libssl-dev libonig-dev
RUN mkdir -p /opt/php \
&& mkdir -p /tmp/php \
&& cd /tmp/php \
&& wget -O php.tgz https://www.php.net/distributions/php-8.4.17.tar.gz \
&& tar zxf php.tgz \
&& rm php.tgz \
&& cd * \
&& ./buildconf --force \
&& ./configure --prefix=/opt/php --disable-all --enable-phar --enable-filter --enable-mbstring --with-openssl --with-iconv --with-zip \
&& make -j$(nproc) \
&& make install
ENV PATH="$PATH:/opt/php/bin"
COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie
RUN pie install -v php/sodium
FROM alpine AS test_pie_installs_system_deps_on_alpine
RUN apk add php php-phar php-mbstring php-iconv php-openssl bzip2-dev libbz2 build-base autoconf bison re2c libtool php84-dev
COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie
RUN pie install -v php/sodium
FROM fedora AS test_pie_installs_system_deps_on_fedora
RUN dnf install -y php php-pecl-zip unzip gcc make autoconf bison re2c libtool php-devel
COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie
RUN pie install -v php/sodium

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Php\PieUnitTest\DependencyResolver\DependencyInstaller;
use Php\Pie\DependencyResolver\DependencyInstaller\PrescanSystemDependencies;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(PrescanSystemDependencies::class)]
final class PrescanSystemDependenciesTest extends TestCase
{
public function testNoPackageManager(): void
{
self::markTestIncomplete('todo'); // @todo
}
public function testAllDependenciesSatisifed(): void
{
self::markTestIncomplete('todo'); // @todo
}
public function testMissingDependencyThatDoesNotHaveAnyPackageManagerDefinition(): void
{
self::markTestIncomplete('todo'); // @todo
}
public function testMissingDependencyThatDoesNotHaveMyPackageManagerDefinition(): void
{
self::markTestIncomplete('todo'); // @todo
}
public function testMissingDependenciesFailToInstall(): void
{
self::markTestIncomplete('todo'); // @todo
}
public function testMissingDependenciesAreSuccessfullyInstalled(): void
{
self::markTestIncomplete('todo'); // @todo
}
}