diff --git a/src/Command/InstallExtensionsForProjectCommand.php b/src/Command/InstallExtensionsForProjectCommand.php
index c5746a35..ebfe2b9d 100644
--- a/src/Command/InstallExtensionsForProjectCommand.php
+++ b/src/Command/InstallExtensionsForProjectCommand.php
@@ -5,6 +5,7 @@
namespace Php\Pie\Command;
use Composer\Package\Link;
+use Composer\Package\Version\VersionParser;
use OutOfRangeException;
use Php\Pie\ComposerIntegration\PieComposerFactory;
use Php\Pie\ComposerIntegration\PieComposerRequest;
@@ -16,6 +17,8 @@
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
use Php\Pie\Installing\InstallForPhpProject\InstallPiePackageFromPath;
use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage;
+use Php\Pie\Platform\InstalledPiePackages;
+use Php\Pie\Util\Emoji;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -53,6 +56,7 @@ final class InstallExtensionsForProjectCommand extends Command
public function __construct(
private readonly ComposerFactoryForProject $composerFactoryForProject,
private readonly DetermineExtensionsRequired $determineExtensionsRequired,
+ private readonly InstalledPiePackages $installedPiePackages,
private readonly FindMatchingPackages $findMatchingPackages,
private readonly InstallSelectedPackage $installSelectedPackage,
private readonly InstallPiePackageFromPath $installPiePackageFromPath,
@@ -138,28 +142,62 @@ public function execute(InputInterface $input, OutputInterface $output): int
);
$phpEnabledExtensions = array_keys($targetPlatform->phpBinaryPath->extensions());
+ $installedPiePackages = $this->installedPiePackages->allPiePackages($pieComposer);
$anyErrorsHappened = false;
array_walk(
$extensionsRequired,
- function (Link $link) use ($pieComposer, $phpEnabledExtensions, $input, $output, $helper, &$anyErrorsHappened): void {
- $extension = ExtensionName::normaliseFromString($link->getTarget());
+ function (Link $link) use ($pieComposer, $phpEnabledExtensions, $installedPiePackages, $input, $output, $helper, &$anyErrorsHappened): void {
+ $extension = ExtensionName::normaliseFromString($link->getTarget());
+ $linkRequiresConstraint = $link->getPrettyConstraint();
+
+ $piePackageVersion = null;
+ if (in_array($extension->name(), array_keys($installedPiePackages))) {
+ $piePackageVersion = $installedPiePackages[$extension->name()]->version();
+ }
+
+ $piePackageVersionMatchesLinkConstraint = null;
+ if ($piePackageVersion !== null) {
+ $piePackageVersionMatchesLinkConstraint = $link
+ ->getConstraint()
+ ->matches(
+ (new VersionParser())->parseConstraints($piePackageVersion),
+ );
+ }
if (in_array($extension->name(), $phpEnabledExtensions)) {
+ if ($piePackageVersion !== null && $piePackageVersionMatchesLinkConstraint === false) {
+ $output->writeln(sprintf(
+ '%s: %s:%s %s Version %s is installed, but does not meet the version requirement %s',
+ $link->getDescription(),
+ $link->getTarget(),
+ $linkRequiresConstraint,
+ Emoji::WARNING,
+ $piePackageVersion,
+ $link->getConstraint()->getPrettyString(),
+ ));
+
+ return;
+ }
+
$output->writeln(sprintf(
- '%s: %s ✅ Already installed',
+ '%s: %s:%s %s Already installed',
$link->getDescription(),
- $link,
+ $link->getTarget(),
+ $linkRequiresConstraint,
+ Emoji::GREEN_CHECKMARK,
));
return;
}
$output->writeln(sprintf(
- '%s: %s ⚠️ Missing',
+ '%s: %s:%s %s Missing',
$link->getDescription(),
- $link,
+ $link->getTarget(),
+ $linkRequiresConstraint,
+ Emoji::PROHIBITED,
));
try {
@@ -205,9 +243,14 @@ static function (array $match): string {
return;
}
+ $requestInstallConstraint = '';
+ if ($linkRequiresConstraint !== '*') {
+ $requestInstallConstraint = ':' . $linkRequiresConstraint;
+ }
+
try {
$this->installSelectedPackage->withPieCli(
- substr($selectedPackageAnswer, 0, (int) strpos($selectedPackageAnswer, ':')),
+ substr($selectedPackageAnswer, 0, (int) strpos($selectedPackageAnswer, ':')) . $requestInstallConstraint,
$input,
$output,
);
diff --git a/src/Command/SelfUpdateCommand.php b/src/Command/SelfUpdateCommand.php
index 60676cb5..ea9a1529 100644
--- a/src/Command/SelfUpdateCommand.php
+++ b/src/Command/SelfUpdateCommand.php
@@ -16,6 +16,7 @@
use Php\Pie\SelfManage\Update\ReleaseMetadata;
use Php\Pie\SelfManage\Verify\FailedToVerifyRelease;
use Php\Pie\SelfManage\Verify\VerifyPieReleaseUsingAttestation;
+use Php\Pie\Util\Emoji;
use Php\Pie\Util\PieVersion;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
@@ -42,6 +43,7 @@ public function __construct(
private readonly string $githubApiBaseUrl,
private readonly QuieterConsoleIO $io,
private readonly ContainerInterface $container,
+ private readonly FullPathToSelf $fullPathToSelf,
) {
parent::__construct();
}
@@ -138,7 +140,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
return Command::FAILURE;
}
- $fullPathToSelf = (new FullPathToSelf())();
+ $fullPathToSelf = ($this->fullPathToSelf)();
$output->writeln(
sprintf('Writing new version to %s', $fullPathToSelf),
OutputInterface::VERBOSITY_VERBOSE,
@@ -146,7 +148,11 @@ public function execute(InputInterface $input, OutputInterface $output): int
SudoFilePut::contents($fullPathToSelf, file_get_contents($pharFilename->filePath));
unlink($pharFilename->filePath);
- $output->writeln('✅ PIE has been upgraded to ' . $latestRelease->tag . '');
+ $output->writeln(sprintf(
+ '%s PIE has been upgraded to %s',
+ Emoji::GREEN_CHECKMARK,
+ $latestRelease->tag,
+ ));
$this->exitSuccessfully();
}
diff --git a/src/Command/SelfVerifyCommand.php b/src/Command/SelfVerifyCommand.php
index 2978eddb..b5fd3dad 100644
--- a/src/Command/SelfVerifyCommand.php
+++ b/src/Command/SelfVerifyCommand.php
@@ -14,6 +14,7 @@
use Php\Pie\SelfManage\Update\ReleaseMetadata;
use Php\Pie\SelfManage\Verify\FailedToVerifyRelease;
use Php\Pie\SelfManage\Verify\VerifyPieReleaseUsingAttestation;
+use Php\Pie\Util\Emoji;
use Php\Pie\Util\PieVersion;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
@@ -34,6 +35,7 @@ public function __construct(
private readonly string $githubApiBaseUrl,
private readonly QuieterConsoleIO $io,
private readonly ContainerInterface $container,
+ private readonly FullPathToSelf $fullPathToSelf,
) {
parent::__construct();
}
@@ -64,7 +66,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
$httpDownloader = new HttpDownloader($this->io, $composer->getConfig());
$authHelper = new AuthHelper($this->io, $composer->getConfig());
$latestRelease = new ReleaseMetadata(PieVersion::get(), 'blah');
- $pharFilename = BinaryFile::fromFileWithSha256Checksum((new FullPathToSelf())());
+ $pharFilename = BinaryFile::fromFileWithSha256Checksum(($this->fullPathToSelf)());
$verifyPiePhar = VerifyPieReleaseUsingAttestation::factory($this->githubApiBaseUrl, $httpDownloader, $authHelper);
try {
@@ -80,7 +82,8 @@ public function execute(InputInterface $input, OutputInterface $output): int
}
$output->writeln(sprintf(
- '✅ You are running an authentic PIE version %s.',
+ '%s You are running an authentic PIE version %s.',
+ Emoji::GREEN_CHECKMARK,
$latestRelease->tag,
));
diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php
index 2bb5abdb..aea0af66 100644
--- a/src/Command/ShowCommand.php
+++ b/src/Command/ShowCommand.php
@@ -12,6 +12,7 @@
use Php\Pie\Platform as PiePlatform;
use Php\Pie\Platform\InstalledPiePackages;
use Php\Pie\Platform\OperatingSystem;
+use Php\Pie\Util\Emoji;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -133,7 +134,11 @@ static function (string $version, string $phpExtensionName) use ($showAll, $outp
$unmatchedPiePackages = array_diff(array_keys($piePackages), $piePackagesMatched);
if (count($unmatchedPiePackages)) {
- $output->writeln("\n" . ' ⚠️ PIE packages not loaded:>');
+ $output->writeln(sprintf(
+ '%s %s PIE packages not loaded:>',
+ "\n",
+ Emoji::WARNING,
+ ));
$output->writeln('These extensions were installed with PIE but are not currently enabled.' . "\n");
foreach ($unmatchedPiePackages as $unmatchedPiePackage) {
@@ -179,9 +184,14 @@ private static function verifyChecksumInformation(
try {
$expectedBinaryFileFromMetadata->verifyAgainstOther($actualBinaryFile);
} catch (BinaryFileFailedVerification) {
- return ' ⚠️ was ' . substr($actualBinaryFile->checksum, 0, 8) . '..., expected ' . substr($expectedBinaryFileFromMetadata->checksum, 0, 8) . '...';
+ return sprintf(
+ ' %s was %s..., expected %s...',
+ Emoji::WARNING,
+ substr($actualBinaryFile->checksum, 0, 8),
+ substr($expectedBinaryFileFromMetadata->checksum, 0, 8),
+ );
}
- return ' ✅';
+ return ' ' . Emoji::GREEN_CHECKMARK;
}
}
diff --git a/src/Container.php b/src/Container.php
index 02233a06..b756e232 100644
--- a/src/Container.php
+++ b/src/Container.php
@@ -27,6 +27,7 @@
use Php\Pie\DependencyResolver\ResolveDependencyWithComposer;
use Php\Pie\Downloading\GithubPackageReleaseAssets;
use Php\Pie\Downloading\PackageReleaseAssets;
+use Php\Pie\File\FullPathToSelf;
use Php\Pie\Installing\Ini;
use Php\Pie\Installing\Install;
use Php\Pie\Installing\Uninstall;
@@ -43,6 +44,7 @@
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
+use function getcwd;
use function str_starts_with;
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
@@ -115,6 +117,10 @@ static function (ConsoleCommandEvent $event) use (&$displayedBanner): void {
->needs('$githubApiBaseUrl')
->give('https://api.github.com');
+ $container->when(FullPathToSelf::class)
+ ->needs('$originalCwd')
+ ->give(getcwd());
+
$container->singleton(
Build::class,
static function (ContainerInterface $container): Build {
diff --git a/src/File/FullPathToSelf.php b/src/File/FullPathToSelf.php
index 8f559120..01a6e1e0 100644
--- a/src/File/FullPathToSelf.php
+++ b/src/File/FullPathToSelf.php
@@ -7,7 +7,6 @@
use RuntimeException;
use function array_key_exists;
-use function getcwd;
use function is_string;
use function preg_match;
use function realpath;
@@ -17,6 +16,11 @@
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
class FullPathToSelf
{
+ /** @psalm-suppress PossiblyUnusedMethod no direct reference; used in service locator */
+ public function __construct(private readonly string $originalCwd)
+ {
+ }
+
/** @return non-empty-string */
public function __invoke(): string
{
@@ -28,7 +32,7 @@ public function __invoke(): string
return $this->isAbsolutePath($phpSelf)
? $phpSelf
- : (getcwd() . DIRECTORY_SEPARATOR . $phpSelf);
+ : ($this->originalCwd . DIRECTORY_SEPARATOR . $phpSelf);
}
private function isAbsolutePath(string $path): bool
diff --git a/src/Installing/InstallForPhpProject/InstallSelectedPackage.php b/src/Installing/InstallForPhpProject/InstallSelectedPackage.php
index 3245f754..62022f69 100644
--- a/src/Installing/InstallForPhpProject/InstallSelectedPackage.php
+++ b/src/Installing/InstallForPhpProject/InstallSelectedPackage.php
@@ -21,10 +21,14 @@
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
class InstallSelectedPackage
{
+ public function __construct(private readonly FullPathToSelf $fullPathToSelf)
+ {
+ }
+
public function withPieCli(string $selectedPackage, InputInterface $input, OutputInterface $output): void
{
$process = [
- (new FullPathToSelf())(),
+ ($this->fullPathToSelf)(),
'install',
$selectedPackage,
];
diff --git a/src/Installing/SetupIniFile.php b/src/Installing/SetupIniFile.php
index e169246c..ed053c2a 100644
--- a/src/Installing/SetupIniFile.php
+++ b/src/Installing/SetupIniFile.php
@@ -9,6 +9,7 @@
use Php\Pie\File\BinaryFile;
use Php\Pie\Installing\Ini\SetupIniApproach;
use Php\Pie\Platform\TargetPlatform;
+use Php\Pie\Util\Emoji;
use Symfony\Component\Console\Output\OutputInterface;
use function sprintf;
@@ -34,7 +35,8 @@ public function __invoke(
&& $this->setupIniApproach->setup($targetPlatform, $downloadedPackage, $binaryFile, $output)
) {
$output->writeln(sprintf(
- '✅ Extension is enabled and loaded in %s',
+ '%s Extension is enabled and loaded in %s',
+ Emoji::GREEN_CHECKMARK,
$targetPlatform->phpBinaryPath->phpBinaryPath,
));
} else {
@@ -42,7 +44,7 @@ public function __invoke(
$output->writeln('Automatic extension enabling was skipped.', OutputInterface::VERBOSITY_VERBOSE);
}
- $output->writeln('⚠️ Extension has NOT been automatically enabled.');
+ $output->writeln(sprintf('%s Extension has NOT been automatically enabled.', Emoji::WARNING));
$output->writeln(sprintf(
'You must now add "%s=%s" to your php.ini',
$downloadedPackage->package->extensionType() === ExtensionType::PhpModule ? 'extension' : 'zend_extension',
diff --git a/src/SelfManage/Verify/FallbackVerificationUsingOpenSsl.php b/src/SelfManage/Verify/FallbackVerificationUsingOpenSsl.php
index aca1f7cd..68db5e87 100644
--- a/src/SelfManage/Verify/FallbackVerificationUsingOpenSsl.php
+++ b/src/SelfManage/Verify/FallbackVerificationUsingOpenSsl.php
@@ -10,6 +10,7 @@
use OpenSSLAsymmetricKey;
use Php\Pie\File\BinaryFile;
use Php\Pie\SelfManage\Update\ReleaseMetadata;
+use Php\Pie\Util\Emoji;
use Symfony\Component\Console\Output\OutputInterface;
use Webmozart\Assert\Assert;
@@ -89,7 +90,10 @@ public function verify(ReleaseMetadata $releaseMetadata, BinaryFile $pharFilenam
$output->writeln('#' . $attestationIndex . ': DSSE payload signature verified with certificate.', OutputInterface::VERBOSITY_VERBOSE);
}
- $output->writeln('✅ Verified the new PIE version (using fallback verification)');
+ $output->writeln(sprintf(
+ '%s Verified the new PIE version (using fallback verification)',
+ Emoji::GREEN_CHECKMARK,
+ ));
}
private function assertCertificateSignedByTrustedRoot(Attestation $attestation): void
diff --git a/src/SelfManage/Verify/GithubCliAttestationVerification.php b/src/SelfManage/Verify/GithubCliAttestationVerification.php
index 901246d5..af98ea1c 100644
--- a/src/SelfManage/Verify/GithubCliAttestationVerification.php
+++ b/src/SelfManage/Verify/GithubCliAttestationVerification.php
@@ -6,12 +6,14 @@
use Php\Pie\File\BinaryFile;
use Php\Pie\SelfManage\Update\ReleaseMetadata;
+use Php\Pie\Util\Emoji;
use Php\Pie\Util\Process;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\ExecutableFinder;
use function implode;
+use function sprintf;
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
final class GithubCliAttestationVerification implements VerifyPiePhar
@@ -47,6 +49,6 @@ public function verify(ReleaseMetadata $releaseMetadata, BinaryFile $pharFilenam
throw FailedToVerifyRelease::fromGhCliFailure($releaseMetadata, $processFailedException);
}
- $output->writeln('✅ Verified the new PIE version');
+ $output->writeln(sprintf('%s Verified the new PIE version', Emoji::GREEN_CHECKMARK));
}
}
diff --git a/src/Util/Emoji.php b/src/Util/Emoji.php
new file mode 100644
index 00000000..a368414f
--- /dev/null
+++ b/src/Util/Emoji.php
@@ -0,0 +1,13 @@
+composerFactoryForProject = $this->createMock(ComposerFactoryForProject::class);
$this->findMatchingPackages = $this->createMock(FindMatchingPackages::class);
+ $this->installedPiePackages = $this->createMock(InstalledPiePackages::class);
$this->installSelectedPackage = $this->createMock(InstallSelectedPackage::class);
$this->installPiePackage = $this->createMock(InstallPiePackageFromPath::class);
$this->questionHelper = $this->createMock(QuestionHelper::class);
@@ -76,6 +80,7 @@ function (string $service): mixed {
$cmd = new InstallExtensionsForProjectCommand(
$this->composerFactoryForProject,
new DetermineExtensionsRequired(),
+ $this->installedPiePackages,
$this->findMatchingPackages,
$this->installSelectedPackage,
$this->installPiePackage,
@@ -91,8 +96,15 @@ public function testInstallingExtensionsForPhpProject(): void
{
$rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3');
$rootPackage->setRequires([
- 'ext-standard' => new Link('my/project', 'ext-standard', new Constraint('=', '*'), Link::TYPE_REQUIRE),
- 'ext-foobar' => new Link('my/project', 'ext-foobar', new Constraint('=', '*'), Link::TYPE_REQUIRE),
+ 'ext-standard' => new Link('my/project', 'ext-standard', new Constraint('=', '*'), Link::TYPE_REQUIRE, '*'),
+ 'ext-foobar' => new Link('my/project', 'ext-foobar', new MultiConstraint([
+ new Constraint('>=', '1.2.0.0-dev'),
+ new Constraint('<', '2.0.0.0-dev'),
+ ]), Link::TYPE_REQUIRE, '^1.2'),
+// 'ext-mismatching' => new Link('my/project', 'ext-mismatching', new MultiConstraint([
+// new Constraint('>=', '2.0.0.0-dev'),
+// new Constraint('<', '3.0.0.0-dev'),
+// ]), Link::TYPE_REQUIRE, '^2.0'),
]);
$this->composerFactoryForProject->method('rootPackage')->willReturn($rootPackage);
@@ -107,6 +119,17 @@ public function testInstallingExtensionsForPhpProject(): void
$this->composerFactoryForProject->method('composer')->willReturn($composer);
+// $this->installedPiePackages->method('allPiePackages')->willReturn([
+// 'mismatching' => new Package(
+// $this->createMock(CompletePackageInterface::class),
+// ExtensionType::PhpModule,
+// ExtensionName::normaliseFromString('mismatching'),
+// 'vendor/mismatching',
+// '1.9.3',
+// null,
+// ),
+// ]);
+
$this->findMatchingPackages->method('for')->willReturn([
['name' => 'vendor1/foobar', 'description' => 'The official foobar implementation'],
['name' => 'vendor2/afoobar', 'description' => 'An improved async foobar extension'],
@@ -116,7 +139,7 @@ public function testInstallingExtensionsForPhpProject(): void
$this->installSelectedPackage->expects(self::once())
->method('withPieCli')
- ->with('vendor1/foobar');
+ ->with('vendor1/foobar:^1.2');
$this->commandTester->execute(
[],
@@ -127,8 +150,8 @@ public function testInstallingExtensionsForPhpProject(): void
$this->commandTester->assertCommandIsSuccessful();
self::assertStringContainsString('Checking extensions for your project my/project', $outputString);
- self::assertStringContainsString('requires: my/project requires ext-standard (== *) ✅ Already installed', $outputString);
- self::assertStringContainsString('requires: my/project requires ext-foobar (== *) ⚠️ Missing', $outputString);
+ self::assertStringContainsString('requires: ext-standard:* ✅ Already installed', $outputString);
+ self::assertStringContainsString('requires: ext-foobar:^1.2 🚫 Missing', $outputString);
}
public function testInstallingExtensionsForPieProject(): void
diff --git a/test/unit/Installing/InstallForPhpProject/InstallSelectedPackageTest.php b/test/unit/Installing/InstallForPhpProject/InstallSelectedPackageTest.php
index 87414fc6..f9c63e32 100644
--- a/test/unit/Installing/InstallForPhpProject/InstallSelectedPackageTest.php
+++ b/test/unit/Installing/InstallForPhpProject/InstallSelectedPackageTest.php
@@ -5,6 +5,7 @@
namespace Php\PieUnitTest\Installing\InstallForPhpProject;
use Composer\Util\Platform;
+use Php\Pie\File\FullPathToSelf;
use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
@@ -13,6 +14,7 @@
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\BufferedOutput;
+use function getcwd;
use function trim;
#[CoversClass(InstallSelectedPackage::class)]
@@ -33,7 +35,7 @@ public function testWithPieCli(): void
);
$output = new BufferedOutput();
- (new InstallSelectedPackage())->withPieCli(
+ (new InstallSelectedPackage(new FullPathToSelf(getcwd())))->withPieCli(
'foo/bar',
$input,
$output,