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
25 changes: 25 additions & 0 deletions features/reinstall-extensions.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Feature: Extensions can be re-installed with PIE, but only if needed

# pie download <ext> && pie install <ext>
Example: Installing a previously downloaded extension should install the extension
Given an extension was previously downloaded but not built
When I run a command to install an extension
Then the extension should have been installed and enabled

# pie build <ext> && pie install <ext>
Example: Installing a previously built extension should install the extension
Given an extension was previously built but not installed
When I run a command to install an extension
Then the extension should have been installed and enabled

# pie install <ext> && pie install <ext>
Example: Re-installing an existing extension is a no-op
Given an extension was previously installed and enabled
When I run a command to install an extension
Then the extension should not have been re-installed

# pie install <ext> && pie install --force <ext>
Example: Forcefully re-installing an existing extension should re-install the extension
Given an extension was previously installed and enabled
When I run a command to forcefully install an extension
Then the extension should have been installed and enabled
12 changes: 0 additions & 12 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -240,18 +240,6 @@ parameters:
count: 2
path: test/integration/Command/InstallCommandTest.php

-
message: '#^Call to function assert\(\) with true will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: test/integration/Command/InstallCommandTest.php

-
message: '#^Call to function is_string\(\) with non\-empty\-string will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
count: 1
path: test/integration/Command/InstallCommandTest.php

-
message: '#^Call to function assert\(\) with true will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
Expand Down
2 changes: 0 additions & 2 deletions src/Command/ShowCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use Composer\IO\NullIO;
use Php\Pie\ComposerIntegration\PieComposerFactory;
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys;
use Php\Pie\DependencyResolver\BundledPhpExtensionRefusal;
use Php\Pie\DependencyResolver\Package;
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
Expand All @@ -33,7 +32,6 @@
use function rtrim;
use function sprintf;

/** @phpstan-import-type PieMetadata from PieInstalledJsonMetadataKeys */
#[AsCommand(
name: 'show',
description: 'List the installed modules and their versions.',
Expand Down
130 changes: 130 additions & 0 deletions src/ComposerIntegration/AddInstalledJsonMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

declare(strict_types=1);

namespace Php\Pie\ComposerIntegration;

use Composer\Package\CompleteAliasPackage;
use Composer\Package\CompletePackageInterface;
use Composer\PartialComposer;
use Php\Pie\File\BinaryFile;
use Webmozart\Assert\Assert;

use function array_merge;
use function implode;

/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
class AddInstalledJsonMetadata
{
public function addDownloadMetadata(
PartialComposer $composer,
PieComposerRequest $composerRequest,
CompletePackageInterface $composerPackage,
): void {
$this->addPieMetadata(
$composer,
$composerPackage,
InstalledJsonMetadata::KEY_TARGET_PLATFORM_PHP_PATH,
$composerRequest->targetPlatform->phpBinaryPath->phpBinaryPath,
);
$this->addPieMetadata(
$composer,
$composerPackage,
InstalledJsonMetadata::KEY_TARGET_PLATFORM_PHP_CONFIG_PATH,
$composerRequest->targetPlatform->phpBinaryPath->phpConfigPath(),
);
$this->addPieMetadata(
$composer,
$composerPackage,
InstalledJsonMetadata::KEY_TARGET_PLATFORM_PHP_VERSION,
$composerRequest->targetPlatform->phpBinaryPath->version(),
);
$this->addPieMetadata(
$composer,
$composerPackage,
InstalledJsonMetadata::KEY_TARGET_PLATFORM_PHP_THREAD_SAFETY,
$composerRequest->targetPlatform->threadSafety->name,
);
$this->addPieMetadata(
$composer,
$composerPackage,
InstalledJsonMetadata::KEY_TARGET_PLATFORM_PHP_WINDOWS_COMPILER,
$composerRequest->targetPlatform->windowsCompiler?->name,
);
$this->addPieMetadata(
$composer,
$composerPackage,
InstalledJsonMetadata::KEY_TARGET_PLATFORM_ARCHITECTURE,
$composerRequest->targetPlatform->architecture->name,
);
}

public function addBuildMetadata(
PartialComposer $composer,
PieComposerRequest $composerRequest,
CompletePackageInterface $composerPackage,
BinaryFile $builtBinary,
): void {
$this->addPieMetadata(
$composer,
$composerPackage,
InstalledJsonMetadata::KEY_CONFIGURE_OPTIONS,
implode(' ', $composerRequest->configureOptions),
);

$this->addPieMetadata(
$composer,
$composerPackage,
InstalledJsonMetadata::KEY_PHPIZE_BINARY,
$composerRequest->targetPlatform->phpizePath->phpizeBinaryPath ?? null,
);

$this->addPieMetadata(
$composer,
$composerPackage,
InstalledJsonMetadata::KEY_BUILT_BINARY,
$builtBinary->filePath,
);

$this->addPieMetadata(
$composer,
$composerPackage,
InstalledJsonMetadata::KEY_BINARY_CHECKSUM,
$builtBinary->checksum,
);
}

public function addInstallMetadata(
PartialComposer $composer,
CompletePackageInterface $composerPackage,
BinaryFile $installedBinary,
): void {
$this->addPieMetadata(
$composer,
$composerPackage,
InstalledJsonMetadata::KEY_INSTALLED_BINARY,
$installedBinary->filePath,
);
}

/** @param InstalledJsonMetadata::KEY_* $key */
private function addPieMetadata(
PartialComposer $composer,
CompletePackageInterface $composerPackage,
string $key,
string|null $value,
): void {
$localRepositoryPackage = $composer
->getRepositoryManager()
->getLocalRepository()
->findPackages($composerPackage->getName())[0];

if ($localRepositoryPackage instanceof CompleteAliasPackage) {
$localRepositoryPackage = $localRepositoryPackage->getAliasOf();
}

Assert::methodExists($localRepositoryPackage, 'setExtra');

$localRepositoryPackage->setExtra(array_merge($localRepositoryPackage->getExtra(), [$key => $value]));
}
}
65 changes: 62 additions & 3 deletions src/ComposerIntegration/ComposerIntegrationHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@
use Composer\Composer;
use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory;
use Composer\Installer;
use Composer\IO\IOInterface;
use Composer\Package\CompleteAliasPackage;
use Composer\Package\CompletePackageInterface;
use Php\Pie\DependencyResolver\Package;
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
use Php\Pie\ExtensionName;
use Php\Pie\Platform;
use Php\Pie\Platform\TargetPlatform;
use Php\Pie\Util\Emoji;
use Psr\Container\ContainerInterface;

