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
1 change: 1 addition & 0 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ jobs:
- '8.2'
- '8.3'
- '8.4'
- '8.5'
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
with:
Expand Down
6 changes: 6 additions & 0 deletions features/install-extensions.feature
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ Feature: Extensions can be installed with PIE
Example: Multiple extensions can be installed at once
When I run a command to install multiple extensions
Then the extensions should have been installed and enabled

# pie install --from-lock

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to review/update docs

Example: I can install exact versions contained in the lockfile
Given I have a lock file
When I run a command to install from the lockfile
Then the extensions should have been updated to the lock
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
beStrictAboutOutputDuringTests="true"
displayDetailsOnSkippedTests="true"
displayDetailsOnTestsThatTriggerWarnings="true"
displayDetailsOnPhpunitDeprecations="true"
failOnRisky="true"
failOnWarning="true">
<php>
Expand Down
4 changes: 2 additions & 2 deletions src/Command/CommandHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ static function (RequestedPackageAndVersion $requestedNameAndVersion) use ($depe
}

/**
* @param non-empty-list<Package> $packages
* @param list<Package> $packages
*
* @throws ConfigureOptionCollision if two of the requested packages declare a configure option with the same name.
*/
Expand Down Expand Up @@ -429,7 +429,7 @@ public static function bindConfigureOptionsFromPackage(Command $command, array $
}

/**
* @param non-empty-list<Package> $packages
* @param list<Package> $packages
*
* @return array<string, list<non-empty-string>> Keyed by package name
*/
Expand Down
53 changes: 44 additions & 9 deletions src/Command/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Php\Pie\Command;

use Composer\IO\IOInterface;
use InvalidArgumentException;
use Php\Pie\ComposerIntegration\ComposerIntegrationHandler;
use Php\Pie\ComposerIntegration\ComposerRunFailed;
use Php\Pie\ComposerIntegration\PieComposerFactory;
Expand All @@ -24,6 +25,7 @@
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;

Expand All @@ -33,6 +35,8 @@
)]
final class InstallCommand extends Command
{
public const OPTION_FROM_LOCK = 'from-lock';

public function __construct(
private readonly ContainerInterface $container,
private readonly DependencyResolver $dependencyResolver,
Expand All @@ -51,11 +55,31 @@ public function configure(): void
parent::configure();

CommandHelper::configureDownloadBuildInstallOptions($this);

$this->addOption(
self::OPTION_FROM_LOCK,
null,
InputOption::VALUE_NONE,
'Install the exact versions specified in the pie.lock file for the target PHP.',
);
}

public static function shouldInstallFromLock(InputInterface $input): bool
{
return $input->hasOption(self::OPTION_FROM_LOCK) && (bool) $input->getOption(self::OPTION_FROM_LOCK);
}

public function execute(InputInterface $input, OutputInterface $output): int
{
if (! $input->getArgument(CommandHelper::ARG_REQUESTED_PACKAGE_AND_VERSION)) {
$installFromLock = self::shouldInstallFromLock($input);

if ($installFromLock && $input->getArgument(CommandHelper::ARG_REQUESTED_PACKAGE_AND_VERSION)) {
$this->io->writeError('<error>The --from-lock option installs all extensions from the lock file and cannot be combined with a specific package argument.</error>');

return self::INVALID;
}

if (! $installFromLock && ! $input->getArgument(CommandHelper::ARG_REQUESTED_PACKAGE_AND_VERSION)) {
return ($this->invokeSubCommand)(
$this,
['command' => 'install-extensions-for-project'],
Expand All @@ -78,6 +102,12 @@ public function execute(InputInterface $input, OutputInterface $output): int
$targetPlatform,
$this->container,
);
} catch (InvalidArgumentException $noPackagesRequested) {
if (! $installFromLock) {
throw $noPackagesRequested;
}

$requestedNamesAndVersions = [];
}

$forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input);
Expand Down Expand Up @@ -122,15 +152,18 @@ public function execute(InputInterface $input, OutputInterface $output): int
}
}

