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
3 changes: 3 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
displayDetailsOnTestsThatTriggerWarnings="true"
failOnRisky="true"
failOnWarning="true">
<php>
<env name="COMPOSER_NO_INTERACTION" value="1" />
</php>
<testsuites>
<testsuite name="unit">
<directory>test/unit</directory>
Expand Down
24 changes: 16 additions & 8 deletions src/Command/CommandHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,15 @@
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
final class CommandHelper
{
public const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version';
public const OPTION_WITH_PHP_CONFIG = 'with-php-config';
public const OPTION_WITH_PHP_PATH = 'with-php-path';
public const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path';
public const OPTION_WORKING_DIRECTORY = 'working-dir';
private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs';
private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension';
private const OPTION_FORCE = 'force';
public const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version';
public const OPTION_WITH_PHP_CONFIG = 'with-php-config';
public const OPTION_WITH_PHP_PATH = 'with-php-path';
public const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path';
public const OPTION_WORKING_DIRECTORY = 'working-dir';
public const OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL = 'allow-non-interactive-project-install';
private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs';
private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension';
private const OPTION_FORCE = 'force';

/** @psalm-suppress UnusedConstructor */
private function __construct()
Expand Down Expand Up @@ -125,6 +126,13 @@ public static function configureDownloadBuildInstallOptions(Command $command, bo

self::configurePhpConfigOptions($command);

$command->addOption(
self::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL,
null,
InputOption::VALUE_NONE,
'When installing a PHP project, allow non-interactive project installations. Only used in certain contexts.',
);

/**
* 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...
Expand Down
73 changes: 55 additions & 18 deletions src/Command/InstallExtensionsForProjectCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
use Php\Pie\Installing\InstallForPhpProject\InstallPiePackageFromPath;
use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage;
use Php\Pie\Platform;
use Php\Pie\Platform\InstalledPiePackages;
use Php\Pie\Util\Emoji;
use Psr\Container\ContainerInterface;
Expand All @@ -30,13 +31,16 @@
use Symfony\Component\Console\Question\ChoiceQuestion;
use Throwable;

use function array_column;
use function array_keys;
use function array_map;
use function array_merge;
use function array_walk;
use function assert;
use function chdir;
use function count;
use function getcwd;
use function implode;
use function in_array;
use function is_dir;
use function is_string;
Expand Down Expand Up @@ -123,6 +127,16 @@ public function execute(InputInterface $input, OutputInterface $output): int
return $exit;
}

$allowNonInteractive = $input->hasOption(CommandHelper::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL) && $input->getOption(CommandHelper::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL);
if (! Platform::isInteractive() && ! $allowNonInteractive) {
$output->writeln(sprintf(
'<warning>Aborting! You are not running in interactive mode, and --%s was not specified.</warning>',
CommandHelper::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL,
));

return Command::FAILURE;
}

$targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);

$output->writeln(sprintf(
Expand Down Expand Up @@ -221,27 +235,46 @@ function (Link $link) use ($pieComposer, $phpEnabledExtensions, $installedPiePac
return;
}

$choiceQuestion = new ChoiceQuestion(
"\nThe following packages may be suitable, which would you like to install: ",
array_merge(
['None'],
array_map(
static function (array $match): string {
return sprintf('%s: %s', $match['name'], $match['description'] ?? 'no description available');
},
$matches,
if (! Platform::isInteractive() && count($matches) > 1) {
$anyErrorsHappened = true;

// @todo Figure out if there is a way to improve this, safely
$output->writeln(sprintf(
"<warning>Multiple packages were found for %s:</warning>\n %s\n\n<warning>This means you cannot `pie install` this project interactively for now.</warning>",
$extension->nameWithExtPrefix(),
implode("\n ", array_column($matches, 'name')),
));

return;
}

if (Platform::isInteractive()) {
$choiceQuestion = new ChoiceQuestion(
"\nThe following packages may be suitable, which would you like to install: ",
array_merge(
['None'],
array_map(
static function (array $match): string {
return sprintf('%s: %s', $match['name'], $match['description'] ?? 'no description available');
},
$matches,
),
),
),
0,
);
0,
);

$selectedPackageAnswer = (string) $helper->ask($input, $output, $choiceQuestion);
$selectedPackageAnswer = (string) $helper->ask($input, $output, $choiceQuestion);

if ($selectedPackageAnswer === 'None') {
$output->writeln('Okay I won\'t install anything for ' . $extension->name());
$anyErrorsHappened = true;
if ($selectedPackageAnswer === 'None') {
$output->writeln('Okay I won\'t install anything for ' . $extension->name());
$anyErrorsHappened = true;

return;
return;
}

$selectedPackageName = substr($selectedPackageAnswer, 0, (int) strpos($selectedPackageAnswer, ':'));
} else {
$selectedPackageName = $matches[0]['name'];
}

$requestInstallConstraint = '';
Expand All @@ -250,8 +283,12 @@ static function (array $match): string {
}

try {
$output->writeln(
sprintf('Invoking pie install of %s%s', $selectedPackageName, $requestInstallConstraint),
OutputInterface::VERBOSITY_VERBOSE,
);
$this->installSelectedPackage->withPieCli(
substr($selectedPackageAnswer, 0, (int) strpos($selectedPackageAnswer, ':')) . $requestInstallConstraint,
$selectedPackageName . $requestInstallConstraint,
$input,
$output,
);
Expand Down
67 changes: 49 additions & 18 deletions test/integration/Command/InstallExtensionsForProjectCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,6 @@ public function testInstallingExtensionsForPhpProject(): void
new Constraint('>=', '1.2.0.0-dev'),
new Constraint('<', '2.0.0.0-dev'),
]), Link::TYPE_REQUIRE, '^1.2'),
// 'ext-mismatching' => new Link('my/project', 'ext-mismatching', new MultiConstraint([
// new Constraint('>=', '2.0.0.0-dev'),
// new Constraint('<', '3.0.0.0-dev'),
// ]), Link::TYPE_REQUIRE, '^2.0'),
]);
$this->composerFactoryForProject->method('rootPackage')->willReturn($rootPackage);

Expand All @@ -119,20 +115,8 @@ public function testInstallingExtensionsForPhpProject(): void

$this->composerFactoryForProject->method('composer')->willReturn($composer);

// $this->installedPiePackages->method('allPiePackages')->willReturn([
// 'mismatching' => new Package(
// $this->createMock(CompletePackageInterface::class),
// ExtensionType::PhpModule,
// ExtensionName::normaliseFromString('mismatching'),
// 'vendor/mismatching',
// '1.9.3',
// null,
// ),
// ]);

$this->findMatchingPackages->method('for')->willReturn([
['name' => 'vendor1/foobar', 'description' => 'The official foobar implementation'],
['name' => 'vendor2/afoobar', 'description' => 'An improved async foobar extension'],
]);

$this->questionHelper->method('ask')->willReturn('vendor1/foobar: The official foobar implementation');
Expand All @@ -142,16 +126,63 @@ public function testInstallingExtensionsForPhpProject(): void
->with('vendor1/foobar:^1.2');

$this->commandTester->execute(
[],
['--allow-non-interactive-project-install' => true],
['verbosity' => BufferedOutput::VERBOSITY_VERY_VERBOSE],
);

$outputString = $this->commandTester->getDisplay();

$this->commandTester->assertCommandIsSuccessful();
$this->commandTester->assertCommandIsSuccessful($outputString);
self::assertStringContainsString('Checking extensions for your project my/project', $outputString);
self::assertStringContainsString('requires: ext-standard:* ✅ Already installed', $outputString);
self::assertStringContainsString('requires: ext-foobar:^1.2 🚫 Missing', $outputString);
}

public function testInstallingExtensionsForPhpProjectWithMultipleMatches(): 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, '*'),
'ext-foobar' => new Link('my/project', 'ext-foobar', new MultiConstraint([
new Constraint('>=', '1.2.0.0-dev'),
new Constraint('<', '2.0.0.0-dev'),
]), Link::TYPE_REQUIRE, '^1.2'),
]);
$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->findMatchingPackages->method('for')->willReturn([
['name' => 'vendor1/foobar', 'description' => 'The official foobar implementation'],
['name' => 'vendor2/afoobar', 'description' => 'An improved async foobar extension'],
]);

$this->questionHelper->method('ask')->willReturn('vendor1/foobar: The official foobar implementation');

$this->installSelectedPackage->expects(self::never())
->method('withPieCli');

$this->commandTester->execute(
['--allow-non-interactive-project-install' => true],
['verbosity' => BufferedOutput::VERBOSITY_VERY_VERBOSE],
);

$outputString = $this->commandTester->getDisplay();

self::assertSame(Command::FAILURE, $this->commandTester->getStatusCode());
self::assertStringContainsString('Checking extensions for your project my/project', $outputString);
self::assertStringContainsString('requires: ext-standard:* ✅ Already installed', $outputString);
self::assertStringContainsString('requires: ext-foobar:^1.2 🚫 Missing', $outputString);
self::assertStringContainsString('Multiple packages were found for ext-foobar', $outputString);
}

public function testInstallingExtensionsForPieProject(): void
Expand Down
Loading