Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions src/Command/CommandHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<DownloadUrlMethod> */
public static function determineSuppressedDownloadUrlMethods(InputInterface $input): array
{
Expand Down
5 changes: 4 additions & 1 deletion src/Command/InstallExtensionsForProjectCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static function linkFilter(Link $link): bool
}

/** @return array<string, Link> */
public function forProject(Composer $composer): array
public function forProject(Composer $composer, bool $noDev = false): array
{
$requires = [];
$removeDevPackages = [];
Expand All @@ -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())]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace Php\PieUnitTest\Installing\InstallForPhpProject;

use Composer\Composer;
use Composer\Package\CompletePackage;
use Composer\Package\Link;
use Composer\Package\RootPackage;
use Composer\Repository\InstalledArrayRepository;
use Composer\Repository\RepositoryManager;
use Composer\Semver\Constraint\Constraint;
use Php\Pie\Installing\InstallForPhpProject\DetermineExtensionsRequired;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(DetermineExtensionsRequired::class)]
final class DetermineExtensionsRequiredTest extends TestCase
{
private function composerFor(RootPackage $rootPackage, InstalledArrayRepository $installedRepository): Composer
{
$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);

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);
}
}
Loading