diff --git a/docs/usage.md b/docs/usage.md index eb5bfcc2..f80d2ef9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -459,6 +459,16 @@ pie install \ > The `--allow-non-interactive-project-install` will no longer work. You must > provide package selections from PIE 1.5 onwards. +### Excluding require-dev extensions + +By default, PIE checks extensions declared in both `require` and +`require-dev`. To skip extensions that are only declared in `require-dev` +(for example, `ext-xdebug` in a production build), pass `--no-dev`: + +```bash +pie install --no-dev +``` + ## Install extensions from pie.lock If you have an existing `pie.json` and `pie.lock` for a given PHP install, diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 86b1e0b9..b54ae8bb 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -73,6 +73,7 @@ final class CommandHelper public const OPTION_WITH_PHP_PATH = 'with-php-path'; public const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path'; public const OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL = 'allow-non-interactive-project-install'; + public const OPTION_NO_DEV = 'no-dev'; private const OPTION_PACKAGE_SELECTION = 'select'; private const OPTION_WORKING_DIRECTORY = 'working-dir'; private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs'; @@ -180,6 +181,13 @@ public static function configureDownloadBuildInstallOptions(Command $command, bo 'Deprecated and ignored. Will emit a warning if used.', ); + $command->addOption( + self::OPTION_NO_DEV, + null, + InputOption::VALUE_NONE, + 'When checking a project for required extensions, exclude any extensions declared in the root package\'s require-dev.', + ); + $command->addOption( self::OPTION_PACKAGE_SELECTION, null, @@ -305,6 +313,11 @@ public static function determineForceInstallingPackageVersion(InputInterface $in return $input->hasOption(self::OPTION_FORCE) && $input->getOption(self::OPTION_FORCE); } + public static function noDev(InputInterface $input): bool + { + return $input->hasOption(self::OPTION_NO_DEV) && $input->getOption(self::OPTION_NO_DEV); + } + /** @return list */ public static function determineSuppressedDownloadUrlMethods(InputInterface $input): array { diff --git a/src/Command/InstallExtensionsForProjectCommand.php b/src/Command/InstallExtensionsForProjectCommand.php index 884b161c..da46af88 100644 --- a/src/Command/InstallExtensionsForProjectCommand.php +++ b/src/Command/InstallExtensionsForProjectCommand.php @@ -124,7 +124,10 @@ private function handlePhpProject(InputInterface $input, RootPackageInterface $r getcwd(), )); - $extensionsRequired = $this->determineExtensionsRequired->forProject($this->composerFactoryForProject->composer($this->io)); + $extensionsRequired = $this->determineExtensionsRequired->forProject( + $this->composerFactoryForProject->composer($this->io), + CommandHelper::noDev($input), + ); $pieComposer = PieComposerFactory::createPieComposer( $this->container, diff --git a/src/Installing/InstallForPhpProject/DetermineExtensionsRequired.php b/src/Installing/InstallForPhpProject/DetermineExtensionsRequired.php index 60619b64..1f411181 100644 --- a/src/Installing/InstallForPhpProject/DetermineExtensionsRequired.php +++ b/src/Installing/InstallForPhpProject/DetermineExtensionsRequired.php @@ -31,7 +31,7 @@ public static function linkFilter(Link $link): bool } /** @return array */ - public function forProject(Composer $composer): array + public function forProject(Composer $composer, bool $noDev = false): array { $requires = []; $removeDevPackages = []; @@ -44,8 +44,10 @@ public function forProject(Composer $composer): array $removeDevPackages = $installedRepo->getDevPackageNames(); } - foreach (array_filter($composer->getPackage()->getDevRequires(), [self::class, 'linkFilter']) as $require => $link) { - $requires[$require] = $link; + if (! $noDev) { + foreach (array_filter($composer->getPackage()->getDevRequires(), [self::class, 'linkFilter']) as $require => $link) { + $requires[$require] = $link; + } } $installedRepo = new InstalledRepository([$installedRepo, new RootPackageRepository(clone $composer->getPackage())]); diff --git a/test/integration/Command/InstallExtensionsForProjectCommandTest.php b/test/integration/Command/InstallExtensionsForProjectCommandTest.php index 8376dc9b..53000176 100644 --- a/test/integration/Command/InstallExtensionsForProjectCommandTest.php +++ b/test/integration/Command/InstallExtensionsForProjectCommandTest.php @@ -157,6 +157,79 @@ public function testInstallingExtensionsForPhpProject(): void self::assertStringContainsString('requires: ext-foobar:^1.2 🚫 Missing', $outputString); } + public function testInstallingExtensionsForPhpProjectIncludesDevRequiresByDefault(): void + { + $rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3'); + $rootPackage->setRequires([ + 'ext-standard' => new Link('my/project', 'ext-standard', new Constraint('=', '*'), Link::TYPE_REQUIRE, '*'), + ]); + $rootPackage->setDevRequires([ + 'ext-foobar' => new Link('my/project', 'ext-foobar', new Constraint('=', '*'), Link::TYPE_DEV_REQUIRE, '*'), + ]); + $this->composerFactoryForProject->method('rootPackage')->willReturn($rootPackage); + + $installedRepository = new InstalledArrayRepository([$rootPackage]); + + $repositoryManager = $this->createMock(RepositoryManager::class); + $repositoryManager->method('getLocalRepository')->willReturn($installedRepository); + + $composer = $this->createMock(Composer::class); + $composer->method('getPackage')->willReturn($rootPackage); + $composer->method('getRepositoryManager')->willReturn($repositoryManager); + + $this->composerFactoryForProject->method('composer')->willReturn($composer); + + $this->installedPiePackages->method('allPiePackages')->willReturn(new PiePackageList([])); + + $this->commandTester->execute( + [], + ['verbosity' => BufferedOutput::VERBOSITY_VERY_VERBOSE], + ); + + $outputString = $this->commandTester->getDisplay(); + + self::assertStringContainsString('Checking extensions for your project my/project', $outputString); + self::assertStringContainsString('requires: ext-standard:* ✅ Already installed', $outputString); + self::assertStringContainsString('ext-foobar:* 🚫 Missing', $outputString); + } + + public function testInstallingExtensionsForPhpProjectExcludesDevRequiresWhenNoDevOptionSet(): void + { + $rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3'); + $rootPackage->setRequires([ + 'ext-standard' => new Link('my/project', 'ext-standard', new Constraint('=', '*'), Link::TYPE_REQUIRE, '*'), + ]); + $rootPackage->setDevRequires([ + 'ext-foobar' => new Link('my/project', 'ext-foobar', new Constraint('=', '*'), Link::TYPE_DEV_REQUIRE, '*'), + ]); + $this->composerFactoryForProject->method('rootPackage')->willReturn($rootPackage); + + $installedRepository = new InstalledArrayRepository([$rootPackage]); + + $repositoryManager = $this->createMock(RepositoryManager::class); + $repositoryManager->method('getLocalRepository')->willReturn($installedRepository); + + $composer = $this->createMock(Composer::class); + $composer->method('getPackage')->willReturn($rootPackage); + $composer->method('getRepositoryManager')->willReturn($repositoryManager); + + $this->composerFactoryForProject->method('composer')->willReturn($composer); + + $this->installedPiePackages->method('allPiePackages')->willReturn(new PiePackageList([])); + + $this->commandTester->execute( + ['--no-dev' => true], + ['verbosity' => BufferedOutput::VERBOSITY_VERY_VERBOSE], + ); + + $outputString = $this->commandTester->getDisplay(); + + $this->commandTester->assertCommandIsSuccessful($outputString); + self::assertStringContainsString('Checking extensions for your project my/project', $outputString); + self::assertStringContainsString('requires: ext-standard:* ✅ Already installed', $outputString); + self::assertStringNotContainsString('ext-foobar', $outputString); + } + public function testInstallingExtensionsForPhpProjectWithMultipleMatches(): void { $rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3'); diff --git a/test/unit/Installing/InstallForPhpProject/DetermineExtensionsRequiredTest.php b/test/unit/Installing/InstallForPhpProject/DetermineExtensionsRequiredTest.php new file mode 100644 index 00000000..36c46a75 --- /dev/null +++ b/test/unit/Installing/InstallForPhpProject/DetermineExtensionsRequiredTest.php @@ -0,0 +1,79 @@ +createMock(RepositoryManager::class); + $repositoryManager->method('getLocalRepository')->willReturn($installedRepository); + + $composer = $this->createMock(Composer::class); + $composer->method('getPackage')->willReturn($rootPackage); + $composer->method('getRepositoryManager')->willReturn($repositoryManager); + + return $composer; + } + + public function testForProjectIncludesDevRequiresByDefault(): void + { + $rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3'); + $rootPackage->setRequires(['ext-redis' => new Link('my/project', 'ext-redis', new Constraint('=', '*'), Link::TYPE_REQUIRE, '*')]); + $rootPackage->setDevRequires(['ext-xdebug' => new Link('my/project', 'ext-xdebug', new Constraint('=', '*'), Link::TYPE_DEV_REQUIRE, '*')]); + + $composer = $this->composerFor($rootPackage, new InstalledArrayRepository([$rootPackage])); + + $requires = (new DetermineExtensionsRequired())->forProject($composer); + + self::assertArrayHasKey('ext-redis', $requires); + self::assertArrayHasKey('ext-xdebug', $requires); + } + + public function testForProjectExcludesRootDevRequiresWhenNoDevIsTrue(): void + { + $rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3'); + $rootPackage->setRequires(['ext-redis' => new Link('my/project', 'ext-redis', new Constraint('=', '*'), Link::TYPE_REQUIRE, '*')]); + $rootPackage->setDevRequires(['ext-xdebug' => new Link('my/project', 'ext-xdebug', new Constraint('=', '*'), Link::TYPE_DEV_REQUIRE, '*')]); + + $composer = $this->composerFor($rootPackage, new InstalledArrayRepository([$rootPackage])); + + $requires = (new DetermineExtensionsRequired())->forProject($composer, true); + + self::assertArrayHasKey('ext-redis', $requires); + self::assertArrayNotHasKey('ext-xdebug', $requires); + } + + public function testForProjectStillIncludesRequiresFromNonDevInstalledPackagesWhenNoDevIsTrue(): void + { + $rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3'); + $rootPackage->setDevRequires(['ext-xdebug' => new Link('my/project', 'ext-xdebug', new Constraint('=', '*'), Link::TYPE_DEV_REQUIRE, '*')]); + + $dependencyPackage = new CompletePackage('vendor/some-lib', '1.0.0.0', '1.0.0'); + $dependencyPackage->setRequires(['ext-mbstring' => new Link('vendor/some-lib', 'ext-mbstring', new Constraint('=', '*'), Link::TYPE_REQUIRE, '*')]); + + $installedRepository = new InstalledArrayRepository([$rootPackage, $dependencyPackage]); + $installedRepository->setDevPackageNames([]); + + $composer = $this->composerFor($rootPackage, $installedRepository); + + $requires = (new DetermineExtensionsRequired())->forProject($composer, true); + + self::assertArrayHasKey('ext-mbstring', $requires); + self::assertArrayNotHasKey('ext-xdebug', $requires); + } +}