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:
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user