Skip to content
Open
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
2 changes: 2 additions & 0 deletions bin/pie
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use Php\Pie\Command\SelfUpdateCommand;
use Php\Pie\Command\SelfVerifyCommand;
use Php\Pie\Command\ShowCommand;
use Php\Pie\Command\UninstallCommand;
use Php\Pie\Command\UpgradeCommand;
use Php\Pie\Util\PieVersion;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
Expand Down Expand Up @@ -48,6 +49,7 @@ $application->setCommandLoader(new ContainerCommandLoader(
'download' => DownloadCommand::class,
'build' => BuildCommand::class,
'install' => InstallCommand::class,
'upgrade' => UpgradeCommand::class,
'info' => InfoCommand::class,
'show' => ShowCommand::class,
'repository:list' => RepositoryListCommand::class,
Expand Down
13 changes: 13 additions & 0 deletions features/upgrade-extensions.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Feature: PIE extensions can be upgraded with PIE

# pie upgrade
Example: I can upgrade existing PIE extensions
Given I have installed PIE extensions that have upgrades available
When I run a command to upgrade my extensions
Then the extensions should have been upgraded to the latest versions

# pie upgrade
Example: I can upgrade existing PIE extensions that had been built with configure options
Given I have installed PIE extensions with configure options that have upgrades available
When I run a command to upgrade my extensions
Then the extension has been upgraded with the previous configure options
124 changes: 124 additions & 0 deletions src/Command/UpgradeCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

declare(strict_types=1);

namespace Php\Pie\Command;

use Composer\IO\IOInterface;
use Php\Pie\ComposerIntegration\ComposerIntegrationHandler;
use Php\Pie\ComposerIntegration\ComposerRunFailed;
use Php\Pie\ComposerIntegration\PieComposerFactory;
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ComposerIntegration\PieOperation;
use Php\Pie\Platform;
use Php\Pie\Platform\InstalledPiePackages;
use Php\Pie\Platform\PackageManager;
use Php\Pie\Platform\TargetPlatform;
use Php\Pie\SelfManage\BuildTools\CheckAllBuildTools;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Webmozart\Assert\Assert;

use function explode;
use function file_exists;

#[AsCommand(
name: 'upgrade',
description: 'Upgrade all PIE-installed extensions to their latest allowed versions.',
)]
final class UpgradeCommand extends Command
{
public function __construct(
private readonly ContainerInterface $container,
private readonly ComposerIntegrationHandler $composerIntegrationHandler,
private readonly IOInterface $io,
private readonly CheckAllBuildTools $checkBuildTools,
private readonly InstalledPiePackages $installedPiePackages,
) {
parent::__construct();
}

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

CommandHelper::configureDownloadBuildInstallOptions($this, false);
}

public function execute(InputInterface $input, OutputInterface $output): int
{
if (! TargetPlatform::isRunningAsRoot()) {
$this->io->write('This command may need elevated privileges, and may prompt you for your password.');
}

$targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io);

$forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input);
CommandHelper::applyNoCacheOptionIfSet($input, $this->io);

if (CommandHelper::shouldCheckForBuildTools($input)) {
$this->checkBuildTools->check(
$this->io,
PackageManager::detect(),
$targetPlatform,
CommandHelper::autoInstallBuildTools($input),
);
}

if (! file_exists(PieComposerFactory::getLockFile(Platform::getPieJsonFilename($targetPlatform)))) {
$this->io->writeError('<error>No PIE extensions are currently installed, so there is nothing to upgrade.</error>');

return Command::INVALID;
}

$existingComposer = PieComposerFactory::createPieComposer(
$this->container,
PieComposerRequest::noOperation($this->io, $targetPlatform),
);

$configureOptions = [];
foreach ($this->installedPiePackages->allPiePackages($existingComposer)->packages() as $installedPackage) {
$existingConfigureOptions = $installedPackage->installedJsonMetadata()->configureOptions();
if ($existingConfigureOptions === null) {
continue;
}

$existingConfigureOptionsList = explode(' ', $existingConfigureOptions);
Assert::allStringNotEmpty($existingConfigureOptionsList);

$configureOptions[$installedPackage->name()] = $existingConfigureOptionsList;
}

$composer = PieComposerFactory::createPieComposer(
$this->container,
new PieComposerRequest(
$this->io,
$targetPlatform,
[],
PieOperation::Install,
$configureOptions,
CommandHelper::determineAttemptToSetupIniFile($input),
installAllPackages: true,
),
);