use function assert;
use function file_exists;
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 */
class ComposerIntegrationHandler
Expand Down Expand Up @@ -63,9 +70,61 @@ public function runInstall(
// Refresh the Composer instance so it re-reads the updated pie.json
$composer = PieComposerFactory::recreatePieComposer($this->container, $composer);

// Removing the package from the local repository will trick Composer into "re-installing" it :)
foreach ($composer->getRepositoryManager()->getLocalRepository()->findPackages($requestedPackageAndVersion->package) as $pkg) {
$composer->getRepositoryManager()->getLocalRepository()->removePackage($pkg);
foreach ($composer->getRepositoryManager()->getLocalRepository()->getPackages() as $localRepoPackage) {
$extName = ExtensionName::determineFromComposerPackage($localRepoPackage);

if ($localRepoPackage instanceof CompleteAliasPackage) {
$localRepoPackage = $localRepoPackage->getAliasOf();
}

assert($localRepoPackage instanceof CompletePackageInterface);
$piePackage = Package::fromComposerCompletePackage($localRepoPackage);
$installedJsonMetadata = $piePackage->installedJsonMetadata();
$status = $piePackage->verifyPackageStatus($targetPlatform);

$this->arrayCollectionIo->write(sprintf(
'Install status %s (%s) status=%s',
$localRepoPackage->getName(),
$extName->name(),
$status->description(),
), verbosity: IOInterface::VERY_VERBOSE);

if ($status->isVerified() && ! $forceInstallPackageVersion) {
$this->arrayCollectionIo->write(sprintf(
'%s PIE package %s (%s) is already installed and verified.',
Emoji::GREEN_CHECKMARK,
$localRepoPackage->getName(),
$extName->name(),
), verbosity: IOInterface::QUIET);
continue;
}

if (! $installedJsonMetadata->isInstalled() && $installedJsonMetadata->isBuilt()) {
$this->arrayCollectionIo->write(sprintf(
'%s PIE package %s (%s) was previously built but not installed.',
Emoji::INFO,
$localRepoPackage->getName(),
$extName->name(),
), verbosity: IOInterface::VERBOSE);
}

if (! $installedJsonMetadata->isInstalled() && ! $installedJsonMetadata->isBuilt() && $installedJsonMetadata->isDownloaded()) {
$this->arrayCollectionIo->write(sprintf(
'%s PIE package %s (%s) was previously downloaded but not built.',
Emoji::INFO,
$localRepoPackage->getName(),
$extName->name(),
), verbosity: IOInterface::VERBOSE);
}

$this->arrayCollectionIo->write(sprintf(
'%s Package status of %s (%s) is not yet verified, adding to install candidates: %s',
Emoji::WARNING,
$localRepoPackage->getName(),
$extName->name(),
$status->description(),
));
$composer->getRepositoryManager()->getLocalRepository()->removePackage($localRepoPackage);
}

$composerInstaller = PieComposerInstaller::createWithPhpBinary(
Expand Down
8 changes: 4 additions & 4 deletions src/ComposerIntegration/InstallAndBuildProcess.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class InstallAndBuildProcess
public function __construct(
private readonly Build $pieBuild,
private readonly Install $pieInstall,
private readonly InstalledJsonMetadata $installedJsonMetadata,
private readonly AddInstalledJsonMetadata $addInstalledJsonMetadata,
private readonly PlaceholderReplacer $placeholderReplacer,
) {
}
Expand Down Expand Up @@ -50,7 +50,7 @@ public function __invoke(
$downloadedPackage,
);

$this->installedJsonMetadata->addDownloadMetadata(
$this->addInstalledJsonMetadata->addDownloadMetadata(
$composer,
$composerRequest,
$composerPackage,
Expand All @@ -65,7 +65,7 @@ public function __invoke(
$io,
);

$this->installedJsonMetadata->addBuildMetadata(
$this->addInstalledJsonMetadata->addBuildMetadata(
$composer,
$composerRequest,
$composerPackage,
Expand All @@ -77,7 +77,7 @@ public function __invoke(
return;
}

$this->installedJsonMetadata->addInstallMetadata(
$this->addInstalledJsonMetadata->addInstallMetadata(
$composer,
$composerPackage,
($this->pieInstall)(
Expand Down
Loading
Loading