Skip to content

Commit 538d0d7

Browse files
authored
Merge pull request #652 from asgrim/587-install-from-lock
587: add `pie install --from-lock`
2 parents 315a570 + 044ecd0 commit 538d0d7

23 files changed

Lines changed: 672 additions & 34 deletions

.github/workflows/continuous-integration.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ jobs:
160160
- '8.2'
161161
- '8.3'
162162
- '8.4'
163+
- '8.5'
163164
steps:
164165
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
165166
with:

docs/usage.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,25 @@ pie install \
459459
> The `--allow-non-interactive-project-install` will no longer work. You must
460460
> provide package selections from PIE 1.5 onwards.
461461
462+
## Install extensions from pie.lock
463+
464+
If you have an existing `pie.json` and `pie.lock` for a given PHP install,
465+
place these files in the directory indicated by the `pie show -v` path for
466+
`Using pie.json`, e.g.:
467+
468+
```bash
469+
$ php8.2 /usr/local/bin/pie show -v
470+
🥧 PHP Installer for Extensions (PIE) 1.5.0, from The PHP Foundation
471+
You are running PHP 8.2.31
472+
Target PHP installation: 8.2.31 nts, on Linux/OSX/etc x86_64 (from /usr/bin/php8.2)
473+
Using pie.json: /home/blah/.config/pie/php8.2_7cfa96d5dfc1df10afeb65851159197b/pie.json
474+
...
475+
```
476+
477+
Move your `pie.json` and `pie.lock` into this path, then you can run
478+
`pie install --from-lock` which will install the locked extension dependencies
479+
specified in that `pie.lock`.
480+
462481
## Comparison with PECL
463482

464483
Since PIE is a replacement for PECL, here is a comparison of the commands that

features/install-extensions.feature

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,9 @@ Feature: Extensions can be installed with PIE
1414
Example: Multiple extensions can be installed at once
1515
When I run a command to install multiple extensions
1616
Then the extensions should have been installed and enabled
17+
18+
# pie install --from-lock
19+
Example: I can install exact versions contained in the lockfile
20+
Given I have a lock file
21+
When I run a command to install from the lockfile
22+
Then the extensions should have been updated to the lock

phpunit.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
beStrictAboutOutputDuringTests="true"
1010
displayDetailsOnSkippedTests="true"
1111
displayDetailsOnTestsThatTriggerWarnings="true"
12+
displayDetailsOnPhpunitDeprecations="true"
1213
failOnRisky="true"
1314
failOnWarning="true">
1415
<php>

src/Command/CommandHelper.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ static function (RequestedPackageAndVersion $requestedNameAndVersion) use ($depe
395395
}
396396

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

431431
/**
432-
* @param non-empty-list<Package> $packages
432+
* @param list<Package> $packages
433433
*
434434
* @return array<string, list<non-empty-string>> Keyed by package name
435435
*/

src/Command/InstallCommand.php

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Php\Pie\Command;
66

77
use Composer\IO\IOInterface;
8+
use InvalidArgumentException;
89
use Php\Pie\ComposerIntegration\ComposerIntegrationHandler;
910
use Php\Pie\ComposerIntegration\ComposerRunFailed;
1011
use Php\Pie\ComposerIntegration\PieComposerFactory;
@@ -24,6 +25,7 @@
2425
use Symfony\Component\Console\Attribute\AsCommand;
2526
use Symfony\Component\Console\Command\Command;
2627
use Symfony\Component\Console\Input\InputInterface;
28+
use Symfony\Component\Console\Input\InputOption;
2729
use Symfony\Component\Console\Output\OutputInterface;
2830
use Throwable;
2931