$resolvedPackages = [];
try {
$resolvedPackages = CommandHelper::resolveRequestedPackages(
$this->dependencyResolver,
$this->io,
$composer,
$targetPlatform,
$requestedNamesAndVersions,
$forceInstallPackageVersion,
);
if ($requestedNamesAndVersions !== []) {
$resolvedPackages = CommandHelper::resolveRequestedPackages(
$this->dependencyResolver,
$this->io,
$composer,
$targetPlatform,
$requestedNamesAndVersions,
$forceInstallPackageVersion,
);
}
} catch (UnableToResolveRequirement $unableToResolveRequirement) {
return CommandHelper::handlePackageNotFound(
$unableToResolveRequirement,
Expand Down Expand Up @@ -161,6 +194,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
PieOperation::Install,
$configureOptionsValues,
CommandHelper::determineAttemptToSetupIniFile($input),
installAllPackages: $installFromLock,
),
);

Expand All @@ -171,6 +205,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
$targetPlatform,
$forceInstallPackageVersion,
true,
installFromLock: $installFromLock,
);
} catch (ComposerRunFailed $composerRunFailed) {
$this->io->writeError('<error>' . $composerRunFailed->getMessage() . '</error>');
Expand Down
10 changes: 8 additions & 2 deletions src/Command/ShowCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Composer\IO\IOInterface;
use Composer\IO\NullIO;
use InvalidArgumentException;
use Php\Pie\ComposerIntegration\PieComposerFactory;
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\DependencyResolver\BundledPhpExtensionRefusal;
Expand All @@ -26,6 +27,7 @@
use Webmozart\Assert\Assert;

use function array_diff;
use function array_key_exists;
use function array_map;
use function array_walk;
use function count;
Expand Down Expand Up @@ -115,7 +117,7 @@ function (string $version, string $phpExtensionName) use ($composer, $rootPackag
foreach ($pieMatchesForExtension->packages() as $piePackage) {
$packageName = $piePackage->name();
$verificationStatus = $piePackage->verifyPackageStatus($targetPlatform);
$packageRequirement = $rootPackageRequires[$packageName]->getPrettyConstraint();
$packageRequirement = array_key_exists($packageName, $rootPackageRequires) ? $rootPackageRequires[$packageName]->getPrettyConstraint() : null;

if ($verificationStatus === PackageVerificationStatus::InstalledBinaryMetadataMissing) {
continue;
Expand Down Expand Up @@ -145,7 +147,7 @@ function (string $version, string $phpExtensionName) use ($composer, $rootPackag
new RequestedPackageAndVersion($packageName, '*'),
false,
);
} catch (UnableToResolveRequirement | BundledPhpExtensionRefusal) {
} catch (UnableToResolveRequirement | BundledPhpExtensionRefusal | InvalidArgumentException) {
$latestConstrainedPackage = null;
$latestPackage = null;
}
Expand All @@ -163,6 +165,10 @@ function (string $version, string $phpExtensionName) use ($composer, $rootPackag
$updateNotice .= sprintf(', latest version is %s', $latestPackage->piePackage->version());
}

if (! array_key_exists($packageName, $rootPackageRequires)) {
$verificationStatus = PackageVerificationStatus::InstalledButDoesNotExistInRequires;
}

$this->io->write(sprintf(
' <info>%s:%s</info> (from 🥧 <info>%s</info> %s)%s',
$phpExtensionName,
Expand Down
62 changes: 58 additions & 4 deletions src/ComposerIntegration/ComposerIntegrationHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Psr\Container\ContainerInterface;

use function array_map;
use function array_values;
use function assert;
use function file_exists;
use function sprintf;
Expand Down Expand Up @@ -72,6 +73,7 @@ public function runInstall(
TargetPlatform $targetPlatform,
bool $forceInstallPackageVersion,
bool $runCleanup,
bool $installFromLock = false,
): void {
$pieComposerJson = Platform::getPieJsonFilename($targetPlatform);
$pieJsonEditor = PieJsonEditor::fromTargetPlatform($targetPlatform);
Expand All @@ -90,7 +92,14 @@ public function runInstall(
// Refresh the Composer instance so it re-reads the updated pie.json
$composer = PieComposerFactory::recreatePieComposer($this->container, $composer);

foreach ($composer->getRepositoryManager()->getLocalRepository()->getPackages() as $localRepoPackage) {
$resolvedPackageByName = [];
foreach ($resolvedRequestedPackages as $resolvedPackageRequest) {
$resolvedPackageByName[$resolvedPackageRequest->piePackage->composerPackage()->getName()] = $resolvedPackageRequest->piePackage;
}

$localRepository = $composer->getRepositoryManager()->getLocalRepository();

foreach ($localRepository->getPackages() as $localRepoPackage) {
$extName = ExtensionName::determineFromComposerPackage($localRepoPackage);

if ($localRepoPackage instanceof CompleteAliasPackage) {
Expand All @@ -110,6 +119,47 @@ public function runInstall(
), verbosity: IOInterface::VERY_VERBOSE);

if ($status->isVerified() && ! $forceInstallPackageVersion) {
if ($installFromLock) {
$lockedRepo = $composer->getLocker()->getLockedRepository();
$lockedPackage = $lockedRepo->findPackage($localRepoPackage->getName(), '*');

if ($lockedPackage === null) {
// Not in locked repo; Composer will install it anyway
continue;
}

if ($lockedPackage->getVersion() !== $localRepoPackage->getVersion()) {
$this->arrayCollectionIo->write(sprintf(
'%s PIE package %s (%s) is at %s but the lock requires %s, scheduling for reinstall.',
Emoji::WARNING,
$localRepoPackage->getName(),
$extName->name(),
$localRepoPackage->getPrettyVersion(),
$lockedPackage->getPrettyVersion(),
));

// Locked, but the version we installed is different
$localRepository->removePackage($localRepoPackage);
continue;
}
} else {
$resolvedPackage = $resolvedPackageByName[$localRepoPackage->getName()] ?? null;
if ($resolvedPackage !== null && $resolvedPackage->composerPackage()->getVersion() !== $localRepoPackage->getVersion()) {
$this->arrayCollectionIo->write(sprintf(
'%s PIE package %s (%s) is at %s but %s was installed, adding to install candidates.',
Emoji::WARNING,
$localRepoPackage->getName(),
$extName->name(),
$localRepoPackage->getPrettyVersion(),
$resolvedPackage->composerPackage()->getPrettyVersion(),
));

// Resolved a different version than what's installed
$localRepository->removePackage($localRepoPackage);
continue;
}
}

$this->arrayCollectionIo->write(sprintf(
'%s PIE package %s (%s) is already installed and verified.',
Emoji::GREEN_CHECKMARK,
Expand Down Expand Up @@ -144,12 +194,16 @@ public function runInstall(
$extName->name(),
$status->description(),
));
$composer->getRepositoryManager()->getLocalRepository()->removePackage($localRepoPackage);
$localRepository->removePackage($localRepoPackage);
}

$extensionNames = $installFromLock
? array_values(array_map(ExtensionName::determineFromComposerPackage(...), $composer->getLocker()->getLockedRepository()->getPackages()))
: ResolvedPackageRequest::extensionNames($resolvedRequestedPackages);

$composerInstaller = PieComposerInstaller::createWithPhpBinary(
$targetPlatform->phpBinaryPath,
ResolvedPackageRequest::extensionNames($resolvedRequestedPackages),
$extensionNames,
$this->arrayCollectionIo,
$composer,
);
Expand All @@ -161,7 +215,7 @@ public function runInstall(
->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($forceInstallPackageVersion))
->setDownloadOnly(false);

if (file_exists(PieComposerFactory::getLockFile($pieComposerJson))) {
if (! $installFromLock && file_exists(PieComposerFactory::getLockFile($pieComposerJson))) {
$composerInstaller->setUpdate(true);
$composerInstaller->setUpdateAllowList(ResolvedPackageRequest::requestedPackageNames($resolvedRequestedPackages));
}
Expand Down
5 changes: 5 additions & 0 deletions src/ComposerIntegration/PieComposerRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public function __construct(
public readonly PieOperation $operation,
public readonly array $configureOptions,
public readonly bool $attemptToSetupIniFile,
public readonly bool $installAllPackages = false,
) {
$this->requestedPackageNames = array_map(static fn (RequestedPackageAndVersion $request) => $request->package, $this->requestedPackages);
}
Expand Down Expand Up @@ -62,6 +63,10 @@ public static function noOperation(

public function isFor(string $packageName): bool
{
if ($this->installAllPackages) {
return true;
}

return in_array($packageName, $this->requestedPackageNames);
}
}
6 changes: 5 additions & 1 deletion src/ComposerIntegration/UninstallProcess.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ public function __invoke(
$status = $piePackage->verifyPackageStatus($composerRequest->targetPlatform);

if ($status->isVerified()) {
$io->write(sprintf('👋 <info>Removed extension:</info> %s', ($this->uninstall)($targetPlatform, $piePackage)->filePath));
$io->write(sprintf(
'👋 <info>Removed extension %s:</info> %s',
$piePackage->prettyNameAndVersion(),
($this->uninstall)($targetPlatform, $piePackage)->filePath,
));
} else {
$io->writeError(sprintf('<warning>Did not remove extension file:</warning> %s', $status->description()));
}
Expand Down
6 changes: 3 additions & 3 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ static function (): SystemDependenciesDefinition {
return $container;
}

public static function testFactory(): ContainerInterface
public static function testFactory(OutputInterface $output = new NullOutput()): ContainerInterface
{
self::$testBuffer ??= new BufferIO();

Expand All @@ -243,10 +243,10 @@ public static function testFactory(): ContainerInterface
// QuieterConsoleIO is wired separately from IOInterface in self::factory(), and writes
// directly to a real ConsoleOutput; override it here only, so tests don't leak its output
// to the terminal.
$container->singleton(QuieterConsoleIO::class, static function (ContainerInterface $container): QuieterConsoleIO {
$container->singleton(QuieterConsoleIO::class, static function (ContainerInterface $container) use ($output): QuieterConsoleIO {
return new QuieterConsoleIO(
$container->get(InputInterface::class),
new NullOutput(),
$output,
new MinimalHelperSet(
[
'question' => new QuestionHelper(),
Expand Down
4 changes: 2 additions & 2 deletions src/DependencyResolver/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,10 @@ public function installedJsonMetadata(): InstalledJsonMetadata
public function verifyPackageStatus(TargetPlatform $targetPlatform): PackageVerificationStatus
{
$extensionPath = $targetPlatform->phpBinaryPath->extensionPath();
$extensionEnding = $targetPlatform->operatingSystem === OperatingSystem::Windows ? '.dll' : '.so';
$isWindows = $targetPlatform->operatingSystem === OperatingSystem::Windows;
$phpExtensionName = $this->extensionName->name();

$actualBinaryPathByConvention = $extensionPath . DIRECTORY_SEPARATOR . $phpExtensionName . $extensionEnding;
$actualBinaryPathByConvention = $extensionPath . DIRECTORY_SEPARATOR . ($isWindows ? 'php_' : '') . $phpExtensionName . ($isWindows ? '.dll' : '.so');

// The extension may not be in the usual path (since you can specify a full path to an extension in the INI file)
if (! file_exists($actualBinaryPathByConvention)) {
Expand Down
4 changes: 2 additions & 2 deletions src/DependencyResolver/ResolvedPackageRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ public static function requestedPackageNames(array $resolvedPackageRequests): ar
}

/**
* @param non-empty-list<self> $resolvedPackageRequests
* @param list<self> $resolvedPackageRequests
*
* @return non-empty-list<Package>
* @return list<Package>
*/
public static function piePackages(array $resolvedPackageRequests): array
{
Expand Down
Loading
Loading