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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,36 @@ You must now add "extension=example_pie_extension" to your php.ini
$
```

## Installing all extensions for a project

When in your PHP project, you can install any missing top-level extensions:

```
$ pie install
🥧 PHP Installer for Extensions (PIE), 0.9.0, from The PHP Foundation
You are running PHP 8.3.19
Target PHP installation: 8.3.19 nts, on Linux/OSX/etc x86_64 (from /usr/bin/php8.3)
Checking extensions for your project your-vendor/your-project
requires: curl ✅ Already installed
requires: intl ✅ Already installed
requires: json ✅ Already installed
requires: example_pie_extension ⚠️ Missing

The following packages may be suitable, which would you like to install:
[0] None
[1] asgrim/example-pie-extension: Example PIE extension
> 1
> 🥧 PHP Installer for Extensions (PIE), 0.9.0, from The PHP Foundation
> This command may need elevated privileges, and may prompt you for your password.
> You are running PHP 8.3.19
> Target PHP installation: 8.3.19 nts, on Linux/OSX/etc x86_64 (from /usr/bin/php8.3)
> Found package: asgrim/example-pie-extension:2.0.2 which provides ext-example_pie_extension
... (snip) ...
> ✅ Extension is enabled and loaded in /usr/bin/php8.3

Finished checking extensions.
```

## More documentation...

The full documentation for PIE can be found in [usage](./docs/usage.md) docs.
2 changes: 2 additions & 0 deletions bin/pie
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ declare(strict_types=1);
namespace Php\Pie;

use Php\Pie\Command\BuildCommand;
use Php\Pie\Command\InstallExtensionsForProjectCommand;
use Php\Pie\Command\DownloadCommand;
use Php\Pie\Command\InfoCommand;
use Php\Pie\Command\InstallCommand;
Expand Down Expand Up @@ -44,6 +45,7 @@ $application->setCommandLoader(new ContainerCommandLoader(
'repository:remove' => RepositoryRemoveCommand::class,
'uninstall' => UninstallCommand::class,
'self-update' => SelfUpdateCommand::class,
'install-extensions-for-project' => InstallExtensionsForProjectCommand::class,
]
));

Expand Down
33 changes: 33 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,39 @@ You can list the repositories for the target PHP installation with:

* `pie repository:list [--with-php-config=...]`

## Check and install missing extensions for your project

You can use `pie install` when in a PHP project working directory to check the
extensions the project requires are present. If an extension is missing, PIE
will try to find an installation candidate and interactively ask if you would
like to install one. For example:

```
$ pie install
🥧 PHP Installer for Extensions (PIE), 0.9.0, from The PHP Foundation
You are running PHP 8.3.19
Target PHP installation: 8.3.19 nts, on Linux/OSX/etc x86_64 (from /usr/bin/php8.3)
Checking extensions for your project your-vendor/your-project
requires: curl ✅ Already installed
requires: intl ✅ Already installed
requires: json ✅ Already installed
requires: example_pie_extension ⚠️ Missing

The following packages may be suitable, which would you like to install:
[0] None
[1] asgrim/example-pie-extension: Example PIE extension
> 1
> 🥧 PHP Installer for Extensions (PIE), 0.9.0, from The PHP Foundation
> This command may need elevated privileges, and may prompt you for your password.
> You are running PHP 8.3.19
> Target PHP installation: 8.3.19 nts, on Linux/OSX/etc x86_64 (from /usr/bin/php8.3)
> Found package: asgrim/example-pie-extension:2.0.2 which provides ext-example_pie_extension
... (snip) ...
> ✅ Extension is enabled and loaded in /usr/bin/php8.3