@@ -33,6 +35,8 @@
3335
)]
3436
final class InstallCommand extends Command
3537
{
38+
public const OPTION_FROM_LOCK = 'from-lock';
39+
3640
public function __construct(
3741
private readonly ContainerInterface $container,
3842
private readonly DependencyResolver $dependencyResolver,
@@ -51,11 +55,31 @@ public function configure(): void
5155
parent::configure();
5256

5357
CommandHelper::configureDownloadBuildInstallOptions($this);
58+
59+
$this->addOption(
60+
self::OPTION_FROM_LOCK,
61+
null,
62+
InputOption::VALUE_NONE,
63+
'Install the exact versions specified in the pie.lock file for the target PHP.',
64+
);
65+
}
66+
67+
public static function shouldInstallFromLock(InputInterface $input): bool
68+
{
69+
return $input->hasOption(self::OPTION_FROM_LOCK) && (bool) $input->getOption(self::OPTION_FROM_LOCK);
5470
}
5571

5672
public function execute(InputInterface $input, OutputInterface $output): int
5773
{
58-
if (! $input->getArgument(CommandHelper::ARG_REQUESTED_PACKAGE_AND_VERSION)) {
74+
$installFromLock = self::shouldInstallFromLock($input);
75+
76+
if ($installFromLock && $input->getArgument(CommandHelper::ARG_REQUESTED_PACKAGE_AND_VERSION)) {
77+
$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>');
78+
79+
return self::INVALID;
80+
}
81+
82+
if (! $installFromLock && ! $input->getArgument(CommandHelper::ARG_REQUESTED_PACKAGE_AND_VERSION)) {
5983
return ($this->invokeSubCommand)(
6084
$this,
6185
['command' => 'install-extensions-for-project'],
@@ -78,6 +102,12 @@ public function execute(InputInterface $input, OutputInterface $output): int
78102
$targetPlatform,
79103
$this->container,
80104
);
105+
} catch (InvalidArgumentException $noPackagesRequested) {
106+
if (! $installFromLock) {
107+
throw $noPackagesRequested;
108+
}
109+
110+
$requestedNamesAndVersions = [];
81111
}
82112

83113
$forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input);
@@ -122,15 +152,18 @@ public function execute(InputInterface $input, OutputInterface $output): int
122152
}
123153
}
124154

155+
$resolvedPackages = [];
125156
try {
126-
$resolvedPackages = CommandHelper::resolveRequestedPackages(
127-
$this->dependencyResolver,
128-
$this->io,
129-
$composer,
130-
$targetPlatform,
131-
$requestedNamesAndVersions,
132-
$forceInstallPackageVersion,
133-
);
157+
if ($requestedNamesAndVersions !== []) {
158+
$resolvedPackages = CommandHelper::resolveRequestedPackages(
159+
$this->dependencyResolver,
160+
$this->io,
161+
$composer,
162+
$targetPlatform,
163+
$requestedNamesAndVersions,
164+
$forceInstallPackageVersion,
165+
);
166+
}
134167
} catch (UnableToResolveRequirement $unableToResolveRequirement) {
135168
return CommandHelper::handlePackageNotFound(
136169
$unableToResolveRequirement,
@@ -161,6 +194,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
161194
PieOperation::Install,
162195
$configureOptionsValues,
163196
CommandHelper::determineAttemptToSetupIniFile($input),
197+
installAllPackages: $installFromLock,
164198
),
165199
);
166200