try {
$this->composerIntegrationHandler->runInstall(
[],
$composer,
$targetPlatform,
$forceInstallPackageVersion,
true,
);
} catch (ComposerRunFailed $composerRunFailed) {
$this->io->writeError('<error>' . $composerRunFailed->getMessage() . '</error>');

return $composerRunFailed->getCode();
}

return Command::SUCCESS;
}
}
4 changes: 3 additions & 1 deletion src/ComposerIntegration/ComposerIntegrationHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ public function runInstall(
$localRepository->removePackage($localRepoPackage);
}

$extensionNames = $installFromLock
// When no specific packages were resolved (e.g. `--from-lock`, or `upgrade`), treat every
// package tracked in the lock file as being installed/updated by this operation.
$extensionNames = $resolvedRequestedPackages === []
? array_values(array_map(ExtensionName::determineFromComposerPackage(...), $composer->getLocker()->getLockedRepository()->getPackages()))
: ResolvedPackageRequest::extensionNames($resolvedRequestedPackages);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Composer\Composer;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\Installer\InstallerEvent;
use Composer\Installer\InstallerEvents;
use Composer\IO\IOInterface;
Expand Down Expand Up @@ -56,11 +57,11 @@ public function __invoke(InstallerEvent $installerEvent): void
array_walk(
$operations,
function (OperationInterface $operation): void {
if (! $operation instanceof InstallOperation) {
if (! $operation instanceof InstallOperation && ! $operation instanceof UpdateOperation) {
return;
}

$composerPackage = $operation->getPackage();
$composerPackage = $operation instanceof UpdateOperation ? $operation->getTargetPackage() : $operation->getPackage();
if (! $composerPackage instanceof CompletePackageInterface) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Transaction;
use Composer\Installer\InstallerEvent;
use Composer\Installer\InstallerEvents;
Expand Down Expand Up @@ -46,7 +47,7 @@ public function __invoke(InstallerEvent $installerEvent): void
$newOperations = array_filter(
$installerEvent->getTransaction()?->getOperations() ?? [],
function (OperationInterface $operation) use ($pieOutput): bool {
if (! $operation instanceof InstallOperation && ! $operation instanceof UninstallOperation) {
if (! $operation instanceof InstallOperation && ! $operation instanceof UninstallOperation && ! $operation instanceof UpdateOperation) {
$pieOutput->writeError(
sprintf(
'Unexpected operation during installer: %s',
Expand All @@ -58,13 +59,15 @@ function (OperationInterface $operation) use ($pieOutput): bool {
return false;
}

$isRequestedPiePackage = $this->composerRequest->isFor($operation->getPackage()->getName());
$operationPackageName = $operation instanceof UpdateOperation ? $operation->getTargetPackage()->getName() : $operation->getPackage()->getName();

$isRequestedPiePackage = $this->composerRequest->isFor($operationPackageName);

if (! $isRequestedPiePackage) {
$pieOutput->writeError(
sprintf(
'Filtering package %s from install operations, as it was not the requested package',
$operation->getPackage()->getName(),
$operationPackageName,
),
verbosity: IOInterface::VERY_VERBOSE,
);
Expand Down
129 changes: 73 additions & 56 deletions src/ComposerIntegration/PiePackageInstaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,40 +39,38 @@ public function install(InstalledRepositoryInterface $repo, PackageInterface $pa
$composerPackage = $package;

return parent::install($repo, $composerPackage)
?->then(function () use ($composerPackage) {
$io = $this->composerRequest->pieOutput;

if (! $this->composerRequest->isFor($composerPackage->getName())) {
$io->write(
sprintf(
'<comment>Skipping %s install request from Composer as it was not the expected PIE package(s) %s</comment>',
$composerPackage->getName(),
implode(', ', array_map(static fn (RequestedPackageAndVersion $req) => $req->package, $this->composerRequest->requestedPackages)),
),
verbosity: IOInterface::VERY_VERBOSE,
?->then($this->onlyForRequestedPiePackage(
$composerPackage,
'install',
function (CompletePackageInterface $composerPackage): void {
($this->installAndBuildProcess)(
$this->composer,
$this->composerRequest,
$composerPackage,
$this->getInstallPath($composerPackage),
);
},
));
}

return null;
}

if (! $composerPackage instanceof CompletePackageInterface) {
$io->writeError(sprintf(
'<error>Not using PIE to install %s as it was not a Complete Package</error>',
$composerPackage->getName(),
));

return null;
}

($this->installAndBuildProcess)(
$this->composer,
$this->composerRequest,
$composerPackage,
$this->getInstallPath($composerPackage),
);

return null;
});
/** @inheritDoc */
public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target)
{
$composerPackage = $target;

return parent::update($repo, $initial, $target)
?->then($this->onlyForRequestedPiePackage(
$composerPackage,
'update',
function (CompletePackageInterface $composerPackage): void {
($this->installAndBuildProcess)(
$this->composer,
$this->composerRequest,
$composerPackage,
$this->getInstallPath($composerPackage),
);
},
));
}

/** @inheritDoc */
Expand All @@ -81,37 +79,56 @@ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $
$composerPackage = $package;

return parent::uninstall($repo, $composerPackage)
?->then(function () use ($composerPackage) {
$io = $this->composerRequest->pieOutput;

if (! $this->composerRequest->isFor($composerPackage->getName())) {
$io->write(
sprintf(
'<comment>Skipping %s uninstall request from Composer as it was not the expected PIE package(s) %s</comment>',
$composerPackage->getName(),
implode(', ', array_map(static fn (RequestedPackageAndVersion $req) => $req->package, $this->composerRequest->requestedPackages)),
),
verbosity: IOInterface::VERY_VERBOSE,
?->then($this->onlyForRequestedPiePackage(
$composerPackage,
'uninstall',
function (CompletePackageInterface $composerPackage): void {
($this->uninstallProcess)(
$this->composerRequest,
$composerPackage,
);
},
));
}

return null;
}
/**
* Wraps the given callback so it only runs when Composer's operation is for a PIE package we actually
* requested, and that package has full metadata available.
*
* @return callable(): null
*/
private function onlyForRequestedPiePackage(PackageInterface $composerPackage, string $verb, callable $onVerified): callable
{
return function () use ($composerPackage, $verb, $onVerified) {
$io = $this->composerRequest->pieOutput;

if (! $composerPackage instanceof CompletePackageInterface) {
$io->writeError(sprintf(
'<error>Not using PIE to install %s as it was not a Complete Package</error>',
if (! $this->composerRequest->isFor($composerPackage->getName())) {
$io->write(
sprintf(
'<comment>Skipping %s %s request from Composer as it was not the expected PIE package(s) %s</comment>',
$composerPackage->getName(),
));
$verb,
implode(', ', array_map(static fn (RequestedPackageAndVersion $req) => $req->package, $this->composerRequest->requestedPackages)),
),
verbosity: IOInterface::VERY_VERBOSE,
);

return null;
}
return null;
}

($this->uninstallProcess)(
$this->composerRequest,
$composerPackage,
);
if (! $composerPackage instanceof CompletePackageInterface) {
$io->writeError(sprintf(
'<error>Not using PIE to %s %s as it was not a Complete Package</error>',
$verb,
$composerPackage->getName(),
));

return null;
});
}

$onVerified($composerPackage);

return null;
};
}
}
2 changes: 2 additions & 0 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Php\Pie\Command\SelfVerifyCommand;
use Php\Pie\Command\ShowCommand;
use Php\Pie\Command\UninstallCommand;
use Php\Pie\Command\UpgradeCommand;
use Php\Pie\ComposerIntegration\MinimalHelperSet;
use Php\Pie\ComposerIntegration\QuieterConsoleIO;
use Php\Pie\DependencyResolver\DependencyInstaller\SystemDependenciesDefinition;
Expand Down Expand Up @@ -114,6 +115,7 @@ static function (ConsoleCommandEvent $event) use (&$displayedBanner, $io): void
$container->singleton(DownloadCommand::class);
$container->singleton(BuildCommand::class);
$container->singleton(InstallCommand::class);
$container->singleton(UpgradeCommand::class);
$container->singleton(InfoCommand::class);
$container->singleton(ShowCommand::class);
$container->singleton(RepositoryListCommand::class);
Expand Down
6 changes: 6 additions & 0 deletions test/assets/pie-upgrade-lock/pie.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"require": {
"xdebug/xdebug": "^3.5",
"asgrim/example-pie-extension": "^2.0"
}
}
Loading
Loading