Finished checking extensions.
```

## Comparison with PECL

Since PIE is a replacement for PECL, here is a comparison of the commands that
Expand Down
16 changes: 8 additions & 8 deletions src/Command/CommandHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@
/** @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
{
private const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version';
private const OPTION_WITH_PHP_CONFIG = 'with-php-config';
private const OPTION_WITH_PHP_PATH = 'with-php-path';
private const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path';
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';
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 @@ -77,7 +77,7 @@ public static function configureDownloadBuildInstallOptions(Command $command): v
{
$command->addArgument(
self::ARG_REQUESTED_PACKAGE_AND_VERSION,
InputArgument::REQUIRED,
InputArgument::OPTIONAL,
'The PIE package name and version constraint to use, in the format {vendor/package}{?:{?version-constraint}{?@stability}}, for example `xdebug/xdebug:^3.4@alpha`, `xdebug/xdebug:@alpha`, `xdebug/xdebug:^3.4`, etc.',
);
$command->addOption(
Expand Down
31 changes: 30 additions & 1 deletion src/Command/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

use Webmozart\Assert\Assert;

use function array_combine;
use function array_filter;
use function array_keys;
use function array_map;
use function array_merge;
use function array_values;
use function sprintf;

#[AsCommand(
Expand All @@ -40,8 +48,29 @@ public function configure(): void
CommandHelper::configureDownloadBuildInstallOptions($this);
}

private function invokeInstallForProject(InputInterface $input, OutputInterface $output): int
{
$originalSuppliedOptions = array_filter($input->getOptions());
$installForProjectInput = new ArrayInput(array_merge(
['command' => 'install-extensions-for-project'],
array_combine(
array_map(static fn ($key) => '--' . $key, array_keys($originalSuppliedOptions)),
array_values($originalSuppliedOptions),
),
));

$application = $this->getApplication();
Assert::notNull($application);

return $application->doRun($installForProjectInput, $output);
}

public function execute(InputInterface $input, OutputInterface $output): int
{
if (! $input->getArgument(CommandHelper::ARG_REQUESTED_PACKAGE_AND_VERSION)) {
return $this->invokeInstallForProject($input, $output);
}

if (! TargetPlatform::isRunningAsRoot()) {
$output->writeln('This command may need elevated privileges, and may prompt you for your password.');
}
Expand Down
204 changes: 204 additions & 0 deletions src/Command/InstallExtensionsForProjectCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<?php

declare(strict_types=1);

namespace Php\Pie\Command;

use Composer\Package\Link;
use OutOfRangeException;
use Php\Pie\ComposerIntegration\PieComposerFactory;
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
use Php\Pie\Installing\InstallForPhpProject\FindRootPackage;
use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Throwable;

use function array_filter;
use function array_keys;
use function array_map;
use function array_merge;
use function array_walk;
use function assert;
use function getcwd;
use function in_array;
use function sprintf;
use function str_starts_with;
use function strlen;
use function strpos;
use function substr;

use const PHP_EOL;

#[AsCommand(
name: 'install-extensions-for-project',
description: 'Check a project for its extension dependencies, and offers to install them',
)]
final class InstallExtensionsForProjectCommand extends Command
{
public function __construct(
private readonly FindRootPackage $findRootPackage,
private readonly FindMatchingPackages $findMatchingPackages,
private readonly InstallSelectedPackage $installSelectedPackage,
private readonly ContainerInterface $container,
) {
parent::__construct();
}

public function configure(): void
{
parent::configure();

CommandHelper::configurePhpConfigOptions($this);
}

public function execute(InputInterface $input, OutputInterface $output): int
{
$helper = $this->getHelper('question');
assert($helper instanceof QuestionHelper);

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

$rootPackage = $this->findRootPackage->forCwd($input, $output);

if (ExtensionType::isValid($rootPackage->getType())) {
$output->writeln('<error>This composer.json is for an extension, installing missing packages is not supported.</error>');

return Command::INVALID;
}

$output->writeln(sprintf(
'Checking extensions for your project <info>%s</info> (path: %s)',
$rootPackage->getPrettyName(),
getcwd(),
));

$rootPackageExtensionsRequired = array_filter(
array_merge($rootPackage->getRequires(), $rootPackage->getDevRequires()),
static function (Link $link) {
$linkTarget = $link->getTarget();
if (! str_starts_with($linkTarget, 'ext-')) {
return false;
}

return ExtensionName::isValidExtensionName(substr($linkTarget, strlen('ext-')));
},
);

$pieComposer = PieComposerFactory::createPieComposer(
$this->container,
PieComposerRequest::noOperation(
new NullOutput(),
$targetPlatform,
),
);

$phpEnabledExtensions = array_keys($targetPlatform->phpBinaryPath->extensions());

$anyErrorsHappened = false;

array_walk(
$rootPackageExtensionsRequired,
function (Link $link) use ($pieComposer, $phpEnabledExtensions, $input, $output, $helper, &$anyErrorsHappened): void {
$extension = ExtensionName::normaliseFromString($link->getTarget());

if (in_array($extension->name(), $phpEnabledExtensions)) {
$output->writeln(sprintf(
'%s: <info>%s</info> ✅ Already installed',
$link->getDescription(),
$extension->name(),
));

return;
}

$output->writeln(sprintf(
'%s: <comment>%s</comment> ⚠️ Missing',
$link->getDescription(),
$extension->name(),
));

try {
$matches = $this->findMatchingPackages->for($pieComposer, $extension);
} catch (OutOfRangeException) {
$anyErrorsHappened = true;

$message = sprintf(
'<error>No packages were found for %s</error>',
$extension->nameWithExtPrefix(),
);

if ($output instanceof ConsoleOutputInterface) {
$output->getErrorOutput()->writeln($message);

return;
}

$output->writeln($message);

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,
),
),
0,
);

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

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

return;
}

try {
$this->installSelectedPackage->withPieCli(
substr($selectedPackageAnswer, 0, (int) strpos($selectedPackageAnswer, ':')),
$input,
$output,
);
} catch (Throwable $t) {
$anyErrorsHappened = true;

$message = '<error>' . $t->getMessage() . '</error>';

if ($output instanceof ConsoleOutputInterface) {
$output->getErrorOutput()->writeln($message);

return;
}

$output->writeln($message);
}
},
);

$output->writeln(PHP_EOL . 'Finished checking extensions.');

/**
* @psalm-suppress TypeDoesNotContainType
* @psalm-suppress RedundantCondition
*/
return $anyErrorsHappened ? self::FAILURE : self::SUCCESS;
}
}
2 changes: 2 additions & 0 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Php\Pie\Command\DownloadCommand;
use Php\Pie\Command\InfoCommand;
use Php\Pie\Command\InstallCommand;
use Php\Pie\Command\InstallExtensionsForProjectCommand;
use Php\Pie\Command\RepositoryAddCommand;
use Php\Pie\Command\RepositoryListCommand;
use Php\Pie\Command\RepositoryRemoveCommand;
Expand Down Expand Up @@ -58,6 +59,7 @@ public static function factory(): ContainerInterface
$container->singleton(RepositoryRemoveCommand::class);
$container->singleton(UninstallCommand::class);
$container->singleton(SelfUpdateCommand::class);
$container->singleton(InstallExtensionsForProjectCommand::class);

$container->singleton(QuieterConsoleIO::class, static function (ContainerInterface $container): QuieterConsoleIO {
return new QuieterConsoleIO(
Expand Down
Loading