@@ -171,6 +205,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
171205
$targetPlatform,
172206
$forceInstallPackageVersion,
173207
true,
208+
installFromLock: $installFromLock,
174209
);
175210
} catch (ComposerRunFailed $composerRunFailed) {
176211
$this->io->writeError('<error>' . $composerRunFailed->getMessage() . '</error>');

src/Command/ShowCommand.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Composer\IO\IOInterface;
88
use Composer\IO\NullIO;
9+
use InvalidArgumentException;
910
use Php\Pie\ComposerIntegration\PieComposerFactory;
1011
use Php\Pie\ComposerIntegration\PieComposerRequest;
1112
use Php\Pie\DependencyResolver\BundledPhpExtensionRefusal;
@@ -26,6 +27,7 @@
2627
use Webmozart\Assert\Assert;
2728

2829
use function array_diff;
30+
use function array_key_exists;
2931
use function array_map;
3032
use function array_walk;
3133
use function count;
@@ -115,7 +117,7 @@ function (string $version, string $phpExtensionName) use ($composer, $rootPackag
115117
foreach ($pieMatchesForExtension->packages() as $piePackage) {
116118
$packageName = $piePackage->name();
117119
$verificationStatus = $piePackage->verifyPackageStatus($targetPlatform);
118-
$packageRequirement = $rootPackageRequires[$packageName]->getPrettyConstraint();
120+
$packageRequirement = array_key_exists($packageName, $rootPackageRequires) ? $rootPackageRequires[$packageName]->getPrettyConstraint() : null;
119121

120122
if ($verificationStatus === PackageVerificationStatus::InstalledBinaryMetadataMissing) {
121123
continue;
@@ -145,7 +147,7 @@ function (string $version, string $phpExtensionName) use ($composer, $rootPackag
145147
new RequestedPackageAndVersion($packageName, '*'),
146148
false,
147149
);
148-
} catch (UnableToResolveRequirement | BundledPhpExtensionRefusal) {
150+
} catch (UnableToResolveRequirement | BundledPhpExtensionRefusal | InvalidArgumentException) {
149151
$latestConstrainedPackage = null;
150152
$latestPackage = null;
151153
}
@@ -163,6 +165,10 @@ function (string $version, string $phpExtensionName) use ($composer, $rootPackag
163165
$updateNotice .= sprintf(', latest version is %s', $latestPackage->piePackage->version());
164166
}
165167

168+
if (! array_key_exists($packageName, $rootPackageRequires)) {
169+
$verificationStatus = PackageVerificationStatus::InstalledButDoesNotExistInRequires;
170+
}
171+
166172
$this->io->write(sprintf(
167173
' <info>%s:%s</info> (from 🥧 <info>%s</info> %s)%s',
168174
$phpExtensionName,

src/ComposerIntegration/ComposerIntegrationHandler.php

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Psr\Container\ContainerInterface;
2020

2121
use function array_map;
22+
use function array_values;
2223
use function assert;
2324
use function file_exists;
2425
use function sprintf;
@@ -72,6 +73,7 @@ public function runInstall(
7273
TargetPlatform $targetPlatform,
7374
bool $forceInstallPackageVersion,
7475
bool $runCleanup,
76+
bool $installFromLock = false,
7577
): void {
7678
$pieComposerJson = Platform::getPieJsonFilename($targetPlatform);
7779
$pieJsonEditor = PieJsonEditor::fromTargetPlatform($targetPlatform);
@@ -90,7 +92,14 @@ public function runInstall(
9092
// Refresh the Composer instance so it re-reads the updated pie.json
9193
$composer = PieComposerFactory::recreatePieComposer($this->container, $composer);
9294

93-
foreach ($composer->getRepositoryManager()->getLocalRepository()->getPackages() as $localRepoPackage) {
95+
$resolvedPackageByName = [];
96+
foreach ($resolvedRequestedPackages as $resolvedPackageRequest) {
97+
$resolvedPackageByName[$resolvedPackageRequest->piePackage->composerPackage()->getName()] = $resolvedPackageRequest->piePackage;
98+
}
99+
100+
$localRepository = $composer->getRepositoryManager()->getLocalRepository();
101+
102+
foreach ($localRepository->getPackages() as $localRepoPackage) {
94103
$extName = ExtensionName::determineFromComposerPackage($localRepoPackage);
95104

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

112121
if ($status->isVerified() && ! $forceInstallPackageVersion) {
122+
if ($installFromLock) {
123+
$lockedRepo = $composer->getLocker()->getLockedRepository();
124+
$lockedPackage = $lockedRepo->findPackage($localRepoPackage->getName(), '*');
125+
126+
if ($lockedPackage === null) {
127+
// Not in locked repo; Composer will install it anyway
128+
continue;
129+
}
130+
131+
if ($lockedPackage->getVersion() !== $localRepoPackage->getVersion()) {
132+
$this->arrayCollectionIo->write(sprintf(
133+
'%s PIE package %s (%s) is at %s but the lock requires %s, scheduling for reinstall.',
134+
Emoji::WARNING,
135+
$localRepoPackage->getName(),
136+
$extName->name(),
137+
$localRepoPackage->getPrettyVersion(),
138+
$lockedPackage->getPrettyVersion(),
139+
));
140+
141+
// Locked, but the version we installed is different
142+
$localRepository->removePackage($localRepoPackage);
143+
continue;
144+
}
145+
} else {
146+
$resolvedPackage = $resolvedPackageByName[$localRepoPackage->getName()] ?? null;
147+
if ($resolvedPackage !== null && $resolvedPackage->composerPackage()->getVersion() !== $localRepoPackage->getVersion()) {
148+
$this->arrayCollectionIo->write(sprintf(
149+
'%s PIE package %s (%s) is at %s but %s was installed, adding to install candidates.',
150+
Emoji::WARNING,
151+
$localRepoPackage->getName(),
152+
$extName->name(),
153+
$localRepoPackage->getPrettyVersion(),
154+
$resolvedPackage->composerPackage()->getPrettyVersion(),
155+
));
156+
157+
// Resolved a different version than what's installed
158+
$localRepository->removePackage($localRepoPackage);
159+
continue;
160+
}
161+
}
162+
113163
$this->arrayCollectionIo->write(sprintf(
114164
'%s PIE package %s (%s) is already installed and verified.',
115165
Emoji::GREEN_CHECKMARK,
@@ -144,12 +194,16 @@ public function runInstall(
144194
$extName->name(),
145195
$status->description(),
146196
));
147-
$composer->getRepositoryManager()->getLocalRepository()->removePackage($localRepoPackage);
197+
$localRepository->removePackage($localRepoPackage);
148198
}
149199

200+
$extensionNames = $installFromLock
201+
? array_values(array_map(ExtensionName::determineFromComposerPackage(...), $composer->getLocker()->getLockedRepository()->getPackages()))
202+
: ResolvedPackageRequest::extensionNames($resolvedRequestedPackages);
203+
150204
$composerInstaller = PieComposerInstaller::createWithPhpBinary(
151205
$targetPlatform->phpBinaryPath,
152-
ResolvedPackageRequest::extensionNames($resolvedRequestedPackages),
206+
$extensionNames,
153207
$this->arrayCollectionIo,
154208
$composer,
155209
);
@@ -161,7 +215,7 @@ public function runInstall(
161215
->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($forceInstallPackageVersion))
162216
->setDownloadOnly(false);
163217

164-
if (file_exists(PieComposerFactory::getLockFile($pieComposerJson))) {
218+
if (! $installFromLock && file_exists(PieComposerFactory::getLockFile($pieComposerJson))) {
165219
$composerInstaller->setUpdate(true);
166220
$composerInstaller->setUpdateAllowList(ResolvedPackageRequest::requestedPackageNames($resolvedRequestedPackages));
167221
}

src/ComposerIntegration/PieComposerRequest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public function __construct(
3232
public readonly PieOperation $operation,
3333
public readonly array $configureOptions,
3434
public readonly bool $attemptToSetupIniFile,
35+
public readonly bool $installAllPackages = false,
3536
) {
3637
$this->requestedPackageNames = array_map(static fn (RequestedPackageAndVersion $request) => $request->package, $this->requestedPackages);
3738
}
@@ -62,6 +63,10 @@ public static function noOperation(
6263

6364
public function isFor(string $packageName): bool
6465
{
66+
if ($this->installAllPackages) {
67+
return true;
68+
}
69+
6570
return in_array($packageName, $this->requestedPackageNames);
6671
}
6772
}

src/ComposerIntegration/UninstallProcess.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ public function __invoke(
3636
$status = $piePackage->verifyPackageStatus($composerRequest->targetPlatform);
3737

3838
if ($status->isVerified()) {
39-
$io->write(sprintf('👋 <info>Removed extension:</info> %s', ($this->uninstall)($targetPlatform, $piePackage)->filePath));
39+
$io->write(sprintf(
40+
'👋 <info>Removed extension %s:</info> %s',
41+
$piePackage->prettyNameAndVersion(),
42+
($this->uninstall)($targetPlatform, $piePackage)->filePath,
43+
));
4044
} else {
4145
$io->writeError(sprintf('<warning>Did not remove extension file:</warning> %s', $status->description()));
4246
}

0 commit comments

Comments
 (0)