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

Merge pull request #526 from asgrim/435-prompt-to-install-missing-system-deps

435: prompt to install missing system deps
This commit is contained in:
James Titcumb
2026-03-06 10:30:12 +00:00
committed by GitHub
33 changed files with 1020 additions and 78 deletions

0
.noai Normal file
View File

View File

@@ -2,11 +2,12 @@
## What is PIE?
PIE is a new installer for PHP extensions, intended to eventually replace PECL.
It is distributed as a [PHAR](https://www.php.net/manual/en/intro.phar.php),
just like Composer, and works in a similar way to Composer, but it installs PHP
extensions (PHP Modules or Zend Extensions) to your PHP installation, rather
than pulling PHP packages into your project or library.
PIE is the official installer for PHP extensions, which replaces
[PECL](https://pecl.php.net/) (which is now deprecated). PIE is distributed as a
[PHAR](https://www.php.net/manual/en/intro.phar.php), just like Composer, and
works in a similar way to Composer, but it installs PHP extensions (PHP Modules
or Zend Extensions) to your PHP installation, rather than pulling PHP packages
into your project or library.
# Using PIE - what do I need to get started?
@@ -15,12 +16,8 @@ than pulling PHP packages into your project or library.
You will need PHP 8.1 or newer to run PIE, but PIE can install an extension to
any other installed PHP version.
On Linux, you will need a build toolchain installed. On Debian/Ubuntu type
systems, you could run something like:
```shell
sudo apt install gcc make autoconf libtool bison re2c pkg-config php-dev
```
On Linux/OSX, if any build tools needed are missing, PIE will ask if you would
like to automatically install them first (this is a new feature in 1.4.0).
On Windows, you do not need any build toolchain installed, since PHP extensions
for Windows are distributed as pre-compiled packages containing the extension
@@ -38,7 +35,9 @@ Further installation details can be found in the [usage](./docs/usage.md) docs.
This documentation assumes you have moved `pie.phar` into your `$PATH`, e.g.
`/usr/local/bin/pie` on non-Windows systems or created an alias in your shell RC file.
## Installing a single extension using PIE
## Using PIE
### Installing a single extension using PIE
You can install an extension using the `install` command. For example, to
install the `example_pie_extension` extension, you would run:
@@ -57,7 +56,7 @@ You must now add "extension=example_pie_extension" to your php.ini
$
```
## Installing all extensions for a PHP project
### Installing all extensions for a PHP project
When in your PHP project, you can install any missing top-level extensions:
@@ -87,6 +86,12 @@ The following packages may be suitable, which would you like to install:
Finished checking extensions.
```
> [!TIP]
> If you are running PIE in a non-interactive shell (for example, CI, a
> container), pass the `--allow-non-interactive-project-install` flag to run
> this command. It may still fail if more than one PIE package provides a
> particular extension.
## Extensions that support PIE
A list of extensions that support PIE can be found on
@@ -105,6 +110,6 @@ A list of extensions that support PIE can be found on
If you are an extension maintainer wanting to add PIE support to your extension,
please read [extension-maintainers](./docs/extension-maintainers.md).
## More documentation...
# More documentation...
The full documentation for PIE can be found in [usage](./docs/usage.md) docs.

View File

@@ -46,6 +46,7 @@
"bnf/phpstan-psr-container": "^1.1",
"doctrine/coding-standard": "^14.0.0",
"phpstan/phpstan": "^2.1.38",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-webmozart-assert": "^2.0",
"phpunit/phpunit": "^10.5.63"
},

58
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6392877b0b53e6d18d3d156173dcbba3",
"content-hash": "1138a5a4004fa55c3068062f3a2adc43",
"packages": [
{
"name": "composer/ca-bundle",
@@ -3362,6 +3362,62 @@
],
"time": "2026-01-30T17:12:46+00:00"
},
{
"name": "phpstan/phpstan-phpunit",
"version": "2.0.16",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-phpunit.git",
"reference": "6ab598e1bc106e6827fd346ae4a12b4a5d634c32"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/6ab598e1bc106e6827fd346ae4a12b4a5d634c32",
"reference": "6ab598e1bc106e6827fd346ae4a12b4a5d634c32",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.1.32"
},
"conflict": {
"phpunit/phpunit": "<7.0"
},
"require-dev": {
"nikic/php-parser": "^5",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.6"
},
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"extension.neon",
"rules.neon"
]
}
},
"autoload": {
"psr-4": {
"PHPStan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPUnit extensions and rules for PHPStan",
"keywords": [
"static analysis"
],
"support": {
"issues": "https://github.com/phpstan/phpstan-phpunit/issues",
"source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.16"
},
"time": "2026-02-14T09:05:21+00:00"
},
{
"name": "phpstan/phpstan-webmozart-assert",
"version": "2.0.0",

View File

@@ -345,11 +345,16 @@ The list of accepted OS families: "windows", "bsd", "darwin", "solaris", "linux"
#### Extension dependencies
Extension authors may define some dependencies in `require`, but practically,
Extension authors may define some dependencies in `require`, but typically,
most extensions would not need to define dependencies, except for the PHP
versions supported by the extension. Dependencies on other extensions may be
defined, for example `ext-json`. However, dependencies on a regular PHP package
(such as `monolog/monolog`) SHOULD NOT be specified in your `require` section.
versions supported by the extension, and system libraries.
Dependencies on a regular PHP package (such as `monolog/monolog`) SHOULD NOT be
specified in your extension's `require` section.
##### Dependencies on other extensions
Dependencies on other extensions may be defined, for example `ext-json`.
It is worth noting that if your extension does define a dependency on another
dependency, and this is not available, someone installing your extension would
@@ -360,6 +365,47 @@ Cannot use myvendor/myextension's latest version 1.2.3 as it requires
ext-something * which is missing from your platform.
```
##### System Library Dependencies
In PIE 1.4.0, the ability for extension authors to define system library
dependencies was added, and in some cases, automatically install them.
The following libraries are supported at the moment. **If you would like to add
a library, please [open a discussion](https://github.com/php/pie/discussions)
in the first instance.** Don't just open a PR without discussing first please!
We are adding libraries and improving this feature over time. If the automatic
install of a system dependency that is supported below in your package manager
is NOT working, then please [report a bug](https://github.com/php/pie/issues).
| Library | Checked by PIE | Auto-installs in |
|---------------|----------------|--------------------|
| lib-curl | ✅ | apt, apk, dnf, yum |
| lib-enchant | ✅ | ❌ |
| lib-enchant-2 | ✅ | ❌ |
| lib-sodium | ✅ | apt, apk, dnf, yum |
| lib-ffi | ✅ | apt, apk, dnf, yum |
| lib-xslt | ✅ | apt, apk, dnf, yum |
| lib-zip | ✅ | apt, apk, dnf, yum |
| lib-png | ✅ | ❌ |
| lib-avif | ✅ | ❌ |
| lib-webp | ✅ | ❌ |
| lib-jpeg | ✅ | apt, apk, dnf, yum |
| lib-xpm | ✅ | ❌ |
| lib-freetype2 | ✅ | ❌ |
| lib-gdlib | ✅ | ❌ |
| lib-gmp | ✅ | ❌ |
| lib-sasl | ✅ | ❌ |
| lib-onig | ✅ | ❌ |
| lib-odbc | ✅ | ❌ |
| lib-capstone | ✅ | ❌ |
| lib-pcre | ✅ | ❌ |
| lib-edit | ✅ | ❌ |
| lib-snmp | ✅ | ❌ |
| lib-argon2 | ✅ | ❌ |
| lib-uriparser | ✅ | ❌ |
| lib-exslt | ✅ | ❌ |
#### Checking the extension will work
First up, you can use `composer validate` to check your `composer.json` is

View File

@@ -281,6 +281,31 @@ pie install example/some-extension --with-some-library-name=/path/to/the/lib
pie install example/some-extension --with-some-library-name=/path/to/the/lib --enable-some-functionality
```
### Build tools check
PIE will attempt to check the presence of build tools (such as gcc, make, etc.)
before running. If any are missing, an interactive prompt will ask if you would
like to install the missing tools. If you are running in non-interactive mode
(for example, in a CI pipeline, container build, etc), PIE will **not**
install these tools automatically. If you would like to install the build tools
in a non-interactive terminal, pass the `--auto-install-build-tools` and the
prompt will be skipped.
To skip the build tools check entirely, pass the `--no-build-tools-check` flag.
### System library dependencies check
PIE will attempt to check the presence of system library dependencies before
installing an extension. If any are missing, an interactive prompt will ask if
you would like to install the missing tools. If you are running in
non-interactive mode (for example, in a CI pipeline, container build, etc), PIE
will **not** install these dependencies automatically. If you would like to
install the system dependencies in a non-interactive terminal, pass the
`--auto-install-system-dependencies` and the prompt will be skipped.
To skip the dependencies check entirely, pass the
`--no-system-dependencies-check` flag.
### Configuring the INI file
PIE will automatically try to enable the extension by adding `extension=...` or

View File

@@ -288,6 +288,12 @@ parameters:
count: 1
path: src/Platform.php
-
message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(Composer\\Package\\BasePackage\)\: mixed\)\|null, Closure\(Composer\\Package\\CompletePackageInterface\)\: Php\\Pie\\DependencyResolver\\Package given\.$#'
identifier: argument.type
count: 1
path: src/Platform/InstalledPiePackages.php
-
message: '#^Call to function array_key_exists\(\) with 1 and array\{non\-falsy\-string, non\-empty\-string, non\-empty\-string\} will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
@@ -354,18 +360,6 @@ parameters:
count: 1
path: test/integration/Command/InstallCommandTest.php
-
message: '#^Parameter \#1 \$originalClassName of method PHPUnit\\Framework\\TestCase\:\:createMock\(\) expects class\-string\<object\>, string given\.$#'
identifier: argument.type
count: 1
path: test/integration/Command/InstallExtensionsForProjectCommandTest.php
-
message: '#^Unable to resolve the template type RealInstanceType in call to method PHPUnit\\Framework\\TestCase\:\:createMock\(\)$#'
identifier: argument.templateType
count: 1
path: test/integration/Command/InstallExtensionsForProjectCommandTest.php
-
message: '#^Call to function assert\(\) with true will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType

View File

@@ -2,6 +2,7 @@ includes:
- phpstan-baseline.neon
- vendor/bnf/phpstan-psr-container/extension.neon
- vendor/phpstan/phpstan-webmozart-assert/extension.neon
- vendor/phpstan/phpstan-phpunit/extension.neon
parameters:
level: 10

View File

@@ -11,17 +11,19 @@ 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;
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
use Php\Pie\Platform\PackageManager;
use Php\Pie\SelfManage\BuildTools\CheckAllBuildTools;
use Php\Pie\SelfManage\BuildTools\PackageManager;
use Psr\Container\ContainerInterface;
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;
@@ -34,6 +36,7 @@ final class BuildCommand extends Command
public function __construct(
private readonly ContainerInterface $container,
private readonly DependencyResolver $dependencyResolver,
private readonly PrescanSystemDependencies $prescanSystemDependencies,
private readonly ComposerIntegrationHandler $composerIntegrationHandler,
private readonly FindMatchingPackages $findMatchingPackages,
private readonly IOInterface $io,
@@ -88,6 +91,22 @@ final class BuildCommand extends Command
),
);
if (CommandHelper::shouldCheckSystemDependencies($input)) {
try {
($this->prescanSystemDependencies)(
$composer,
$targetPlatform,
$requestedNameAndVersion,
CommandHelper::autoInstallSystemDependencies($input),
);
} 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

@@ -63,6 +63,8 @@ final class CommandHelper
private const OPTION_NO_CACHE = 'no-cache';
private const OPTION_AUTO_INSTALL_BUILD_TOOLS = 'auto-install-build-tools';
private const OPTION_SUPPRESS_BUILD_TOOLS_CHECK = 'no-build-tools-check';
private const OPTION_AUTO_INSTALL_SYSTEM_DEPENDENCIES = 'auto-install-system-dependencies';
private const OPTION_SUPPRESS_SYSTEM_DEPENDENCIES_CHECK = 'no-system-dependencies-check';
private function __construct()
{
@@ -154,6 +156,19 @@ final class CommandHelper
'Do not perform the check to see if build tools are present on the system.',
);
$command->addOption(
self::OPTION_AUTO_INSTALL_SYSTEM_DEPENDENCIES,
null,
InputOption::VALUE_NONE,
'If system dependencies missing, automatically install them, instead of prompting.',
);
$command->addOption(
self::OPTION_SUPPRESS_SYSTEM_DEPENDENCIES_CHECK,
null,
InputOption::VALUE_NONE,
'Do not perform the check to see if system dependencies are present on the system.',
);
/**
* Allows additional options for the `./configure` command to be passed here.
* Note, this means you probably need to call {@see self::validateInput()} to validate the input manually...
@@ -267,6 +282,22 @@ final class CommandHelper
|| ! $input->getOption(self::OPTION_SUPPRESS_BUILD_TOOLS_CHECK);
}
public static function autoInstallSystemDependencies(InputInterface $input): bool
{
return $input->hasOption(self::OPTION_AUTO_INSTALL_SYSTEM_DEPENDENCIES)
&& $input->getOption(self::OPTION_AUTO_INSTALL_SYSTEM_DEPENDENCIES);
}
public static function shouldCheckSystemDependencies(InputInterface $input): bool
{
if (Platform::isWindows()) {
return false;
}
return ! $input->hasOption(self::OPTION_SUPPRESS_SYSTEM_DEPENDENCIES_CHECK)
|| ! $input->getOption(self::OPTION_SUPPRESS_SYSTEM_DEPENDENCIES_CHECK);
}
public static function requestedNameAndVersionPair(InputInterface $input): RequestedPackageAndVersion
{
$requestedPackageString = $input->getArgument(self::ARG_REQUESTED_PACKAGE_AND_VERSION);

View File

@@ -5,17 +5,15 @@ declare(strict_types=1);
namespace Php\Pie\Command;
use Composer\IO\IOInterface;
use Composer\Semver\Constraint\Constraint;
use Php\Pie\ComposerIntegration\PhpBinaryPathBasedPlatformRepository;
use Php\Pie\ComposerIntegration\PieComposerFactory;
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ComposerIntegration\PieOperation;
use Php\Pie\DependencyResolver\BundledPhpExtensionRefusal;
use Php\Pie\DependencyResolver\DependencyResolver;
use Php\Pie\DependencyResolver\FetchDependencyStatuses;
use Php\Pie\DependencyResolver\InvalidPackageName;
use Php\Pie\DependencyResolver\UnableToResolveRequirement;
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
use Php\Pie\Platform\InstalledPiePackages;
use Php\Pie\Platform\ThreadSafetyMode;
use Php\Pie\Util\Emoji;
use Psr\Container\ContainerInterface;
@@ -24,7 +22,6 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function array_key_exists;
use function count;
use function in_array;
use function sprintf;
@@ -38,6 +35,7 @@ final class InfoCommand extends Command
public function __construct(
private readonly ContainerInterface $container,
private readonly DependencyResolver $dependencyResolver,
private readonly FetchDependencyStatuses $fetchDependencyStatuses,
private readonly FindMatchingPackages $findMatchingPackages,
private readonly IOInterface $io,
) {
@@ -127,30 +125,11 @@ final class InfoCommand extends Command
));
$this->io->write("\n<options=bold,underscore>Dependencies:</>");
$requires = $package->composerPackage()->getRequires();
if (count($requires) > 0) {
/** @var array<string, list<Constraint>> $platformConstraints */
$platformConstraints = [];
$composerPlatform = new PhpBinaryPathBasedPlatformRepository($targetPlatform->phpBinaryPath, $composer, new InstalledPiePackages(), null);
foreach ($composerPlatform->getPackages() as $platformPackage) {
$platformConstraints[$platformPackage->getName()][] = new Constraint('==', $platformPackage->getVersion());
}
foreach ($requires as $requireName => $requireLink) {
$packageStatus = sprintf(' %s: %s %%s', $requireName, $requireLink->getConstraint()->getPrettyString());
if (! array_key_exists($requireName, $platformConstraints)) {
$this->io->write(sprintf($packageStatus, Emoji::PROHIBITED . ' (not installed)'));
continue;
}
foreach ($platformConstraints[$requireName] as $constraint) {
if ($requireLink->getConstraint()->matches($constraint)) {
$this->io->write(sprintf($packageStatus, Emoji::GREEN_CHECKMARK));
} else {
$this->io->write(sprintf($packageStatus, Emoji::PROHIBITED . ' (your version is ' . $constraint->getVersion() . ')'));
}
}
$dependencyStatuses = ($this->fetchDependencyStatuses)($targetPlatform, $composer, $package->composerPackage());
if (count($dependencyStatuses) > 0) {
foreach ($dependencyStatuses as $dependencyStatus) {
$this->io->write(' ' . $dependencyStatus->asPrettyString());
}
} else {
$this->io->write(' No dependencies.');

View File

@@ -11,18 +11,20 @@ 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;
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
use Php\Pie\Platform\PackageManager;
use Php\Pie\Platform\TargetPlatform;
use Php\Pie\SelfManage\BuildTools\CheckAllBuildTools;
use Php\Pie\SelfManage\BuildTools\PackageManager;
use Psr\Container\ContainerInterface;
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,22 @@ final class InstallCommand extends Command
),
);
if (CommandHelper::shouldCheckSystemDependencies($input)) {
try {
($this->prescanSystemDependencies)(
$composer,
$targetPlatform,
$requestedNameAndVersion,
CommandHelper::autoInstallSystemDependencies($input),
);
} 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\SystemDependenciesDefinition::default()}
*/
private function addLibrariesUsingPkgConfig(): void
{
$this->detectLibraryWithPkgConfig('curl', 'libcurl');

View File

@@ -28,6 +28,7 @@ use Php\Pie\Command\ShowCommand;
use Php\Pie\Command\UninstallCommand;
use Php\Pie\ComposerIntegration\MinimalHelperSet;
use Php\Pie\ComposerIntegration\QuieterConsoleIO;
use Php\Pie\DependencyResolver\DependencyInstaller\SystemDependenciesDefinition;
use Php\Pie\DependencyResolver\DependencyResolver;
use Php\Pie\DependencyResolver\ResolveDependencyWithComposer;
use Php\Pie\Downloading\GithubPackageReleaseAssets;
@@ -39,6 +40,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 +211,20 @@ final class Container
$container->alias(Ini\RemoveIniEntryWithFileGetContents::class, Ini\RemoveIniEntry::class);
$container->singleton(
PackageManager::class,
static function (): PackageManager|null {
return PackageManager::detect();
},
);
$container->singleton(
SystemDependenciesDefinition::class,
static function (): SystemDependenciesDefinition {
return SystemDependenciesDefinition::default();
},
);
return $container;
}

View File

@@ -0,0 +1,150 @@
<?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
{
public function __construct(
private readonly DependencyResolver $dependencyResolver,
private readonly FetchDependencyStatuses $fetchDependencyStatuses,
private readonly SystemDependenciesDefinition $systemDependenciesDefinition,
private readonly PackageManager|null $packageManager,
private readonly IOInterface $io,
) {
}
public function __invoke(
Composer $composer,
TargetPlatform $targetPlatform,
RequestedPackageAndVersion $requestedNameAndVersion,
bool $autoInstallIfMissing,
): 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));
if (! $this->io->isInteractive() && ! $autoInstallIfMissing) {
$this->io->writeError('<warning>You are not running in interactive mode, and you did not provide the --auto-install-system-dependencies flag.');
$this->io->writeError('You may need to run: ' . $proposedInstallCommand . '</warning>');
$this->io->writeError('');
return;
}
$this->io->write(sprintf('<info>Need to install missing system dependencies:</info> %s', $proposedInstallCommand));
if ($this->io->isInteractive() && ! $autoInstallIfMissing) {
if (! $this->io->askConfirmation('<question>Would you like to install them now?</question>', false)) {
$this->io->write('<comment>Ok, but things might not work. Just so you know.</comment>');
return;
}
}
try {
$this->packageManager->install($packageManagerPackages);
$this->io->write('<info>Missing system dependencies have been installed.</info>');
} 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->systemDependenciesDefinition->definition)) {
$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->systemDependenciesDefinition->definition[$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->systemDependenciesDefinition->definition[$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

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Php\Pie\DependencyResolver\DependencyInstaller;
use Php\Pie\Platform\PackageManager;
class SystemDependenciesDefinition
{
/** @param array<non-empty-string, array<non-empty-string, non-empty-string>> $definition */
public function __construct(public readonly array $definition)
{
}
/**
* Checks for the existence of these libraries should be added into
* {@see \Php\Pie\ComposerIntegration\PhpBinaryPathBasedPlatformRepository::addLibrariesUsingPkgConfig()}
*/
public static function default(): self
{
return new self([
'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)',
],
'zip' => [
PackageManager::Apt->value => 'libzip-dev',
PackageManager::Apk->value => 'libzip-dev',
PackageManager::Dnf->value => 'pkgconfig(libzip)',
PackageManager::Yum->value => 'pkgconfig(libzip)',
],
'xslt' => [
PackageManager::Apt->value => 'libxslt1-dev',
PackageManager::Apk->value => 'libxslt-dev',
PackageManager::Dnf->value => 'pkgconfig(libxslt)',
PackageManager::Yum->value => 'pkgconfig(libxslt)',
],
'ffi' => [
PackageManager::Apt->value => 'libffi-dev',
PackageManager::Apk->value => 'libffi-dev',
PackageManager::Dnf->value => 'pkgconfig(libffi)',
PackageManager::Yum->value => 'pkgconfig(libffi)',
],
'curl' => [
PackageManager::Apt->value => 'libcurl4-openssl-dev',
PackageManager::Apk->value => 'curl-dev',
PackageManager::Dnf->value => 'pkgconfig(libcurl)',
PackageManager::Yum->value => 'pkgconfig(libcurl)',
],
]);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Php\Pie\DependencyResolver;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\ConstraintInterface;
use Php\Pie\Util\Emoji;
use function sprintf;
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
final class DependencyStatus
{
public function __construct(
public readonly string $name,
public readonly ConstraintInterface $requireConstraint,
public readonly Constraint|null $installedVersion,
) {
}
public function asPrettyString(): string
{
$statusTemplate = sprintf('%s: %s %%s', $this->name, $this->requireConstraint->getPrettyString());
if ($this->installedVersion === null) {
return sprintf($statusTemplate, Emoji::PROHIBITED . ' (not installed)');
}
if (! $this->requireConstraint->matches($this->installedVersion)) {
return sprintf($statusTemplate, Emoji::PROHIBITED . ' (your version is ' . $this->installedVersion->getVersion() . ')');
}
return sprintf($statusTemplate, Emoji::GREEN_CHECKMARK);
}
public function satisfied(): bool
{
return $this->installedVersion !== null && $this->requireConstraint->matches($this->installedVersion);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Php\Pie\DependencyResolver;
use Composer\Composer;
use Composer\Package\CompletePackageInterface;
use Composer\Semver\Constraint\Constraint;
use Php\Pie\ComposerIntegration\PhpBinaryPathBasedPlatformRepository;
use Php\Pie\Platform\InstalledPiePackages;
use Php\Pie\Platform\TargetPlatform;
use function array_key_exists;
use function count;
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
class FetchDependencyStatuses
{
/** @return list<DependencyStatus> */
public function __invoke(TargetPlatform $targetPlatform, Composer $composer, CompletePackageInterface $package): array
{
$requires = $package->getRequires();
if (count($requires) <= 0) {
return [];
}
/** @var array<string, Constraint> $platformConstraints */
$platformConstraints = [];
$composerPlatform = new PhpBinaryPathBasedPlatformRepository($targetPlatform->phpBinaryPath, $composer, new InstalledPiePackages(), null);
foreach ($composerPlatform->getPackages() as $platformPackage) {
$platformConstraints[$platformPackage->getName()] = new Constraint('==', $platformPackage->getVersion());
}
$checkedPackages = [];
foreach ($requires as $requireName => $requireLink) {
$checkedPackages[] = new DependencyStatus(
$requireName,
$requireLink->getConstraint(),
array_key_exists($requireName, $platformConstraints) ? $platformConstraints[$requireName] : null,
);
}
return $checkedPackages;
}
}

View File

@@ -7,7 +7,9 @@ namespace Php\Pie\Platform;
use Composer\Composer;
use Composer\Package\BasePackage;
use Composer\Package\CompletePackageInterface;
use InvalidArgumentException;
use Php\Pie\DependencyResolver\Package;
use Php\Pie\ExtensionName;
use function array_combine;
use function array_filter;
@@ -38,6 +40,12 @@ class InstalledPiePackages
->getLocalRepository()
->getPackages(),
static function (BasePackage $basePackage): bool {
try {
ExtensionName::determineFromComposerPackage($basePackage);
} catch (InvalidArgumentException) {
return false;
}
return $basePackage instanceof CompletePackageInterface;
},
),

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Php\Pie\SelfManage\BuildTools;
namespace Php\Pie\Platform;
use Php\Pie\File\Sudo;
use Php\Pie\Platform;

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Php\Pie\SelfManage\BuildTools;
use Php\Pie\Platform\PackageManager;
use Php\Pie\Platform\TargetPlatform;
use Symfony\Component\Process\ExecutableFinder;

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Php\Pie\SelfManage\BuildTools;
use Composer\IO\IOInterface;
use Php\Pie\Platform\PackageManager;
use Php\Pie\Platform\TargetPlatform;
use Throwable;

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Php\Pie\SelfManage\BuildTools;
use Php\Pie\Platform\PackageManager;
use Php\Pie\Platform\TargetPhp\PhpizePath;
use Php\Pie\Platform\TargetPlatform;
use RuntimeException;

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 --auto-install-system-dependencies 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 --auto-install-system-dependencies 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 --auto-install-system-dependencies php/sodium

View File

@@ -58,17 +58,15 @@ final class InstallExtensionsForProjectCommandTest extends TestCase
$container->method('get')->willReturnCallback(
/** @param class-string $service */
function (string $service): mixed {
switch ($service) {
case QuieterConsoleIO::class:
return new QuieterConsoleIO(
new ArrayInput([]),
new BufferedOutput(),
new MinimalHelperSet(['question' => new QuestionHelper()]),
);
default:
return $this->createMock($service);
}
/** @var class-string $service */
return match ($service) {
QuieterConsoleIO::class => new QuieterConsoleIO(
new ArrayInput([]),
new BufferedOutput(),
new MinimalHelperSet(['question' => new QuestionHelper()]),
),
default => $this->createMock($service),
};
},
);

View File

@@ -0,0 +1,297 @@
<?php
declare(strict_types=1);
namespace Php\PieUnitTest\DependencyResolver\DependencyInstaller;
use Composer\Composer;
use Composer\IO\BufferIO;
use Composer\Package\CompletePackage;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\VersionParser;
use Php\Pie\DependencyResolver\DependencyInstaller\PrescanSystemDependencies;
use Php\Pie\DependencyResolver\DependencyInstaller\SystemDependenciesDefinition;
use Php\Pie\DependencyResolver\DependencyResolver;
use Php\Pie\DependencyResolver\DependencyStatus;
use Php\Pie\DependencyResolver\FetchDependencyStatuses;
use Php\Pie\DependencyResolver\Package;
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
use Php\Pie\Platform\PackageManager;
use Php\Pie\Platform\TargetPlatform;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Output\StreamOutput;
#[CoversClass(PrescanSystemDependencies::class)]
final class PrescanSystemDependenciesTest extends TestCase
{
private readonly DependencyResolver&MockObject $dependencyResolver;
private readonly FetchDependencyStatuses&MockObject $fetchDependencyStatuses;
private readonly BufferIO $io;
private readonly Composer&MockObject $composer;
private readonly TargetPlatform&MockObject $targetPlatform;
public function setUp(): void
{
parent::setUp();
$this->dependencyResolver = $this->createMock(DependencyResolver::class);
$this->fetchDependencyStatuses = $this->createMock(FetchDependencyStatuses::class);
$this->io = new BufferIO(verbosity: StreamOutput::VERBOSITY_VERBOSE);
$this->composer = $this->createMock(Composer::class);
$this->targetPlatform = $this->createMock(TargetPlatform::class);
}
public function testNoPackageManager(): void
{
$scanner = new PrescanSystemDependencies(
$this->dependencyResolver,
$this->fetchDependencyStatuses,
new SystemDependenciesDefinition([]),
null,
$this->io,
);
($scanner)($this->composer, $this->targetPlatform, new RequestedPackageAndVersion('foo/foo', null), true);
self::assertStringContainsString(
'Skipping pre-scan of system dependencies, as a supported package manager could not be detected.',
$this->io->getOutput(),
);
}
public function testAllDependenciesSatisfied(): void
{
$scanner = new PrescanSystemDependencies(
$this->dependencyResolver,
$this->fetchDependencyStatuses,
new SystemDependenciesDefinition([]),
PackageManager::Test,
$this->io,
);
$request = new RequestedPackageAndVersion('foo/foo', null);
$composerPackage = new CompletePackage('foo/foo', '1.0.0.0', '1.0.0');
$piePackage = Package::fromComposerCompletePackage($composerPackage);
$this->dependencyResolver->expects(self::once())
->method('__invoke')
->with($this->composer, $this->targetPlatform, $request, true)
->willReturn($piePackage);
$versionParser = new VersionParser();
$this->fetchDependencyStatuses->expects(self::once())
->method('__invoke')
->with($this->targetPlatform, $this->composer, $composerPackage)
->willReturn([
new DependencyStatus('lib-foo', $versionParser->parseConstraints('^1.0'), new Constraint('=', '1.0.0.0')),
new DependencyStatus('lib-bar', $versionParser->parseConstraints('^2.0'), new Constraint('=', '2.5.1.0')),
]);
($scanner)($this->composer, $this->targetPlatform, $request, true);
self::assertStringContainsString(
'All system dependencies are already installed.',
$this->io->getOutput(),
);
}
public function testMissingDependencyThatDoesNotHaveAnyPackageManagerDefinition(): void
{
$scanner = new PrescanSystemDependencies(
$this->dependencyResolver,
$this->fetchDependencyStatuses,
new SystemDependenciesDefinition([]),
PackageManager::Test,
$this->io,
);
$request = new RequestedPackageAndVersion('foo/foo', null);
$composerPackage = new CompletePackage('foo/foo', '1.0.0.0', '1.0.0');
$piePackage = Package::fromComposerCompletePackage($composerPackage);
$this->dependencyResolver->expects(self::once())
->method('__invoke')
->with($this->composer, $this->targetPlatform, $request, true)
->willReturn($piePackage);
$versionParser = new VersionParser();
$this->fetchDependencyStatuses->expects(self::once())
->method('__invoke')
->with($this->targetPlatform, $this->composer, $composerPackage)
->willReturn([
new DependencyStatus('lib-bar', $versionParser->parseConstraints('^1.0'), null),
]);
($scanner)($this->composer, $this->targetPlatform, $request, true);
$outputString = $this->io->getOutput();
self::assertStringContainsString('Extension foo/foo has unmet dependencies: lib-bar', $outputString);
self::assertStringContainsString('Could not automatically install "lib-bar", as PIE does not have the package manager definition.', $outputString);
self::assertStringContainsString('No system dependencies could be installed automatically by PIE.', $outputString);
}
public function testMissingDependencyThatDoesNotHaveMyPackageManagerDefinition(): void
{
$scanner = new PrescanSystemDependencies(
$this->dependencyResolver,
$this->fetchDependencyStatuses,
new SystemDependenciesDefinition([
'bar' => [
PackageManager::Apt->value => 'libbar-dev',
PackageManager::Apk->value => 'libbar-dev',
],
]),
PackageManager::Test,
$this->io,
);
$request = new RequestedPackageAndVersion('foo/foo', null);
$composerPackage = new CompletePackage('foo/foo', '1.0.0.0', '1.0.0');
$piePackage = Package::fromComposerCompletePackage($composerPackage);
$this->dependencyResolver->expects(self::once())
->method('__invoke')
->with($this->composer, $this->targetPlatform, $request, true)
->willReturn($piePackage);
$versionParser = new VersionParser();
$this->fetchDependencyStatuses->expects(self::once())
->method('__invoke')
->with($this->targetPlatform, $this->composer, $composerPackage)
->willReturn([
new DependencyStatus('lib-bar', $versionParser->parseConstraints('^1.0'), null),
]);
($scanner)($this->composer, $this->targetPlatform, $request, true);
$outputString = $this->io->getOutput();
self::assertStringContainsString('Extension foo/foo has unmet dependencies: lib-bar', $outputString);
self::assertStringContainsString('Could not automatically install "lib-bar", as PIE does not have a definition for "test"', $outputString);
self::assertStringContainsString('No system dependencies could be installed automatically by PIE.', $outputString);
}
public function testMissingDependenciesFailToInstall(): void
{
$scanner = new PrescanSystemDependencies(
$this->dependencyResolver,
$this->fetchDependencyStatuses,
new SystemDependenciesDefinition([
'bar' => [
PackageManager::Apk->value => 'hopefully-this-package-does-not-exist-in-apk',
PackageManager::Test->value => 'libbar-dev',
],
]),
PackageManager::Apk,
$this->io,
);
$request = new RequestedPackageAndVersion('foo/foo', null);
$composerPackage = new CompletePackage('foo/foo', '1.0.0.0', '1.0.0');
$piePackage = Package::fromComposerCompletePackage($composerPackage);
$this->dependencyResolver->expects(self::once())
->method('__invoke')
->with($this->composer, $this->targetPlatform, $request, true)
->willReturn($piePackage);
$versionParser = new VersionParser();
$this->fetchDependencyStatuses->expects(self::once())
->method('__invoke')
->with($this->targetPlatform, $this->composer, $composerPackage)
->willReturn([
new DependencyStatus('lib-bar', $versionParser->parseConstraints('^1.0'), null),
]);
($scanner)($this->composer, $this->targetPlatform, $request, true);
$outputString = $this->io->getOutput();
self::assertStringContainsString('Extension foo/foo has unmet dependencies: lib-bar', $outputString);
self::assertStringContainsString('Failed to install missing system dependencies', $outputString);
}
public function testMissingDependenciesAreSuccessfullyInstalled(): void
{
$scanner = new PrescanSystemDependencies(
$this->dependencyResolver,
$this->fetchDependencyStatuses,
new SystemDependenciesDefinition([
'bar' => [
PackageManager::Apt->value => 'libbar-dev',
PackageManager::Apk->value => 'libbar-dev',
PackageManager::Test->value => 'libbar-dev',
],
]),
PackageManager::Test,
$this->io,
);
$request = new RequestedPackageAndVersion('foo/foo', null);
$composerPackage = new CompletePackage('foo/foo', '1.0.0.0', '1.0.0');
$piePackage = Package::fromComposerCompletePackage($composerPackage);
$this->dependencyResolver->expects(self::once())
->method('__invoke')
->with($this->composer, $this->targetPlatform, $request, true)
->willReturn($piePackage);
$versionParser = new VersionParser();
$this->fetchDependencyStatuses->expects(self::once())
->method('__invoke')
->with($this->targetPlatform, $this->composer, $composerPackage)
->willReturn([
new DependencyStatus('lib-bar', $versionParser->parseConstraints('^1.0'), null),
]);
($scanner)($this->composer, $this->targetPlatform, $request, true);
$outputString = $this->io->getOutput();
self::assertStringContainsString('Extension foo/foo has unmet dependencies: lib-bar', $outputString);
self::assertStringContainsString('Adding test package libbar-dev to be installed for lib-bar', $outputString);
self::assertStringContainsString('Need to install missing system dependencies: echo "fake installing libbar-dev"', $outputString);
}
public function testMissingDependenciesAreNotInstalledWhenShouldNotAutoInstallAndNonInteractive(): void
{
$scanner = new PrescanSystemDependencies(
$this->dependencyResolver,
$this->fetchDependencyStatuses,
new SystemDependenciesDefinition([
'bar' => [
PackageManager::Apt->value => 'libbar-dev',
PackageManager::Apk->value => 'libbar-dev',
PackageManager::Test->value => 'libbar-dev',
],
]),
PackageManager::Test,
$this->io,
);
$request = new RequestedPackageAndVersion('foo/foo', null);
$composerPackage = new CompletePackage('foo/foo', '1.0.0.0', '1.0.0');
$piePackage = Package::fromComposerCompletePackage($composerPackage);
$this->dependencyResolver->expects(self::once())
->method('__invoke')
->with($this->composer, $this->targetPlatform, $request, true)
->willReturn($piePackage);
$versionParser = new VersionParser();
$this->fetchDependencyStatuses->expects(self::once())
->method('__invoke')
->with($this->targetPlatform, $this->composer, $composerPackage)
->willReturn([
new DependencyStatus('lib-bar', $versionParser->parseConstraints('^1.0'), null),
]);
($scanner)($this->composer, $this->targetPlatform, $request, false);
$outputString = $this->io->getOutput();
self::assertStringContainsString('Extension foo/foo has unmet dependencies: lib-bar', $outputString);
self::assertStringContainsString('Adding test package libbar-dev to be installed for lib-bar', $outputString);
self::assertStringContainsString('You are not running in interactive mode, and you did not provide the --auto-install-system-dependencies flag.', $outputString);
self::assertStringContainsString('You may need to run: echo "fake installing libbar-dev"', $outputString);
self::assertStringNotContainsString('Need to install missing system dependencies', $outputString);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Php\PieUnitTest\DependencyResolver;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\MatchAllConstraint;
use Composer\Semver\VersionParser;
use Php\Pie\DependencyResolver\DependencyStatus;
use Php\Pie\Util\Emoji;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(DependencyStatus::class)]
final class DependencyStatusTest extends TestCase
{
public function testDependencyNotInstalled(): void
{
$dependencyStatus = new DependencyStatus('foo', new Constraint('>', '2.0.0'), null);
self::assertSame('foo: > 2.0.0 ' . Emoji::PROHIBITED . ' (not installed)', $dependencyStatus->asPrettyString());
self::assertFalse($dependencyStatus->satisfied());
}
public function testDependencyInstalledAndMatchesAllConstraint(): void
{
$dependencyStatus = new DependencyStatus('foo', new MatchAllConstraint(), new Constraint('=', '1.0.0.0'));
self::assertSame('foo: * ' . Emoji::GREEN_CHECKMARK, $dependencyStatus->asPrettyString());
self::assertTrue($dependencyStatus->satisfied());
}
public function testDependencyInstalledAndMatchesSemverConstraint(): void
{
$dependencyStatus = new DependencyStatus('foo', (new VersionParser())->parseConstraints('^1.0'), new Constraint('=', '1.0.0.0'));
self::assertSame('foo: ^1.0 ' . Emoji::GREEN_CHECKMARK, $dependencyStatus->asPrettyString());
self::assertTrue($dependencyStatus->satisfied());
}
public function testDependencyInstalledButMismatchingVersion(): void
{
$dependencyStatus = new DependencyStatus('foo', new Constraint('>', '2.0.0'), new Constraint('=', '1.2.3.0'));
self::assertSame('foo: > 2.0.0 ' . Emoji::PROHIBITED . ' (your version is 1.2.3.0)', $dependencyStatus->asPrettyString());
self::assertFalse($dependencyStatus->satisfied());
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Php\PieUnitTest\DependencyResolver;
use Composer\Composer;
use Composer\Factory;
use Composer\IO\IOInterface;
use Composer\Package\CompletePackage;
use Composer\Package\Link;
use Composer\Semver\Constraint\Constraint;
use Php\Pie\DependencyResolver\FetchDependencyStatuses;
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
use Php\Pie\Platform\TargetPlatform;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(FetchDependencyStatuses::class)]
final class FetchDependencyStatusesTest extends TestCase
{
public function testNoRequiresReturnsEmptyArray(): void
{
$package = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3');
self::assertEquals([], (new FetchDependencyStatuses())(TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null, null), $this->createMock(Composer::class), $package));
}
public function testRequiresReturnsListOfStatuses(): void
{
$php = PhpBinaryPath::fromCurrentProcess();
$package = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3');
$package->setRequires([
'ext-core' => new Link('__root__', 'ext-core', new Constraint('=', $php->version() . '.0')),
'ext-nonsense_extension' => new Link('__root__', 'ext-nonsense_extension', new Constraint('=', '*')),
'ext-standard' => new Link('__root__', 'ext-standard', new Constraint('<', '1.0.0.0')),
]);
$deps = (new FetchDependencyStatuses())(
TargetPlatform::fromPhpBinaryPath($php, null, null),
Factory::create($this->createMock(IOInterface::class)),
$package,
);
self::assertCount(3, $deps);
self::assertSame('ext-core: == ' . $php->version() . '.0 ✅', $deps[0]->asPrettyString());
self::assertSame('ext-nonsense_extension: == * 🚫 (not installed)', $deps[1]->asPrettyString());
self::assertSame('ext-standard: < 1.0.0.0 🚫 (your version is ' . $php->version() . '.0)', $deps[2]->asPrettyString());
}
}

View File

@@ -39,4 +39,22 @@ final class InstalledPiePackagesTest extends TestCase
self::assertSame('bar2', $packages['bar2']->extensionName()->name());
self::assertSame('foo/bar2', $packages['bar2']->name());
}
public function testInvalidExtensionNamesAreFilteredOut(): void
{
$localRepo = $this->createMock(InstalledRepositoryInterface::class);
$localRepo->method('getPackages')->willReturn([
new CompletePackage('foo/invalid-extension-name', '1.2.3.0', '1.2.3'),
new CompletePackage('invalid-extension-name', '1.2.3.0', '1.2.3'),
new CompletePackage('invalid_extension_name', '1.2.3.0', '1.2.3'),
]);
$repoManager = $this->createMock(RepositoryManager::class);
$repoManager->method('getLocalRepository')->willReturn($localRepo);
$composer = $this->createMock(Composer::class);
$composer->method('getRepositoryManager')->willReturn($repoManager);
self::assertCount(0, (new InstalledPiePackages())->allPiePackages($composer));
}
}

View File

@@ -2,9 +2,9 @@
declare(strict_types=1);
namespace Php\PieUnitTest\SelfManage\BuildTools;
namespace Php\PieUnitTest\Platform;
use Php\Pie\SelfManage\BuildTools\PackageManager;
use Php\Pie\Platform\PackageManager;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

View File

@@ -400,10 +400,8 @@ final class PhpBinaryPathTest extends TestCase
$php = PhpBinaryPath::fromPhpBinaryPath($phpPath);
self::assertArrayHasKey('Core', $php->extensions());
self::assertNotEmpty($php->extensionPath());
self::assertInstanceOf(OperatingSystem::class, $php->operatingSystem());
self::assertNotEmpty($php->version());
self::assertNotEmpty($php->majorMinorVersion());
self::assertInstanceOf(Architecture::class, $php->machineType());
self::assertGreaterThan(0, $php->phpIntSize());
self::assertNotEmpty($php->phpinfo());
}

View File

@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Php\PieUnitTest\SelfManage\BuildTools;
use Php\Pie\Platform\PackageManager;
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
use Php\Pie\Platform\TargetPlatform;
use Php\Pie\SelfManage\BuildTools\BinaryBuildToolFinder;
use Php\Pie\SelfManage\BuildTools\PackageManager;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

View File

@@ -8,12 +8,12 @@ use Composer\IO\BufferIO;
use Php\Pie\Platform\Architecture;
use Php\Pie\Platform\OperatingSystem;
use Php\Pie\Platform\OperatingSystemFamily;
use Php\Pie\Platform\PackageManager;
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
use Php\Pie\Platform\TargetPlatform;
use Php\Pie\Platform\ThreadSafetyMode;
use Php\Pie\SelfManage\BuildTools\BinaryBuildToolFinder;
use Php\Pie\SelfManage\BuildTools\CheckAllBuildTools;
use Php\Pie\SelfManage\BuildTools\PackageManager;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Output\OutputInterface;