diff --git a/docs/extension-maintainers.md b/docs/extension-maintainers.md index 9b6036cf..e809daa9 100644 --- a/docs/extension-maintainers.md +++ b/docs/extension-maintainers.md @@ -387,35 +387,44 @@ 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-gpgme | ✅ | apt, apk, dnf, yum | -| lib-pam | ✅ | apt, apk, dnf, yum | -| lib-sasl | ✅ | ❌ | -| lib-onig | ✅ | ❌ | -| lib-odbc | ✅ | ❌ | -| lib-capstone | ✅ | ❌ | -| lib-pcre | ✅ | ❌ | -| lib-edit | ✅ | ❌ | -| lib-snmp | ✅ | ❌ | -| lib-argon2 | ✅ | ❌ | -| lib-uriparser | ✅ | ❌ | -| lib-exslt | ✅ | ❌ | +| Library | Checked by PIE | Auto-installs in | +|-----------------|----------------|------------------| +| lib-curl | ✅ | apt, apk, dn | +| lib-enchant | ✅ | ❌ | +| lib-enchant-2 | ✅ | ❌ | +| lib-sodium | ✅ | apt, apk, dnf | +| lib-ffi | ✅ | apt, apk, dnf | +| lib-xslt | ✅ | apt, apk, dnf | +| lib-zip | ✅ | apt, apk, dnf | +| lib-png | ✅ | ❌ | +| lib-avif | ✅ | ❌ | +| lib-webp | ✅ | ❌ | +| lib-jpeg | ✅ | apt, apk, dnf | +| lib-xpm | ✅ | ❌ | +| lib-freetype2 | ✅ | ❌ | +| lib-gdlib | ✅ | ❌ | +| lib-gmp | ✅ | ❌ | +| lib-gpgme | ✅ | apt, apk, dnf | +| lib-pam | ✅ | apt, apk, dnf | +| lib-sasl | ✅ | apt, apk, dnf | +| lib-onig | ✅ | ❌ | +| lib-odbc | ✅ | ❌ | +| lib-capstone | ✅ | ❌ | +| lib-pcre | ✅ | ❌ | +| lib-edit | ✅ | ❌ | +| lib-snmp | ✅ | ❌ | +| lib-argon2 | ✅ | ❌ | +| lib-uriparser | ✅ | ❌ | +| lib-exslt | ✅ | ❌ | +| lib-yaml | ✅ | apt, apk, dnf | +| lib-uv | ✅ | apt, apk, dnf | +| lib-uuid | ✅ | apt, apk, dnf | +| lib-rdkafka | ✅ | apt, apk, dnf | +| lib-zlib | ✅ | apt, apk, dnf | +| lib-memcached | ✅ | apt, apk, dnf | +| lib-imagemagick | ✅ | apt, apk, dnf | +| lib-event | ✅ | apt, apk, dnf | +| lib-openssl | ✅ | apt, apk, dnf | #### Checking the extension will work diff --git a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php index b1a7bccf..de2adbd7 100644 --- a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php +++ b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php @@ -64,6 +64,15 @@ class PhpBinaryPathBasedPlatformRepository extends PlatformRepository 'argon2' => 'libargon2', 'uriparser' => 'liburiparser', 'exslt' => 'libexslt', + 'yaml' => 'yaml-0.1', + 'uv' => 'libuv', + 'uuid' => 'uuid', + 'rdkafka' => 'rdkafka', + 'zlib' => 'zlib', + 'memcached' => 'libmemcached', + 'imagemagick' => 'MagickWand', + 'event' => 'libevent', + 'openssl' => 'openssl', ]; private VersionParser $versionParser; diff --git a/src/DependencyResolver/DependencyInstaller/SystemDependenciesDefinition.php b/src/DependencyResolver/DependencyInstaller/SystemDependenciesDefinition.php index a3cf7dde..705e39a0 100644 --- a/src/DependencyResolver/DependencyInstaller/SystemDependenciesDefinition.php +++ b/src/DependencyResolver/DependencyInstaller/SystemDependenciesDefinition.php @@ -76,6 +76,76 @@ public static function default(): self PackageManager::Microdnf->value => 'pkgconfig(pam)', PackageManager::Yum->value => 'pkgconfig(pam)', ], + 'yaml' => [ + PackageManager::Apt->value => 'libyaml-dev', + PackageManager::Apk->value => 'yaml-dev', + PackageManager::Dnf->value => 'pkgconfig(yaml-0.1)', + PackageManager::Microdnf->value => 'pkgconfig(yaml-0.1)', + PackageManager::Yum->value => 'pkgconfig(yaml-0.1)', + ], + 'uv' => [ + PackageManager::Apt->value => 'libuv1-dev', + PackageManager::Apk->value => 'libuv-dev', + PackageManager::Dnf->value => 'pkgconfig(libuv)', + PackageManager::Microdnf->value => 'pkgconfig(libuv)', + PackageManager::Yum->value => 'pkgconfig(libuv)', + ], + 'uuid' => [ + PackageManager::Apt->value => 'uuid-dev', + PackageManager::Apk->value => 'util-linux-dev', + PackageManager::Dnf->value => 'pkgconfig(uuid)', + PackageManager::Microdnf->value => 'pkgconfig(uuid)', + PackageManager::Yum->value => 'pkgconfig(uuid)', + ], + 'rdkafka' => [ + PackageManager::Apt->value => 'librdkafka-dev', + PackageManager::Apk->value => 'librdkafka-dev', + PackageManager::Dnf->value => 'pkgconfig(rdkafka)', + PackageManager::Microdnf->value => 'pkgconfig(rdkafka)', + PackageManager::Yum->value => 'pkgconfig(rdkafka)', + ], + 'zlib' => [ + PackageManager::Apt->value => 'zlib1g-dev', + PackageManager::Apk->value => 'zlib-dev', + PackageManager::Dnf->value => 'pkgconfig(zlib)', + PackageManager::Microdnf->value => 'pkgconfig(zlib)', + PackageManager::Yum->value => 'pkgconfig(zlib)', + ], + 'sasl' => [ + PackageManager::Apt->value => 'libsasl2-dev', + PackageManager::Apk->value => 'cyrus-sasl-dev', + PackageManager::Dnf->value => 'pkgconfig(libsasl2)', + PackageManager::Microdnf->value => 'pkgconfig(libsasl2)', + PackageManager::Yum->value => 'pkgconfig(libsasl2)', + ], + 'memcached' => [ + PackageManager::Apt->value => 'libmemcached-dev', + PackageManager::Apk->value => 'libmemcached-dev', + PackageManager::Dnf->value => 'pkgconfig(libmemcached)', + PackageManager::Microdnf->value => 'pkgconfig(libmemcached)', + PackageManager::Yum->value => 'pkgconfig(libmemcached)', + ], + 'imagemagick' => [ + PackageManager::Apt->value => 'libmagickwand-dev', + PackageManager::Apk->value => 'imagemagick-dev', + PackageManager::Dnf->value => 'pkgconfig(MagickWand)', + PackageManager::Microdnf->value => 'pkgconfig(MagickWand)', + PackageManager::Yum->value => 'pkgconfig(MagickWand)', + ], + 'event' => [ + PackageManager::Apt->value => 'libevent-dev', + PackageManager::Apk->value => 'libevent-dev', + PackageManager::Dnf->value => 'pkgconfig(libevent)', + PackageManager::Microdnf->value => 'pkgconfig(libevent)', + PackageManager::Yum->value => 'pkgconfig(libevent)', + ], + 'openssl' => [ + PackageManager::Apt->value => 'libssl-dev', + PackageManager::Apk->value => 'openssl-dev', + PackageManager::Dnf->value => 'pkgconfig(openssl)', + PackageManager::Microdnf->value => 'pkgconfig(openssl)', + PackageManager::Yum->value => 'pkgconfig(openssl)', + ], ]); } } diff --git a/test/unit/ComposerIntegration/PhpBinaryPathBasedPlatformRepositoryTest.php b/test/unit/ComposerIntegration/PhpBinaryPathBasedPlatformRepositoryTest.php index f9df5e9a..56ed7f05 100644 --- a/test/unit/ComposerIntegration/PhpBinaryPathBasedPlatformRepositoryTest.php +++ b/test/unit/ComposerIntegration/PhpBinaryPathBasedPlatformRepositoryTest.php @@ -190,6 +190,69 @@ public function testPlatformRepositoryContainsLibrariesReportedByPkgConfig(): vo ); } + /** @return array */ + public static function librariesDetectedUsingPkgConfig(): array + { + return [ + 'curl' => ['curl', 'libcurl', '8.12.1'], + 'enchant' => ['enchant', 'enchant', '1.6.0'], + 'enchant-2' => ['enchant-2', 'enchant-2', '2.3.3'], + 'sodium' => ['sodium', 'libsodium', '1.0.18'], + 'ffi' => ['ffi', 'libffi', '3.4.6'], + 'xslt' => ['xslt', 'libxslt', '1.1.39'], + 'zip' => ['zip', 'libzip', '1.10.1'], + 'png' => ['png', 'libpng', '1.6.43'], + 'avif' => ['avif', 'libavif', '1.0.4'], + 'webp' => ['webp', 'libwebp', '1.3.2'], + 'jpeg' => ['jpeg', 'libjpeg', '2.1.5'], + 'xpm' => ['xpm', 'xpm', '3.5.17'], + 'freetype2' => ['freetype2', 'freetype2', '26.1.20'], + 'gdlib' => ['gdlib', 'gdlib', '2.3.3'], + 'gmp' => ['gmp', 'gmp', '6.3.0'], + 'gpgme' => ['gpgme', 'gpgme', '1.18.0'], + 'pam' => ['pam', 'pam', '1.5.3'], + 'sasl' => ['sasl', 'libsasl2', '2.1.28'], + 'onig' => ['onig', 'oniguruma', '6.9.9'], + 'odbc' => ['odbc', 'libiodbc', '3.52.16'], + 'capstone' => ['capstone', 'capstone', '4.0.2'], + 'pcre' => ['pcre', 'libpcre2-8', '10.42'], + 'edit' => ['edit', 'libedit', '3.1'], + 'snmp' => ['snmp', 'netsnmp', '5.9.4'], + 'argon2' => ['argon2', 'libargon2', '20190702'], + 'uriparser' => ['uriparser', 'liburiparser', '0.9.7'], + 'exslt' => ['exslt', 'libexslt', '0.8.21'], + 'yaml' => ['yaml', 'yaml-0.1', '0.2.5'], + 'uv' => ['uv', 'libuv', '1.48.0'], + 'uuid' => ['uuid', 'uuid', '2.39.3'], + 'rdkafka' => ['rdkafka', 'rdkafka', '2.3.0'], + 'zlib' => ['zlib', 'zlib', '1.3'], + 'memcached' => ['memcached', 'libmemcached', '1.1.4'], + 'imagemagick' => ['imagemagick', 'MagickWand', '6.9.12'], + 'event' => ['event', 'libevent', '2.1.12-stable'], + 'openssl' => ['openssl', 'openssl', '3.0.13'], + ]; + } + + #[DataProvider('librariesDetectedUsingPkgConfig')] + public function testLibraryIsDetectedUsingPkgConfig(string $alias, string $pkgConfigName, string $version): void + { + $installedPiePackages = $this->createMock(InstalledPiePackages::class); + $installedPiePackages->method('allPiePackages')->willReturn(new PiePackageList([])); + + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath->method('version')->willReturn('8.1.0'); + $phpBinaryPath->method('extensions')->willReturn([]); + + $pkgConfig = $this->createMock(PkgConfig::class); + $pkgConfig->method('versionsOf')->willReturn([$pkgConfigName => $version]); + + $platformRepository = new PhpBinaryPathBasedPlatformRepository($phpBinaryPath, $this->createMock(Composer::class), $installedPiePackages, $pkgConfig, []); + + $lib = $platformRepository->findPackage('lib-' . $alias, '*'); + self::assertNotNull($lib); + self::assertSame($version, $lib->getPrettyVersion()); + } + /** @return array */ public static function installedLibraries(): array { @@ -227,6 +290,15 @@ public static function installedLibraries(): array ['argon2', 'libargon2'], ['uriparser', 'liburiparser'], ['exslt', 'libexslt'], + ['yaml', 'yaml-0.1'], + ['uv', 'libuv'], + ['uuid', 'uuid'], + ['rdkafka', 'rdkafka'], + ['zlib', 'zlib'], + ['memcached', 'libmemcached'], + ['imagemagick', 'MagickWand'], + ['event', 'libevent'], + ['openssl', 'openssl'], ], static function (array $pkg): bool { try { diff --git a/test/unit/DependencyResolver/DependencyInstaller/PrescanSystemDependenciesTest.php b/test/unit/DependencyResolver/DependencyInstaller/PrescanSystemDependenciesTest.php index 5d5d5bfd..64fdade8 100644 --- a/test/unit/DependencyResolver/DependencyInstaller/PrescanSystemDependenciesTest.php +++ b/test/unit/DependencyResolver/DependencyInstaller/PrescanSystemDependenciesTest.php @@ -30,10 +30,13 @@ use Php\Pie\Platform\TargetPlatform; use Php\Pie\Platform\ThreadSafetyMode; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Output\StreamOutput; +use function implode; + #[CoversClass(PrescanSystemDependencies::class)] final class PrescanSystemDependenciesTest extends TestCase { @@ -370,4 +373,130 @@ public function testMissingDependenciesAreNotInstalledWhenShouldNotAutoInstallAn self::assertStringContainsString('You may need to run: echo "fake installing libbar-dev"', $outputString); self::assertStringNotContainsString('Need to install missing system dependencies', $outputString); } + + /** @return array */ + public static function defaultDefinitions(): array + { + return [ + 'sodium with apt' => ['sodium', PackageManager::Apt, 'libsodium-dev'], + 'sodium with apk' => ['sodium', PackageManager::Apk, 'libsodium-dev'], + 'sodium with dnf' => ['sodium', PackageManager::Dnf, 'pkgconfig(libsodium)'], + 'sodium with microdnf' => ['sodium', PackageManager::Microdnf, 'pkgconfig(libsodium)'], + 'sodium with yum' => ['sodium', PackageManager::Yum, 'pkgconfig(libsodium)'], + 'jpeg with apt' => ['jpeg', PackageManager::Apt, 'libjpeg-dev'], + 'jpeg with apk' => ['jpeg', PackageManager::Apk, 'libjpeg-turbo-dev'], + 'jpeg with dnf' => ['jpeg', PackageManager::Dnf, 'pkgconfig(libjpeg)'], + 'jpeg with microdnf' => ['jpeg', PackageManager::Microdnf, 'pkgconfig(libjpeg)'], + 'jpeg with yum' => ['jpeg', PackageManager::Yum, 'pkgconfig(libjpeg)'], + 'zip with apt' => ['zip', PackageManager::Apt, 'libzip-dev'], + 'zip with apk' => ['zip', PackageManager::Apk, 'libzip-dev'], + 'zip with dnf' => ['zip', PackageManager::Dnf, 'pkgconfig(libzip)'], + 'zip with microdnf' => ['zip', PackageManager::Microdnf, 'pkgconfig(libzip)'], + 'zip with yum' => ['zip', PackageManager::Yum, 'pkgconfig(libzip)'], + 'xslt with apt' => ['xslt', PackageManager::Apt, 'libxslt1-dev'], + 'xslt with apk' => ['xslt', PackageManager::Apk, 'libxslt-dev'], + 'xslt with dnf' => ['xslt', PackageManager::Dnf, 'pkgconfig(libxslt)'], + 'xslt with microdnf' => ['xslt', PackageManager::Microdnf, 'pkgconfig(libxslt)'], + 'xslt with yum' => ['xslt', PackageManager::Yum, 'pkgconfig(libxslt)'], + 'ffi with apt' => ['ffi', PackageManager::Apt, 'libffi-dev'], + 'ffi with apk' => ['ffi', PackageManager::Apk, 'libffi-dev'], + 'ffi with dnf' => ['ffi', PackageManager::Dnf, 'pkgconfig(libffi)'], + 'ffi with microdnf' => ['ffi', PackageManager::Microdnf, 'pkgconfig(libffi)'], + 'ffi with yum' => ['ffi', PackageManager::Yum, 'pkgconfig(libffi)'], + 'curl with apt' => ['curl', PackageManager::Apt, 'libcurl4-openssl-dev'], + 'curl with apk' => ['curl', PackageManager::Apk, 'curl-dev'], + 'curl with dnf' => ['curl', PackageManager::Dnf, 'pkgconfig(libcurl)'], + 'curl with microdnf' => ['curl', PackageManager::Microdnf, 'pkgconfig(libcurl)'], + 'curl with yum' => ['curl', PackageManager::Yum, 'pkgconfig(libcurl)'], + 'gpgme with apt' => ['gpgme', PackageManager::Apt, 'libgpgme-dev'], + 'gpgme with apk' => ['gpgme', PackageManager::Apk, 'gpgme-dev'], + 'gpgme with dnf' => ['gpgme', PackageManager::Dnf, 'pkgconfig(gpgme)'], + 'gpgme with microdnf' => ['gpgme', PackageManager::Microdnf, 'pkgconfig(gpgme)'], + 'gpgme with yum' => ['gpgme', PackageManager::Yum, 'pkgconfig(gpgme)'], + 'pam with apt' => ['pam', PackageManager::Apt, 'libpam0g-dev'], + 'pam with apk' => ['pam', PackageManager::Apk, 'linux-pam-dev'], + 'pam with dnf' => ['pam', PackageManager::Dnf, 'pkgconfig(pam)'], + 'pam with microdnf' => ['pam', PackageManager::Microdnf, 'pkgconfig(pam)'], + 'pam with yum' => ['pam', PackageManager::Yum, 'pkgconfig(pam)'], + 'yaml with apt' => ['yaml', PackageManager::Apt, 'libyaml-dev'], + 'yaml with apk' => ['yaml', PackageManager::Apk, 'yaml-dev'], + 'yaml with dnf' => ['yaml', PackageManager::Dnf, 'pkgconfig(yaml-0.1)'], + 'yaml with microdnf' => ['yaml', PackageManager::Microdnf, 'pkgconfig(yaml-0.1)'], + 'yaml with yum' => ['yaml', PackageManager::Yum, 'pkgconfig(yaml-0.1)'], + 'uv with apt' => ['uv', PackageManager::Apt, 'libuv1-dev'], + 'uv with apk' => ['uv', PackageManager::Apk, 'libuv-dev'], + 'uv with dnf' => ['uv', PackageManager::Dnf, 'pkgconfig(libuv)'], + 'uv with microdnf' => ['uv', PackageManager::Microdnf, 'pkgconfig(libuv)'], + 'uv with yum' => ['uv', PackageManager::Yum, 'pkgconfig(libuv)'], + 'uuid with apt' => ['uuid', PackageManager::Apt, 'uuid-dev'], + 'uuid with apk' => ['uuid', PackageManager::Apk, 'util-linux-dev'], + 'uuid with dnf' => ['uuid', PackageManager::Dnf, 'pkgconfig(uuid)'], + 'uuid with microdnf' => ['uuid', PackageManager::Microdnf, 'pkgconfig(uuid)'], + 'uuid with yum' => ['uuid', PackageManager::Yum, 'pkgconfig(uuid)'], + 'rdkafka with apt' => ['rdkafka', PackageManager::Apt, 'librdkafka-dev'], + 'rdkafka with apk' => ['rdkafka', PackageManager::Apk, 'librdkafka-dev'], + 'rdkafka with dnf' => ['rdkafka', PackageManager::Dnf, 'pkgconfig(rdkafka)'], + 'rdkafka with microdnf' => ['rdkafka', PackageManager::Microdnf, 'pkgconfig(rdkafka)'], + 'rdkafka with yum' => ['rdkafka', PackageManager::Yum, 'pkgconfig(rdkafka)'], + 'zlib with apt' => ['zlib', PackageManager::Apt, 'zlib1g-dev'], + 'zlib with apk' => ['zlib', PackageManager::Apk, 'zlib-dev'], + 'zlib with dnf' => ['zlib', PackageManager::Dnf, 'pkgconfig(zlib)'], + 'zlib with microdnf' => ['zlib', PackageManager::Microdnf, 'pkgconfig(zlib)'], + 'zlib with yum' => ['zlib', PackageManager::Yum, 'pkgconfig(zlib)'], + 'sasl with apt' => ['sasl', PackageManager::Apt, 'libsasl2-dev'], + 'sasl with apk' => ['sasl', PackageManager::Apk, 'cyrus-sasl-dev'], + 'sasl with dnf' => ['sasl', PackageManager::Dnf, 'pkgconfig(libsasl2)'], + 'sasl with microdnf' => ['sasl', PackageManager::Microdnf, 'pkgconfig(libsasl2)'], + 'sasl with yum' => ['sasl', PackageManager::Yum, 'pkgconfig(libsasl2)'], + 'memcached with apt' => ['memcached', PackageManager::Apt, 'libmemcached-dev'], + 'memcached with apk' => ['memcached', PackageManager::Apk, 'libmemcached-dev'], + 'memcached with dnf' => ['memcached', PackageManager::Dnf, 'pkgconfig(libmemcached)'], + 'memcached with microdnf' => ['memcached', PackageManager::Microdnf, 'pkgconfig(libmemcached)'], + 'memcached with yum' => ['memcached', PackageManager::Yum, 'pkgconfig(libmemcached)'], + 'imagemagick with apt' => ['imagemagick', PackageManager::Apt, 'libmagickwand-dev'], + 'imagemagick with apk' => ['imagemagick', PackageManager::Apk, 'imagemagick-dev'], + 'imagemagick with dnf' => ['imagemagick', PackageManager::Dnf, 'pkgconfig(MagickWand)'], + 'imagemagick with microdnf' => ['imagemagick', PackageManager::Microdnf, 'pkgconfig(MagickWand)'], + 'imagemagick with yum' => ['imagemagick', PackageManager::Yum, 'pkgconfig(MagickWand)'], + 'event with apt' => ['event', PackageManager::Apt, 'libevent-dev'], + 'event with apk' => ['event', PackageManager::Apk, 'libevent-dev'], + 'event with dnf' => ['event', PackageManager::Dnf, 'pkgconfig(libevent)'], + 'event with microdnf' => ['event', PackageManager::Microdnf, 'pkgconfig(libevent)'], + 'event with yum' => ['event', PackageManager::Yum, 'pkgconfig(libevent)'], + 'openssl with apt' => ['openssl', PackageManager::Apt, 'libssl-dev'], + 'openssl with apk' => ['openssl', PackageManager::Apk, 'openssl-dev'], + 'openssl with dnf' => ['openssl', PackageManager::Dnf, 'pkgconfig(openssl)'], + 'openssl with microdnf' => ['openssl', PackageManager::Microdnf, 'pkgconfig(openssl)'], + 'openssl with yum' => ['openssl', PackageManager::Yum, 'pkgconfig(openssl)'], + ]; + } + + #[DataProvider('defaultDefinitions')] + public function testDefaultDefinitionProposesSystemPackageForMissingLibrary(string $alias, PackageManager $packageManager, string $expectedSystemPackage): void + { + $scanner = new PrescanSystemDependencies( + $this->dependencyResolver, + $this->fetchDependencyStatuses, + SystemDependenciesDefinition::default(), + $packageManager, + $this->io, + ); + + $request = new RequestedPackageAndVersion('foo/foo', null); + $composerPackage = new CompletePackage('foo/foo', '1.0.0.0', '1.0.0'); + $this->dependencyResolver->method('__invoke') + ->willReturn(new ResolvedPackageRequest(Package::fromComposerCompletePackage($composerPackage), $request)); + + $this->fetchDependencyStatuses->method('__invoke') + ->willReturn([ + new DependencyStatus('lib-' . $alias, (new VersionParser())->parseConstraints('*'), null), + ]); + + ($scanner)($this->composer, $this->targetPlatform, $this->platformRepository, $request, false); + + self::assertStringContainsString( + 'You may need to run: ' . implode(' ', $packageManager->installCommand([$expectedSystemPackage])), + $this->io->getOutput(), + ); + } }