Skip to content

Commit 99d885f

Browse files
authored
Merge pull request #364 from asgrim/non-interactive-pie-project-install
Non interactive pie project install
2 parents d23fc4e + a82b4a5 commit 99d885f

4 files changed

Lines changed: 123 additions & 44 deletions

File tree

phpunit.xml.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
displayDetailsOnTestsThatTriggerWarnings="true"
1212
failOnRisky="true"
1313
failOnWarning="true">
14+
<php>
15+
<env name="COMPOSER_NO_INTERACTION" value="1" />
16+
</php>
1417
<testsuites>
1518
<testsuite name="unit">
1619
<directory>test/unit</directory>

src/Command/CommandHelper.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,15 @@
5151
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
5252
final class CommandHelper
5353
{
54-
public const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version';
55-
public const OPTION_WITH_PHP_CONFIG = 'with-php-config';
56-
public const OPTION_WITH_PHP_PATH = 'with-php-path';
57-
public const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path';
58-
public const OPTION_WORKING_DIRECTORY = 'working-dir';
59-
private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs';
60-
private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension';
61-
private const OPTION_FORCE = 'force';
54+
public const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version';
55+
public const OPTION_WITH_PHP_CONFIG = 'with-php-config';
56+
public const OPTION_WITH_PHP_PATH = 'with-php-path';
57+
public const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path';
58+
public const OPTION_WORKING_DIRECTORY = 'working-dir';
59+
public const OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL = 'allow-non-interactive-project-install';
60+
private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs';
61+
private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension';
62+
private const OPTION_FORCE = 'force';
6263

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

126127
self::configurePhpConfigOptions($command);
127128

129+
$command->addOption(
130+
self::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL,
131+
null,
132+
InputOption::VALUE_NONE,
133+
'When installing a PHP project, allow non-interactive project installations. Only used in certain contexts.',
134+
);
135+
128136
/**
129137
* Allows additional options for the `./configure` command to be passed here.
130138
* Note, this means you probably need to call {@see self::validateInput()} to validate the input manually...

src/Command/InstallExtensionsForProjectCommand.php

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
1818
use Php\Pie\Installing\InstallForPhpProject\InstallPiePackageFromPath;
1919
use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage;
20+
use Php\Pie\Platform;
2021
use Php\Pie\Platform\InstalledPiePackages;
2122
use Php\Pie\Util\Emoji;
2223
use Psr\Container\ContainerInterface;
@@ -30,13 +31,16 @@
3031
use Symfony\Component\Console\Question\ChoiceQuestion;
3132
use Throwable;
3233

34+
use function array_column;
3335
use function array_keys;
3436
use function array_map;
3537
use function array_merge;
3638
use function array_walk;
3739
use function assert;
3840
use function chdir;
41+
use function count;
3942
use function getcwd;
43+
use function implode;
4044
use function in_array;
4145
use function is_dir;
4246
use function is_string;
@@ -123,6 +127,16 @@ public function execute(InputInterface $input, OutputInterface $output): int
123127
return $exit;
124128
}
125129

130+
$allowNonInteractive = $input->hasOption(CommandHelper::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL) && $input->getOption(CommandHelper::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL);
131+
if (! Platform::isInteractive() && ! $allowNonInteractive) {
132+
$output->writeln(sprintf(
133+
'<warning>Aborting! You are not running in interactive mode, and --%s was not specified.</warning>',
134+
CommandHelper::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL,
135+
));
136+
137+
return Command::FAILURE;
138+
}
139+
126140
$targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
127141

128142
$output->writeln(sprintf(
@@ -221,27 +235,46 @@ function (Link $link) use ($pieComposer, $phpEnabledExtensions, $installedPiePac
221235
return;
222236
}
223237

224-
$choiceQuestion = new ChoiceQuestion(
225-
"\nThe following packages may be suitable, which would you like to install: ",
226-
array_merge(
227-
['None'],
228-
array_map(
229-
static function (array $match): string {
230-
return sprintf('%s: %s', $match['name'], $match['description'] ?? 'no description available');
231-
},
232-
$matches,
238+
if (! Platform::isInteractive() && count($matches) > 1) {
239+
$anyErrorsHappened = true;
240+
241+
// @todo Figure out if there is a way to improve this, safely
242+
$output->writeln(sprintf(
243+
"<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>",
244+
$extension->nameWithExtPrefix(),
245+
implode("\n ", array_column($matches, 'name')),
246+
));
247+
248+
return;
249+
}
250+
251+
if (Platform::isInteractive()) {
252+
$choiceQuestion = new ChoiceQuestion(
253+
"\nThe following packages may be suitable, which would you like to install: ",
254+
array_merge(
255+
['None'],
256+
array_map(
257+
static function (array $match): string {
258+
return sprintf('%s: %s', $match['name'], $match['description'] ?? 'no description available');
259+
},
260+
$matches,
261+
),
233262
),
234-
),
235-
0,
236-
);
263+
0,
264+
);
237265

238-
$selectedPackageAnswer = (string) $helper->ask($input, $output, $choiceQuestion);
266+
$selectedPackageAnswer = (string) $helper->ask($input, $output, $choiceQuestion);
239267

240-
if ($selectedPackageAnswer === 'None') {
241-
$output->writeln('Okay I won\'t install anything for ' . $extension->name());
242-
$anyErrorsHappened = true;
268+
if ($selectedPackageAnswer === 'None') {
269+
$output->writeln('Okay I won\'t install anything for ' . $extension->name());
270+
$anyErrorsHappened = true;
243271

244-
return;
272+
return;
273+
}
274+
275+
$selectedPackageName = substr($selectedPackageAnswer, 0, (int) strpos($selectedPackageAnswer, ':'));
276+
} else {
277+
$selectedPackageName = $matches[0]['name'];
245278
}
246279

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

252285
try {
286+
$output->writeln(
287+
sprintf('Invoking pie install of %s%s', $selectedPackageName, $requestInstallConstraint),
288+
OutputInterface::VERBOSITY_VERBOSE,
289+
);
253290
$this->installSelectedPackage->withPieCli(
254-
substr($selectedPackageAnswer, 0, (int) strpos($selectedPackageAnswer, ':')) . $requestInstallConstraint,
291+
$selectedPackageName . $requestInstallConstraint,
255292
$input,
256293
$output,
257294
);

test/integration/Command/InstallExtensionsForProjectCommandTest.php

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,6 @@ public function testInstallingExtensionsForPhpProject(): void
101101
new Constraint('>=', '1.2.0.0-dev'),
102102
new Constraint('<', '2.0.0.0-dev'),
103103
]), Link::TYPE_REQUIRE, '^1.2'),
104-
// 'ext-mismatching' => new Link('my/project', 'ext-mismatching', new MultiConstraint([
105-
// new Constraint('>=', '2.0.0.0-dev'),
106-
// new Constraint('<', '3.0.0.0-dev'),
107-
// ]), Link::TYPE_REQUIRE, '^2.0'),
108104
]);
109105
$this->composerFactoryForProject->method('rootPackage')->willReturn($rootPackage);
110106

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

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

122-
// $this->installedPiePackages->method('allPiePackages')->willReturn([
123-
// 'mismatching' => new Package(
124-
// $this->createMock(CompletePackageInterface::class),
125-
// ExtensionType::PhpModule,
126-
// ExtensionName::normaliseFromString('mismatching'),
127-
// 'vendor/mismatching',
128-
// '1.9.3',
129-
// null,
130-
// ),
131-
// ]);
132-
133118
$this->findMatchingPackages->method('for')->willReturn([
134119
['name' => 'vendor1/foobar', 'description' => 'The official foobar implementation'],
135-
['name' => 'vendor2/afoobar', 'description' => 'An improved async foobar extension'],
136120
]);
137121

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

144128
$this->commandTester->execute(
145-
[],
129+
['--allow-non-interactive-project-install' => true],
146130
['verbosity' => BufferedOutput::VERBOSITY_VERY_VERBOSE],
147131
);
148132

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

151-
$this->commandTester->assertCommandIsSuccessful();
135+
$this->commandTester->assertCommandIsSuccessful($outputString);
136+
self::assertStringContainsString('Checking extensions for your project my/project', $outputString);
137+
self::assertStringContainsString('requires: ext-standard:* ✅ Already installed', $outputString);
138+
self::assertStringContainsString('requires: ext-foobar:^1.2 🚫 Missing', $outputString);
139+
}
140+
141+
public function testInstallingExtensionsForPhpProjectWithMultipleMatches(): void
142+
{
143+
$rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3');
144+
$rootPackage->setRequires([
145+
'ext-standard' => new Link('my/project', 'ext-standard', new Constraint('=', '*'), Link::TYPE_REQUIRE, '*'),
146+
'ext-foobar' => new Link('my/project', 'ext-foobar', new MultiConstraint([
147+
new Constraint('>=', '1.2.0.0-dev'),
148+
new Constraint('<', '2.0.0.0-dev'),
149+
]), Link::TYPE_REQUIRE, '^1.2'),
150+
]);
151+
$this->composerFactoryForProject->method('rootPackage')->willReturn($rootPackage);
152+
153+
$installedRepository = new InstalledArrayRepository([$rootPackage]);
154+
155+
$repositoryManager = $this->createMock(RepositoryManager::class);
156+
$repositoryManager->method('getLocalRepository')->willReturn($installedRepository);
157+
158+
$composer = $this->createMock(Composer::class);
159+
$composer->method('getPackage')->willReturn($rootPackage);
160+
$composer->method('getRepositoryManager')->willReturn($repositoryManager);
161+
162+
$this->composerFactoryForProject->method('composer')->willReturn($composer);
163+
164+
$this->findMatchingPackages->method('for')->willReturn([
165+
['name' => 'vendor1/foobar', 'description' => 'The official foobar implementation'],
166+
['name' => 'vendor2/afoobar', 'description' => 'An improved async foobar extension'],
167+
]);
168+
169+
$this->questionHelper->method('ask')->willReturn('vendor1/foobar: The official foobar implementation');
170+
171+
$this->installSelectedPackage->expects(self::never())
172+
->method('withPieCli');
173+
174+
$this->commandTester->execute(
175+
['--allow-non-interactive-project-install' => true],
176+
['verbosity' => BufferedOutput::VERBOSITY_VERY_VERBOSE],
177+
);
178+
179+
$outputString = $this->commandTester->getDisplay();
180+
181+
self::assertSame(Command::FAILURE, $this->commandTester->getStatusCode());
152182
self::assertStringContainsString('Checking extensions for your project my/project', $outputString);
153183
self::assertStringContainsString('requires: ext-standard:* ✅ Already installed', $outputString);
154184
self::assertStringContainsString('requires: ext-foobar:^1.2 🚫 Missing', $outputString);
185+
self::assertStringContainsString('Multiple packages were found for ext-foobar', $outputString);
155186
}
156187

157188
public function testInstallingExtensionsForPieProject(): void

0 commit comments

Comments
 (0)