diff --git a/bin/pie b/bin/pie index 2b907bb1..caa0a2f4 100755 --- a/bin/pie +++ b/bin/pie @@ -13,6 +13,7 @@ use Php\Pie\Command\RepositoryAddCommand; use Php\Pie\Command\RepositoryListCommand; use Php\Pie\Command\RepositoryRemoveCommand; use Php\Pie\Command\ShowCommand; +use Php\Pie\Command\UninstallCommand; use Php\Pie\Util\PieVersion; use Symfony\Component\Console\Application; use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; @@ -37,6 +38,7 @@ $application->setCommandLoader(new ContainerCommandLoader( 'repository:list' => RepositoryListCommand::class, 'repository:add' => RepositoryAddCommand::class, 'repository:remove' => RepositoryRemoveCommand::class, + 'uninstall' => UninstallCommand::class, ] )); diff --git a/composer.json b/composer.json index ba043ff4..037a249d 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ ], "require": { "php": "8.1.*||8.2.*||8.3.*||8.4.*", - "composer/composer": "^2.8.5", + "composer/composer": "dev-main", "composer/pcre": "^3.3.2", "composer/semver": "^3.4.3", "fidry/cpu-core-counter": "^1.2", diff --git a/composer.lock b/composer.lock index c6ecdfbd..fa318734 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "819abdc1a00f374dc389cde52c096979", + "content-hash": "010234aff469fe878aace1bf281c1dff", "packages": [ { "name": "composer/ca-bundle", @@ -157,16 +157,16 @@ }, { "name": "composer/composer", - "version": "2.8.5", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "ae208dc1e182bd45d99fcecb956501da212454a1" + "reference": "b70b6bd0709f6b2071935ee2f2b6061b812724d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/ae208dc1e182bd45d99fcecb956501da212454a1", - "reference": "ae208dc1e182bd45d99fcecb956501da212454a1", + "url": "https://api.github.com/repos/composer/composer/zipball/b70b6bd0709f6b2071935ee2f2b6061b812724d0", + "reference": "b70b6bd0709f6b2071935ee2f2b6061b812724d0", "shasum": "" }, "require": { @@ -205,6 +205,7 @@ "ext-zip": "Enabling the zip extension allows you to unzip archives", "ext-zlib": "Allow gzip compression of HTTP requests" }, + "default-branch": true, "bin": [ "bin/composer" ], @@ -251,7 +252,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.8.5" + "source": "https://github.com/composer/composer/tree/main" }, "funding": [ { @@ -267,7 +268,7 @@ "type": "tidelift" } ], - "time": "2025-01-21T14:23:40+00:00" + "time": "2025-02-05T10:20:56+00:00" }, { "name": "composer/metadata-minifier", @@ -7003,13 +7004,15 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "composer/composer": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": "8.1.*||8.2.*||8.3.*||8.4.*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.1.99" }, diff --git a/docs/extension-maintainers.md b/docs/extension-maintainers.md index 610728bd..7f34de30 100644 --- a/docs/extension-maintainers.md +++ b/docs/extension-maintainers.md @@ -160,6 +160,26 @@ should specify this path in `build-path`, for example: } ``` +The `build-path` may contain some templated values which are replaced: + + * `{version}` to be replaced with the package version. For example a package + with version 1.2.3 with a `build-path` of `myext-{version}` the actual build + path would become `myext-1.2.3`. + +#### `download-url-method` + +The `download-url-method` directive allows extension maintainers to +change the behaviour of downloading the source package. + + * Setting this to `composer-default`, which is the default value if not + specified, will use the default behaviour implemented by Composer, which is + to use the standard ZIP archive from the GitHub API (or other source control + system). + * Using `pre-packaged-source` will locate a source code package in the release + assets list based matching one of the following naming conventions: + * `php_{ExtensionName}-{Version}-src.tgz` (e.g. `php_myext-1.20.1-src.tgz`) + * `php_{ExtensionName}-{Version}-src.zip` (e.g. `php_myext-1.20.1-src.zip`) + ### Extension dependencies Extension authors may define some dependencies in `require`, but practically, diff --git a/features/uninstall-extensions.feature b/features/uninstall-extensions.feature new file mode 100644 index 00000000..154c10cf --- /dev/null +++ b/features/uninstall-extensions.feature @@ -0,0 +1,8 @@ +Feature: Extensions can be uninstalled with PIE + + # See https://github.com/php/pie/issues/190 for why this is non-Windows + @non-windows + Example: An extension can be uninstalled + Given an extension was previously installed + When I run a command to uninstall an extension + Then the extension should not be installed anymore diff --git a/resources/composer-json-php-ext-schema.json b/resources/composer-json-php-ext-schema.json new file mode 100644 index 00000000..ca283510 --- /dev/null +++ b/resources/composer-json-php-ext-schema.json @@ -0,0 +1,106 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/php/pie/main/composer-json-php-ext-schema.json", + "title": "composer.json php-ext schema", + "description": "Schema for the proposed php-ext section in composer.json that the new PECL will use to build packages", + "type": "object", + "properties": { + "php-ext": { + "type": "object", + "description": "Settings for PHP extension packages.", + "properties": { + "extension-name": { + "type": "string", + "description": "If specified, this will be used as the name of the extension, where needed by tooling. If this is not specified, the extension name will be derived from the Composer package name (e.g. `vendor/name` would become `ext-name`). The extension name may be specified with or without the `ext-` prefix, and tools that use this must normalise this appropriately.", + "example": "ext-xdebug" + }, + "priority": { + "type": "integer", + "description": "This is used to add a prefix to the INI file, e.g. `90-xdebug.ini` which affects the loading order. The priority is a number in the range 10-99 inclusive, with 10 being the highest priority (i.e. will be processed first), and 99 being the lowest priority (i.e. will be processed last). There are two digits so that the files sort correctly on any platform, whether the sorting is natural or not.", + "minimum": 10, + "maximum": 99, + "example": 80, + "default": 80 + }, + "support-zts": { + "type": "boolean", + "description": "Does this package support Zend Thread Safety", + "example": false, + "default": true + }, + "support-nts": { + "type": "boolean", + "description": "Does this package support non-Thread Safe mode", + "example": false, + "default": true + }, + "build-path": { + "type": ["string", "null"], + "description": "If specified, this is the subdirectory that will be used to build the extension instead of the root of the project.", + "example": "my-extension-source", + "default": null + }, + "download-url-method": { + "type": "string", + "description": "If specified, this technique will be used to override the URL that PIE uses to download the asset. The default, if not specified, is composer-default.", + "enum": ["composer-default", "pre-packaged-source"], + "example": "composer-default" + }, + "os-families": { + "type": "array", + "minItems": 1, + "description": "An array of OS families to mark as compatible with the extension. Specifying this property will mean this package is not installable with PIE on any OS family not listed here. Must not be specified alongside os-families-exclude.", + "items": { + "type": "string", + "enum": ["windows", "bsd", "darwin", "solaris", "linux", "unknown"], + "description": "The name of the OS family to mark as compatible." + } + }, + "os-families-exclude": { + "type": "array", + "minItems": 1, + "description": "An array of OS families to mark as incompatible with the extension. Specifying this property will mean this package is installable on any OS family except those listed here. Must not be specified alongside os-families.", + "items": { + "type": "string", + "enum": ["windows", "bsd", "darwin", "solaris", "linux", "unknown"], + "description": "The name of the OS family to exclude." + } + }, + "configure-options": { + "type": "array", + "description": "These configure options make up the flags that can be passed to ./configure when installing the extension.", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "The name of the flag, this would typically be prefixed with `--`, for example, the value 'the-flag' would be passed as `./configure --the-flag`.", + "example": "without-xdebug-compression", + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9-_]*$" + }, + "needs-value": { + "type": "boolean", + "description": "If this is set to true, the flag needs a value (e.g. --with-somelib=), otherwise it is a flag without a value (e.g. --enable-some-feature).", + "example": false, + "default": false + }, + "description": { + "type": "string", + "description": "The description of what the flag does or means.", + "example": "Disable compression through zlib" + } + } + } + } + }, + "allOf": [ + { + "not": { + "required": ["os-families", "os-families-exclude"] + } + } + ] + } + } +} diff --git a/src/Building/Build.php b/src/Building/Build.php index 884b170b..4ec00668 100644 --- a/src/Building/Build.php +++ b/src/Building/Build.php @@ -4,8 +4,8 @@ namespace Php\Pie\Building; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPhp\PhpizePath; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Building/UnixBuild.php b/src/Building/UnixBuild.php index 6e7c9942..ec91385f 100644 --- a/src/Building/UnixBuild.php +++ b/src/Building/UnixBuild.php @@ -4,8 +4,8 @@ namespace Php\Pie\Building; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPhp\PhpizePath; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Util\Process; @@ -76,7 +76,7 @@ public function __invoke( $this->make($targetPlatform, $downloadedPackage, $output, $outputCallback); - $expectedSoFile = $downloadedPackage->extractedSourcePath . '/modules/' . $downloadedPackage->package->extensionName->name() . '.so'; + $expectedSoFile = $downloadedPackage->extractedSourcePath . '/modules/' . $downloadedPackage->package->extensionName()->name() . '.so'; if (! file_exists($expectedSoFile)) { throw ExtensionBinaryNotFound::fromExpectedBinary($expectedSoFile); diff --git a/src/Building/WindowsBuild.php b/src/Building/WindowsBuild.php index 991ac77d..66cbff49 100644 --- a/src/Building/WindowsBuild.php +++ b/src/Building/WindowsBuild.php @@ -4,8 +4,8 @@ namespace Php\Pie\Building; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPhp\PhpizePath; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Platform\WindowsExtensionAssetName; diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php index 09983648..fd5588fa 100644 --- a/src/Command/BuildCommand.php +++ b/src/Command/BuildCommand.php @@ -64,7 +64,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $requestedNameAndVersion, $forceInstallPackageVersion, ); - $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix())); + $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix())); // Now we know what package we have, we can validate the configure options for the command and re-create the // Composer instance with the populated configure options @@ -85,7 +85,7 @@ public function execute(InputInterface $input, OutputInterface $output): int ); try { - ($this->composerIntegrationHandler)( + $this->composerIntegrationHandler->runInstall( $package, $composer, $targetPlatform, diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 1fc4c7a9..a4563145 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -64,6 +64,12 @@ public static function configurePhpConfigOptions(Command $command): void InputOption::VALUE_REQUIRED, 'The path to the `php` binary to use as the target PHP platform on ' . OperatingSystem::Windows->asFriendlyName() . ', e.g. --' . self::OPTION_WITH_PHP_PATH . '=C:\usr\php7.4.33\php.exe', ); + $command->addOption( + self::OPTION_WITH_PHPIZE_PATH, + null, + InputOption::VALUE_REQUIRED, + 'The path to the `phpize` binary to use as the target PHP platform, e.g. --' . self::OPTION_WITH_PHPIZE_PATH . '=/usr/bin/phpize7.4', + ); } public static function configureDownloadBuildInstallOptions(Command $command): void @@ -71,7 +77,7 @@ public static function configureDownloadBuildInstallOptions(Command $command): v $command->addArgument( self::ARG_REQUESTED_PACKAGE_AND_VERSION, InputArgument::REQUIRED, - 'The extension name and version constraint to use, in the format {ext-name}{?:{?version-constraint}{?@stability}}, for example `xdebug/xdebug:^3.4@alpha`, `xdebug/xdebug:@alpha`, `xdebug/xdebug:^3.4`, etc.', + 'The PIE package name and version constraint to use, in the format {vendor/package}{?:{?version-constraint}{?@stability}}, for example `xdebug/xdebug:^3.4@alpha`, `xdebug/xdebug:@alpha`, `xdebug/xdebug:^3.4`, etc.', ); $command->addOption( self::OPTION_MAKE_PARALLEL_JOBS, @@ -79,12 +85,6 @@ public static function configureDownloadBuildInstallOptions(Command $command): v InputOption::VALUE_REQUIRED, 'Override many jobs to run in parallel when running compiling (this is passed to "make -jN" during build). PIE will try to detect this by default.', ); - $command->addOption( - self::OPTION_WITH_PHPIZE_PATH, - null, - InputOption::VALUE_REQUIRED, - 'The path to the `phpize` binary to use as the target PHP platform, e.g. --' . self::OPTION_WITH_PHPIZE_PATH . '=/usr/bin/phpize7.4', - ); $command->addOption( self::OPTION_SKIP_ENABLE_EXTENSION, null, @@ -226,7 +226,7 @@ public static function requestedNameAndVersionPair(InputInterface $input): Reque public static function bindConfigureOptionsFromPackage(Command $command, Package $package, InputInterface $input): void { - foreach ($package->configureOptions as $configureOption) { + foreach ($package->configureOptions() as $configureOption) { $command->addOption( $configureOption->name, null, @@ -242,7 +242,7 @@ public static function bindConfigureOptionsFromPackage(Command $command, Package public static function processConfigureOptionsFromInput(Package $package, InputInterface $input): array { $configureOptionsValues = []; - foreach ($package->configureOptions as $configureOption) { + foreach ($package->configureOptions() as $configureOption) { if (! $input->hasOption($configureOption->name)) { continue; } diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 4a7439b1..9c156459 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -66,10 +66,10 @@ public function execute(InputInterface $input, OutputInterface $output): int $requestedNameAndVersion, $forceInstallPackageVersion, ); - $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix())); + $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix())); try { - ($this->composerIntegrationHandler)( + $this->composerIntegrationHandler->runInstall( $package, $composer, $targetPlatform, diff --git a/src/Command/InfoCommand.php b/src/Command/InfoCommand.php index 0d03e934..4cb56949 100644 --- a/src/Command/InfoCommand.php +++ b/src/Command/InfoCommand.php @@ -64,17 +64,17 @@ public function execute(InputInterface $input, OutputInterface $output): int $requestedNameAndVersion, CommandHelper::determineForceInstallingPackageVersion($input), ); - $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix())); + $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix())); - $output->writeln(sprintf('Extension name: %s', $package->extensionName->name())); - $output->writeln(sprintf('Extension type: %s (%s)', $package->extensionType->value, $package->extensionType->name)); - $output->writeln(sprintf('Composer package name: %s', $package->name)); - $output->writeln(sprintf('Version: %s', $package->version)); - $output->writeln(sprintf('Download URL: %s', $package->downloadUrl ?? '(not specified)')); + $output->writeln(sprintf('Extension name: %s', $package->extensionName()->name())); + $output->writeln(sprintf('Extension type: %s (%s)', $package->extensionType()->value, $package->extensionType()->name)); + $output->writeln(sprintf('Composer package name: %s', $package->name())); + $output->writeln(sprintf('Version: %s', $package->version())); + $output->writeln(sprintf('Download URL: %s', $package->downloadUrl() ?? '(not specified)')); - if (count($package->configureOptions)) { + if (count($package->configureOptions())) { $output->writeln('Configure options:'); - foreach ($package->configureOptions as $configureOption) { + foreach ($package->configureOptions() as $configureOption) { $output->writeln(sprintf(' --%s%s (%s)', $configureOption->name, $configureOption->needsValue ? '=?' : '', $configureOption->description)); } } else { diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index 0640b5e4..ae405949 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -69,7 +69,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $requestedNameAndVersion, $forceInstallPackageVersion, ); - $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix())); + $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix())); // Now we know what package we have, we can validate the configure options for the command and re-create the // Composer instance with the populated configure options @@ -90,7 +90,7 @@ public function execute(InputInterface $input, OutputInterface $output): int ); try { - ($this->composerIntegrationHandler)( + $this->composerIntegrationHandler->runInstall( $package, $composer, $targetPlatform, diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php index cb750f11..6bf3a9a7 100644 --- a/src/Command/ShowCommand.php +++ b/src/Command/ShowCommand.php @@ -4,25 +4,21 @@ namespace Php\Pie\Command; -use Composer\Package\BasePackage; -use Composer\Package\CompletePackageInterface; -use Php\Pie\BinaryFile; use Php\Pie\ComposerIntegration\PieComposerFactory; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys; -use Php\Pie\DependencyResolver\Package; +use Php\Pie\File\BinaryFile; +use Php\Pie\File\BinaryFileFailedVerification; +use Php\Pie\Platform\InstalledPiePackages; use Php\Pie\Platform\OperatingSystem; -use Php\Pie\Platform\TargetPlatform; 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\NullOutput; use Symfony\Component\Console\Output\OutputInterface; -use function array_combine; -use function array_filter; use function array_key_exists; -use function array_map; use function array_walk; use function file_exists; use function sprintf; @@ -38,6 +34,7 @@ final class ShowCommand extends Command { public function __construct( + private readonly InstalledPiePackages $installedPiePackages, private readonly ContainerInterface $container, ) { parent::__construct(); @@ -54,7 +51,15 @@ public function execute(InputInterface $input, OutputInterface $output): int { $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); - $piePackages = $this->buildListOfPieInstalledPackages($output, $targetPlatform); + $composer = PieComposerFactory::createPieComposer( + $this->container, + PieComposerRequest::noOperation( + new NullOutput(), + $targetPlatform, + ), + ); + + $piePackages = $this->installedPiePackages->allPiePackages($composer); $phpEnabledExtensions = $targetPlatform->phpBinaryPath->extensions(); $extensionPath = $targetPlatform->phpBinaryPath->extensionPath(); $extensionEnding = $targetPlatform->operatingSystem === OperatingSystem::Windows ? '.dll' : '.so'; @@ -80,7 +85,7 @@ static function (string $version, string $phpExtensionName) use ($output, $piePa $extensionPath, $phpExtensionName, $extensionEnding, - PieInstalledJsonMetadataKeys::pieMetadataFromComposerPackage($piePackage->composerPackage), + PieInstalledJsonMetadataKeys::pieMetadataFromComposerPackage($piePackage->composerPackage()), ), )); }, @@ -99,10 +104,10 @@ private static function verifyChecksumInformation( string $extensionEnding, array $installedJsonMetadata, ): string { - $expectedConventionalBinaryPath = $extensionPath . DIRECTORY_SEPARATOR . $phpExtensionName . $extensionEnding; + $actualBinaryPathByConvention = $extensionPath . DIRECTORY_SEPARATOR . $phpExtensionName . $extensionEnding; // 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($expectedConventionalBinaryPath)) { + if (! file_exists($actualBinaryPathByConvention)) { return ''; } @@ -110,53 +115,23 @@ private static function verifyChecksumInformation( $pieExpectedChecksum = array_key_exists(PieInstalledJsonMetadataKeys::BinaryChecksum->value, $installedJsonMetadata) ? $installedJsonMetadata[PieInstalledJsonMetadataKeys::BinaryChecksum->value] : null; // Some other kind of mismatch of file path, or we don't have a stored checksum available - if ($expectedConventionalBinaryPath !== $pieExpectedBinaryPath || $pieExpectedChecksum === null) { + if ( + $pieExpectedBinaryPath === null + || $pieExpectedChecksum === null + || $pieExpectedBinaryPath !== $actualBinaryPathByConvention + ) { return ''; } - $actualInstalledBinary = BinaryFile::fromFileWithSha256Checksum($expectedConventionalBinaryPath); - if ($actualInstalledBinary->checksum !== $pieExpectedChecksum) { - return ' ⚠️ was ' . substr($actualInstalledBinary->checksum, 0, 8) . '..., expected ' . substr($pieExpectedChecksum, 0, 8) . '...'; + $expectedBinaryFileFromMetadata = new BinaryFile($pieExpectedBinaryPath, $pieExpectedChecksum); + $actualBinaryFile = BinaryFile::fromFileWithSha256Checksum($actualBinaryPathByConvention); + + try { + $expectedBinaryFileFromMetadata->verifyAgainstOther($actualBinaryFile); + } catch (BinaryFileFailedVerification) { + return ' ⚠️ was ' . substr($actualBinaryFile->checksum, 0, 8) . '..., expected ' . substr($expectedBinaryFileFromMetadata->checksum, 0, 8) . '...'; } return ' ✅'; } - - /** @return array */ - private function buildListOfPieInstalledPackages( - OutputInterface $output, - TargetPlatform $targetPlatform, - ): array { - $composerInstalledPackages = array_map( - static function (CompletePackageInterface $package): Package { - return Package::fromComposerCompletePackage($package); - }, - array_filter( - PieComposerFactory::createPieComposer( - $this->container, - PieComposerRequest::noOperation( - $output, - $targetPlatform, - ), - ) - ->getRepositoryManager() - ->getLocalRepository() - ->getPackages(), - static function (BasePackage $basePackage): bool { - return $basePackage instanceof CompletePackageInterface; - }, - ), - ); - - return array_combine( - array_map( - /** @return non-empty-string */ - static function (Package $package): string { - return $package->extensionName->name(); - }, - $composerInstalledPackages, - ), - $composerInstalledPackages, - ); - } } diff --git a/src/Command/UninstallCommand.php b/src/Command/UninstallCommand.php new file mode 100644 index 00000000..0e6997bf --- /dev/null +++ b/src/Command/UninstallCommand.php @@ -0,0 +1,128 @@ +addArgument( + self::ARG_PACKAGE_NAME, + InputArgument::REQUIRED, + 'The package name to remove, in the format {vendor/package}, for example `xdebug/xdebug`', + ); + + CommandHelper::configurePhpConfigOptions($this); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + if (Platform::isWindows()) { + /** + * @todo add support for uninstalling in Windows - see + * {@link https://github.com/php/pie/issues/190} for details + */ + $output->writeln('Uninstalling extensions on Windows is not currently supported.'); + + return 1; + } + + if (! TargetPlatform::isRunningAsRoot()) { + $output->writeln('This command may need elevated privileges, and may prompt you for your password.'); + } + + $packageToRemove = (string) $input->getArgument(self::ARG_PACKAGE_NAME); + Assert::stringNotEmpty($packageToRemove); + $requestedPackageAndVersionToRemove = new RequestedPackageAndVersion($packageToRemove, null); + + $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); + + $composer = PieComposerFactory::createPieComposer( + $this->container, + PieComposerRequest::noOperation( + new NullOutput(), + $targetPlatform, + ), + ); + + $piePackage = $this->findPiePackageByPackageName($packageToRemove, $composer); + + if ($piePackage === null) { + $output->writeln('No package found: ' . $packageToRemove . ''); + + return 1; + } + + $composer = PieComposerFactory::createPieComposer( + $this->container, + new PieComposerRequest( + $output, + $targetPlatform, + $requestedPackageAndVersionToRemove, + PieOperation::Uninstall, + [], // Configure options are not needed for uninstall + null, + true, + ), + ); + + $this->composerIntegrationHandler->runUninstall( + $piePackage, + $composer, + $targetPlatform, + $requestedPackageAndVersionToRemove, + ); + + return 0; + } + + private function findPiePackageByPackageName(string $packageToRemove, Composer $composer): Package|null + { + $piePackages = $this->installedPiePackages->allPiePackages($composer); + + foreach ($piePackages as $piePackage) { + if ($piePackage->name() === $packageToRemove) { + return $piePackage; + } + } + + return null; + } +} diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index 48d93aa8..4c51f724 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -26,7 +26,7 @@ public function __construct( ) { } - public function __invoke( + public function runInstall( Package $package, Composer $composer, TargetPlatform $targetPlatform, @@ -39,7 +39,7 @@ public function __invoke( // If user did not request a specific require version, use Composer to recommend one for the pie.json if ($recommendedRequireVersion === null) { - $recommendedRequireVersion = $versionSelector->findRecommendedRequireVersion($package->composerPackage); + $recommendedRequireVersion = $versionSelector->findRecommendedRequireVersion($package->composerPackage()); } // Write the new requirement to pie.json; because we later essentially just do a `composer install` using that file @@ -60,7 +60,7 @@ public function __invoke( $composerInstaller = PieComposerInstaller::createWithPhpBinary( $targetPlatform->phpBinaryPath, - $package->extensionName, + $package->extensionName(), $this->arrayCollectionIo, $composer, ); @@ -88,4 +88,46 @@ public function __invoke( ($this->vendorCleanup)($composer); } + + public function runUninstall( + Package $packageToRemove, + Composer $composer, + TargetPlatform $targetPlatform, + RequestedPackageAndVersion $requestedPackageAndVersionToRemove, + ): void { + // Write the new requirement to pie.json; because we later essentially just do a `composer install` using that file + $pieComposerJson = Platform::getPieJsonFilename($targetPlatform); + $pieJsonEditor = PieJsonEditor::fromTargetPlatform($targetPlatform); + $originalPieJsonContent = $pieJsonEditor->removeRequire($requestedPackageAndVersionToRemove->package); + + // Refresh the Composer instance so it re-reads the updated pie.json + $composer = PieComposerFactory::recreatePieComposer($this->container, $composer); + + $composerInstaller = PieComposerInstaller::createWithPhpBinary( + $targetPlatform->phpBinaryPath, + $packageToRemove->extensionName(), + $this->arrayCollectionIo, + $composer, + ); + $composerInstaller + ->setAllowedTypes(['php-ext', 'php-ext-zend']) + ->setInstall(true) + ->setIgnoredTypes([]) + ->setDryRun(false) + ->setDownloadOnly(false); + + if (file_exists(PieComposerFactory::getLockFile($pieComposerJson))) { + $composerInstaller->setUpdate(true); + $composerInstaller->setUpdateAllowList([$requestedPackageAndVersionToRemove->package]); + } + + $resultCode = $composerInstaller->run(); + + if ($resultCode !== Installer::ERROR_NONE) { + // Revert composer.json change + $pieJsonEditor->revert($originalPieJsonContent); + + throw ComposerRunFailed::fromExitCode($resultCode); + } + } } diff --git a/src/ComposerIntegration/InstalledJsonMetadata.php b/src/ComposerIntegration/InstalledJsonMetadata.php index 61bc857f..e91aaaaa 100644 --- a/src/ComposerIntegration/InstalledJsonMetadata.php +++ b/src/ComposerIntegration/InstalledJsonMetadata.php @@ -7,8 +7,8 @@ use Composer\Package\CompletePackage; use Composer\Package\CompletePackageInterface; use Composer\PartialComposer; -use Php\Pie\BinaryFile; use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys as MetadataKey; +use Php\Pie\File\BinaryFile; use Webmozart\Assert\Assert; use function array_merge; diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php new file mode 100644 index 00000000..fc8a6cdc --- /dev/null +++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php @@ -0,0 +1,110 @@ +getEventDispatcher() + ->addListener( + InstallerEvents::PRE_OPERATIONS_EXEC, + new self($composer, $io, $container, $composerRequest), + ); + } + + public function __invoke(InstallerEvent $installerEvent): void + { + /** @psalm-suppress InternalMethod */ + $operations = $installerEvent->getTransaction()?->getOperations() ?? []; + + array_walk( + $operations, + function (OperationInterface $operation): void { + if (! $operation instanceof InstallOperation) { + return; + } + + $composerPackage = $operation->getPackage(); + if (! $composerPackage instanceof CompletePackageInterface) { + return; + } + + // Install requests for other packages than the one we want should be ignored + if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { + return; + } + + $piePackage = Package::fromComposerCompletePackage($composerPackage); + $targetPlatform = $this->composerRequest->targetPlatform; + $downloadUrlMethod = DownloadUrlMethod::fromPackage($piePackage, $targetPlatform); + + // Exit early if we should just use Composer's normal download + if ($downloadUrlMethod === DownloadUrlMethod::ComposerDefaultDownload) { + return; + } + + $possibleAssetNames = $downloadUrlMethod->possibleAssetNames($piePackage, $targetPlatform); + if ($possibleAssetNames === null) { + return; + } + + // @todo https://github.com/php/pie/issues/138 will need to depend on the repo type (GH/GL/BB/etc.) + $packageReleaseAssets = $this->container->get(PackageReleaseAssets::class); + + $url = $packageReleaseAssets->findMatchingReleaseAssetUrl( + $targetPlatform, + $piePackage, + new AuthHelper($this->io, $this->composer->getConfig()), + new HttpDownloader($this->io, $this->composer->getConfig()), + $possibleAssetNames, + ); + + $this->composerRequest->pieOutput->writeln('Found prebuilt archive: ' . $url); + $composerPackage->setDistUrl($url); + + if (pathinfo($url, PATHINFO_EXTENSION) !== 'tgz') { + return; + } + + $composerPackage->setDistType('tar'); + }, + ); + } +} diff --git a/src/ComposerIntegration/RemoveUnrelatedInstallOperations.php b/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php similarity index 91% rename from src/ComposerIntegration/RemoveUnrelatedInstallOperations.php rename to src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php index 874ce7ee..905a276f 100644 --- a/src/ComposerIntegration/RemoveUnrelatedInstallOperations.php +++ b/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php @@ -2,15 +2,17 @@ declare(strict_types=1); -namespace Php\Pie\ComposerIntegration; +namespace Php\Pie\ComposerIntegration\Listeners; use Closure; use Composer\Composer; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\OperationInterface; +use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Transaction; use Composer\Installer\InstallerEvent; use Composer\Installer\InstallerEvents; +use Php\Pie\ComposerIntegration\PieComposerRequest; use Symfony\Component\Console\Output\OutputInterface; use function array_filter; @@ -48,7 +50,7 @@ public function __invoke(InstallerEvent $installerEvent): void $newOperations = array_filter( $installerEvent->getTransaction()?->getOperations() ?? [], function (OperationInterface $operation) use ($pieOutput): bool { - if (! $operation instanceof InstallOperation) { + if (! $operation instanceof InstallOperation && ! $operation instanceof UninstallOperation) { $pieOutput->writeln( sprintf( 'Unexpected operation during installer: %s', diff --git a/src/ComposerIntegration/OverrideWindowsUrlInstallListener.php b/src/ComposerIntegration/OverrideWindowsUrlInstallListener.php deleted file mode 100644 index 43e064a3..00000000 --- a/src/ComposerIntegration/OverrideWindowsUrlInstallListener.php +++ /dev/null @@ -1,75 +0,0 @@ -getEventDispatcher() - ->addListener( - InstallerEvents::PRE_OPERATIONS_EXEC, - new self($composer, $io, $container, $composerRequest), - ); - } - - public function __invoke(InstallerEvent $installerEvent): void - { - if ($this->composerRequest->targetPlatform->operatingSystem !== OperatingSystem::Windows) { - return; - } - - /** @psalm-suppress InternalMethod */ - $operations = $installerEvent->getTransaction()?->getOperations() ?? []; - - Assert::count($operations, 1, 'I can only do exactly %d thing at once, %d attempted'); - $operation = reset($operations); - Assert::isInstanceOf($operation, InstallOperation::class, 'I can only handle %2$s, got %s'); - - $composerPackage = $operation->getPackage(); - Assert::isInstanceOf($composerPackage, CompletePackageInterface::class, 'I can only handle %2$s, got %s'); - - $packageReleaseAssets = $this->container->get(PackageReleaseAssets::class); - $url = $packageReleaseAssets->findWindowsDownloadUrlForPackage( - $this->composerRequest->targetPlatform, - Package::fromComposerCompletePackage($composerPackage), - new AuthHelper($this->io, $this->composer->getConfig()), - new HttpDownloader($this->io, $this->composer->getConfig()), - ); - - $this->composerRequest->pieOutput->writeln('Found prebuilt archive: ' . $url); - $composerPackage->setDistUrl($url); - } -} diff --git a/src/ComposerIntegration/PieComposerFactory.php b/src/ComposerIntegration/PieComposerFactory.php index 279348df..ba9dcd28 100644 --- a/src/ComposerIntegration/PieComposerFactory.php +++ b/src/ComposerIntegration/PieComposerFactory.php @@ -9,8 +9,11 @@ use Composer\Installer; use Composer\IO\IOInterface; use Composer\PartialComposer; +use Composer\Repository\InstalledRepositoryInterface; use Composer\Util\Filesystem; use Composer\Util\ProcessExecutor; +use Php\Pie\ComposerIntegration\Listeners\OverrideDownloadUrlInstallListener; +use Php\Pie\ComposerIntegration\Listeners\RemoveUnrelatedInstallOperations; use Php\Pie\ExtensionType; use Php\Pie\Platform; use Psr\Container\ContainerInterface; @@ -36,6 +39,7 @@ protected function createDefaultInstallers(Installer\InstallationManager $im, Pa $type, $fs, $this->container->get(InstallAndBuildProcess::class), + $this->container->get(UninstallProcess::class), $this->composerRequest, ); }; @@ -59,7 +63,7 @@ public static function createPieComposer( true, ); - OverrideWindowsUrlInstallListener::selfRegister($composer, $io, $container, $composerRequest); + OverrideDownloadUrlInstallListener::selfRegister($composer, $io, $container, $composerRequest); RemoveUnrelatedInstallOperations::selfRegister($composer, $composerRequest); $composer->getConfig()->merge(['config' => ['__PIE_REQUEST__' => $composerRequest]]); @@ -68,6 +72,21 @@ public static function createPieComposer( return $composer; } + protected function purgePackages(InstalledRepositoryInterface $repo, Installer\InstallationManager $im): void + { + /** + * This is intentionally a no-op in PIE.... + * + * Why not purge packages? + * + * We have a post install job in {@see VendorCleanup} that cleans up the vendor directory to remove all the + * actual package files; however, this means that Composer thinks they are not installed after that. When + * creating the Composer instance, the last step is to purge packages from the + * {@see InstalledRepositoryInterface} if they no longer exist on disk. But, that means we can't list the + * packages installed with PIE any more! So, we override this method to become a no-op ✅ + */ + } + public static function recreatePieComposer( ContainerInterface $container, Composer $existingComposer, diff --git a/src/ComposerIntegration/PieJsonEditor.php b/src/ComposerIntegration/PieJsonEditor.php index 973bfbff..475bfc47 100644 --- a/src/ComposerIntegration/PieJsonEditor.php +++ b/src/ComposerIntegration/PieJsonEditor.php @@ -82,6 +82,26 @@ public function addRequire(string $package, string $version): string return $originalPieJsonContent; } + /** + * Remove a package from the `require` section of the given `pie.json`. + * Returns the original `pie.json` content, in case it needs to be + * restored later. + * + * @param non-empty-string $package + */ + public function removeRequire(string $package): string + { + $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + + (new JsonConfigSource( + new JsonFile( + $this->pieJsonFilename, + ), + ))->removeLink('require', $package); + + return $originalPieJsonContent; + } + public function revert(string $originalPieJsonContent): void { file_put_contents($this->pieJsonFilename, $originalPieJsonContent); diff --git a/src/ComposerIntegration/PieOperation.php b/src/ComposerIntegration/PieOperation.php index a3345d51..1967ab2e 100644 --- a/src/ComposerIntegration/PieOperation.php +++ b/src/ComposerIntegration/PieOperation.php @@ -11,6 +11,7 @@ enum PieOperation case Download; case Build; case Install; + case Uninstall; public function shouldBuild(): bool { diff --git a/src/ComposerIntegration/PiePackageInstaller.php b/src/ComposerIntegration/PiePackageInstaller.php index d2185cc9..66cdc766 100644 --- a/src/ComposerIntegration/PiePackageInstaller.php +++ b/src/ComposerIntegration/PiePackageInstaller.php @@ -25,6 +25,7 @@ public function __construct( ExtensionType $type, Filesystem $filesystem, private readonly InstallAndBuildProcess $installAndBuildProcess, + private readonly UninstallProcess $uninstallProcess, private readonly PieComposerRequest $composerRequest, ) { parent::__construct($io, $composer, $type->value, $filesystem); @@ -71,4 +72,45 @@ public function install(InstalledRepositoryInterface $repo, PackageInterface $pa return null; }); } + + /** @inheritDoc */ + public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) + { + $composerPackage = $package; + + // @todo check into why not being removed from `vendor/composer/installed.json` + return parent::uninstall($repo, $composerPackage) + ?->then(function () use ($composerPackage) { + $output = $this->composerRequest->pieOutput; + + if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { + $output->writeln( + sprintf( + 'Skipping %s uninstall request from Composer as it was not the expected PIE package %s', + $composerPackage->getName(), + $this->composerRequest->requestedPackage->package, + ), + OutputInterface::VERBOSITY_VERY_VERBOSE, + ); + + return null; + } + + if (! $composerPackage instanceof CompletePackage) { + $output->writeln(sprintf( + 'Not using PIE to install %s as it was not a Complete Package', + $composerPackage->getName(), + )); + + return null; + } + + ($this->uninstallProcess)( + $this->composerRequest, + $composerPackage, + ); + + return null; + }); + } } diff --git a/src/ComposerIntegration/UninstallProcess.php b/src/ComposerIntegration/UninstallProcess.php new file mode 100644 index 00000000..6f4d747a --- /dev/null +++ b/src/ComposerIntegration/UninstallProcess.php @@ -0,0 +1,58 @@ +pieOutput; + + $piePackage = Package::fromComposerCompletePackage($composerPackage); + + $affectedIniFiles = ($this->removeIniEntry)($piePackage, $composerRequest->targetPlatform, $output); + + if (count($affectedIniFiles) === 1) { + $output->writeln( + sprintf('INI file "%s" was updated to remove the extension.', reset($affectedIniFiles)), + OutputInterface::VERBOSITY_VERBOSE, + ); + } elseif (count($affectedIniFiles) === 0) { + $output->writeln( + 'No INI files were updated to remove the extension.', + OutputInterface::VERBOSITY_VERBOSE, + ); + } else { + $output->writeln( + 'The following INI files were updated to remove the extnesion:', + OutputInterface::VERBOSITY_VERBOSE, + ); + array_walk($affectedIniFiles, static fn (string $ini) => $output->writeln(' - ' . $ini)); + } + + $output->writeln(sprintf('👋 Removed extension: %s', ($this->uninstall)($piePackage)->filePath)); + } +} diff --git a/src/Container.php b/src/Container.php index 6f8ef418..351db890 100644 --- a/src/Container.php +++ b/src/Container.php @@ -17,6 +17,7 @@ use Php\Pie\Command\RepositoryListCommand; use Php\Pie\Command\RepositoryRemoveCommand; use Php\Pie\Command\ShowCommand; +use Php\Pie\Command\UninstallCommand; use Php\Pie\ComposerIntegration\MinimalHelperSet; use Php\Pie\ComposerIntegration\QuieterConsoleIO; use Php\Pie\DependencyResolver\DependencyResolver; @@ -25,6 +26,8 @@ use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Installing\Ini; use Php\Pie\Installing\Install; +use Php\Pie\Installing\Uninstall; +use Php\Pie\Installing\UninstallUsingUnlink; use Php\Pie\Installing\UnixInstall; use Php\Pie\Installing\WindowsInstall; use Psr\Container\ContainerInterface; @@ -52,6 +55,7 @@ public static function factory(): ContainerInterface $container->singleton(RepositoryListCommand::class); $container->singleton(RepositoryAddCommand::class); $container->singleton(RepositoryRemoveCommand::class); + $container->singleton(UninstallCommand::class); $container->singleton(QuieterConsoleIO::class, static function (ContainerInterface $container): QuieterConsoleIO { return new QuieterConsoleIO( @@ -107,6 +111,10 @@ static function (ContainerInterface $container): Install { }, ); + $container->alias(UninstallUsingUnlink::class, Uninstall::class); + + $container->alias(Ini\RemoveIniEntryWithFileGetContents::class, Ini\RemoveIniEntry::class); + return $container; } } diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index d09a8a0d..2e4d31e8 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -7,6 +7,7 @@ use Composer\Package\CompletePackageInterface; use InvalidArgumentException; use Php\Pie\ConfigureOption; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; use Php\Pie\Platform\OperatingSystemFamily; @@ -26,53 +27,56 @@ * @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks * * @immutable + * + * @psalm-suppress PropertyNotSetInConstructor */ final class Package { - /** - * @param list $configureOptions - * @param non-empty-list|null $compatibleOsFamilies - * @param non-empty-list|null $incompatibleOsFamilies - */ + /** @var list */ + private array $configureOptions = []; + private int $priority = 80; + private string|null $buildPath = null; + /** @var non-empty-list|null */ + private array|null $compatibleOsFamilies = null; + /** @var non-empty-list|null */ + private array|null $incompatibleOsFamilies = null; + private bool $supportZts = true; + private bool $supportNts = true; + private DownloadUrlMethod|null $downloadUrlMethod = null; + public function __construct( - public readonly CompletePackageInterface $composerPackage, - public readonly ExtensionType $extensionType, - public readonly ExtensionName $extensionName, - public readonly string $name, - public readonly string $version, - public readonly string|null $downloadUrl, - public readonly array $configureOptions, - public readonly bool $supportZts, - public readonly bool $supportNts, - public readonly string|null $buildPath, - public readonly array|null $compatibleOsFamilies, - public readonly array|null $incompatibleOsFamilies, - public readonly int $priority, + private readonly CompletePackageInterface $composerPackage, + private readonly ExtensionType $extensionType, + private readonly ExtensionName $extensionName, + private readonly string $name, + private readonly string $version, + private readonly string|null $downloadUrl, ) { } public static function fromComposerCompletePackage(CompletePackageInterface $completePackage): self { + $package = new self( + $completePackage, + ExtensionType::tryFrom($completePackage->getType()) ?? ExtensionType::PhpModule, + ExtensionName::determineFromComposerPackage($completePackage), + $completePackage->getPrettyName(), + $completePackage->getPrettyVersion(), + $completePackage->getDistUrl(), + ); + $phpExtOptions = $completePackage->getPhpExt(); - $configureOptions = $phpExtOptions !== null && array_key_exists('configure-options', $phpExtOptions) + $package->configureOptions = $phpExtOptions !== null && array_key_exists('configure-options', $phpExtOptions) ? array_map( static fn (array $configureOption): ConfigureOption => ConfigureOption::fromComposerJsonDefinition($configureOption), $phpExtOptions['configure-options'], ) : []; - $supportZts = $phpExtOptions !== null && array_key_exists('support-zts', $phpExtOptions) - ? $phpExtOptions['support-zts'] - : true; - - $supportNts = $phpExtOptions !== null && array_key_exists('support-nts', $phpExtOptions) - ? $phpExtOptions['support-nts'] - : true; - - $buildPath = $phpExtOptions !== null && array_key_exists('build-path', $phpExtOptions) - ? $phpExtOptions['build-path'] - : null; + $package->supportZts = $phpExtOptions['support-zts'] ?? true; + $package->supportNts = $phpExtOptions['support-nts'] ?? true; + $package->buildPath = $phpExtOptions['build-path'] ?? null; $compatibleOsFamilies = $phpExtOptions['os-families'] ?? null; $incompatibleOsFamilies = $phpExtOptions['os-families-exclude'] ?? null; @@ -81,21 +85,16 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com throw new InvalidArgumentException('Cannot specify both "os-families" and "os-families-exclude" in composer.json'); } - return new self( - $completePackage, - ExtensionType::tryFrom($completePackage->getType()) ?? ExtensionType::PhpModule, - ExtensionName::determineFromComposerPackage($completePackage), - $completePackage->getPrettyName(), - $completePackage->getPrettyVersion(), - $completePackage->getDistUrl(), - $configureOptions, - $supportZts, - $supportNts, - $buildPath, - self::convertInputStringsToOperatingSystemFamilies($compatibleOsFamilies), - self::convertInputStringsToOperatingSystemFamilies($incompatibleOsFamilies), - $phpExtOptions['priority'] ?? 80, - ); + $package->compatibleOsFamilies = self::convertInputStringsToOperatingSystemFamilies($compatibleOsFamilies); + $package->incompatibleOsFamilies = self::convertInputStringsToOperatingSystemFamilies($incompatibleOsFamilies); + + $package->priority = $phpExtOptions['priority'] ?? 80; + + if ($phpExtOptions !== null && array_key_exists('download-url-method', $phpExtOptions)) { + $package->downloadUrlMethod = DownloadUrlMethod::tryFrom($phpExtOptions['download-url-method']); + } + + return $package; } public function prettyNameAndVersion(): string @@ -133,17 +132,92 @@ private static function convertInputStringsToOperatingSystemFamilies(array|null return null; } - $osFamilies = []; - foreach ($input as $value) { - $valueToTry = strtolower($value); + Assert::isNonEmptyList($input, 'Expected operating systems families to be a non-empty list.'); - Assert::inArray($valueToTry, OperatingSystemFamily::asValuesList(), 'Expected operating system family to be one of: %2$s. Got: %s'); + return array_map( + static function ($value): OperatingSystemFamily { + Assert::inArray( + strtolower($value), + OperatingSystemFamily::asValuesList(), + 'Expected operating system family to be one of: %2$s. Got: %s', + ); - $osFamilies[] = OperatingSystemFamily::from($valueToTry); - } + return OperatingSystemFamily::from(strtolower($value)); + }, + $input, + ); + } + + public function composerPackage(): CompletePackageInterface + { + return $this->composerPackage; + } - Assert::isNonEmptyList($osFamilies, 'Expected operating systems families to be a non-empty list.'); + public function extensionType(): ExtensionType + { + return $this->extensionType; + } - return $osFamilies; + public function extensionName(): ExtensionName + { + return $this->extensionName; + } + + public function name(): string + { + return $this->name; + } + + public function version(): string + { + return $this->version; + } + + /** @return list */ + public function configureOptions(): array + { + return $this->configureOptions; + } + + public function downloadUrl(): string|null + { + return $this->downloadUrl; + } + + public function priority(): int + { + return $this->priority; + } + + public function buildPath(): string|null + { + return $this->buildPath; + } + + /** @return non-empty-list|null */ + public function compatibleOsFamilies(): array|null + { + return $this->compatibleOsFamilies; + } + + /** @return non-empty-list|null */ + public function incompatibleOsFamilies(): array|null + { + return $this->incompatibleOsFamilies; + } + + public function supportZts(): bool + { + return $this->supportZts; + } + + public function supportNts(): bool + { + return $this->supportNts; + } + + public function downloadUrlMethod(): DownloadUrlMethod|null + { + return $this->downloadUrlMethod; } } diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index d2bdbd28..9e4fe603 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -70,27 +70,27 @@ public function __invoke( private function assertCompatibleThreadSafetyMode(ThreadSafetyMode $threadSafetyMode, Package $resolvedPackage): void { - if ($threadSafetyMode === ThreadSafetyMode::NonThreadSafe && ! $resolvedPackage->supportNts) { + if ($threadSafetyMode === ThreadSafetyMode::NonThreadSafe && ! $resolvedPackage->supportNts()) { throw IncompatibleThreadSafetyMode::ztsExtensionOnNtsPlatform(); } - if ($threadSafetyMode === ThreadSafetyMode::ThreadSafe && ! $resolvedPackage->supportZts) { + if ($threadSafetyMode === ThreadSafetyMode::ThreadSafe && ! $resolvedPackage->supportZts()) { throw IncompatibleThreadSafetyMode::ntsExtensionOnZtsPlatform(); } } private function assertCompatibleOsFamily(TargetPlatform $targetPlatform, Package $resolvedPackage): void { - if ($resolvedPackage->compatibleOsFamilies !== null && ! in_array($targetPlatform->operatingSystemFamily, $resolvedPackage->compatibleOsFamilies, true)) { + if ($resolvedPackage->compatibleOsFamilies() !== null && ! in_array($targetPlatform->operatingSystemFamily, $resolvedPackage->compatibleOsFamilies(), true)) { throw IncompatibleOperatingSystemFamily::notInCompatibleOperatingSystemFamilies( - $resolvedPackage->compatibleOsFamilies, + $resolvedPackage->compatibleOsFamilies(), $targetPlatform->operatingSystemFamily, ); } - if ($resolvedPackage->incompatibleOsFamilies !== null && in_array($targetPlatform->operatingSystemFamily, $resolvedPackage->incompatibleOsFamilies, true)) { + if ($resolvedPackage->incompatibleOsFamilies() !== null && in_array($targetPlatform->operatingSystemFamily, $resolvedPackage->incompatibleOsFamilies(), true)) { throw IncompatibleOperatingSystemFamily::inIncompatibleOperatingSystemFamily( - $resolvedPackage->incompatibleOsFamilies, + $resolvedPackage->incompatibleOsFamilies(), $targetPlatform->operatingSystemFamily, ); } diff --git a/src/Downloading/DownloadUrlMethod.php b/src/Downloading/DownloadUrlMethod.php new file mode 100644 index 00000000..397de170 --- /dev/null +++ b/src/Downloading/DownloadUrlMethod.php @@ -0,0 +1,50 @@ +|null */ + public function possibleAssetNames(Package $package, TargetPlatform $targetPlatform): array|null + { + return match ($this) { + self::WindowsBinaryDownload => WindowsExtensionAssetName::zipNames($targetPlatform, $package), + self::PrePackagedSourceDownload => PrePackagedSourceAssetName::packageNames($package), + self::ComposerDefaultDownload => null, + }; + } + + public static function fromPackage(Package $package, TargetPlatform $targetPlatform): self + { + /** + * PIE does not support building on Windows (yet, at least). Maintainers + * should provide pre-built Windows binaries. + */ + if ($targetPlatform->operatingSystem === OperatingSystem::Windows) { + return self::WindowsBinaryDownload; + } + + /** + * Some packages pre-package source code (e.g. mongodb) as there are + * external dependencies in Git submodules that otherwise aren't + * included in GitHub/Gitlab/etc "dist" downloads + */ + if ($package->downloadUrlMethod() === DownloadUrlMethod::PrePackagedSourceDownload) { + return self::PrePackagedSourceDownload; + } + + return self::ComposerDefaultDownload; + } +} diff --git a/src/Downloading/DownloadedPackage.php b/src/Downloading/DownloadedPackage.php index 3f6adeb8..7836d3d0 100644 --- a/src/Downloading/DownloadedPackage.php +++ b/src/Downloading/DownloadedPackage.php @@ -8,6 +8,7 @@ use function is_string; use function realpath; +use function str_replace; use const DIRECTORY_SEPARATOR; @@ -26,8 +27,12 @@ private function __construct( public static function fromPackageAndExtractedPath(Package $package, string $extractedSourcePath): self { - if ($package->buildPath !== null) { - $extractedSourcePathWithBuildPath = realpath($extractedSourcePath . DIRECTORY_SEPARATOR . $package->buildPath); + if ($package->buildPath() !== null) { + $extractedSourcePathWithBuildPath = realpath( + $extractedSourcePath + . DIRECTORY_SEPARATOR + . str_replace('{version}', $package->version(), $package->buildPath()), + ); if (is_string($extractedSourcePathWithBuildPath)) { $extractedSourcePath = $extractedSourcePathWithBuildPath; diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php index 69f338d0..4b526ae0 100644 --- a/src/Downloading/GithubPackageReleaseAssets.php +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -9,7 +9,6 @@ use Composer\Util\HttpDownloader; use Php\Pie\DependencyResolver\Package; use Php\Pie\Platform\TargetPlatform; -use Php\Pie\Platform\WindowsExtensionAssetName; use Webmozart\Assert\Assert; use function array_map; @@ -25,47 +24,48 @@ public function __construct( ) { } - /** @return non-empty-string */ - public function findWindowsDownloadUrlForPackage( + /** + * @param non-empty-list $possibleReleaseAssetNames + * + * @return non-empty-string + */ + public function findMatchingReleaseAssetUrl( TargetPlatform $targetPlatform, Package $package, AuthHelper $authHelper, HttpDownloader $httpDownloader, + array $possibleReleaseAssetNames, ): string { $releaseAsset = $this->selectMatchingReleaseAsset( - $targetPlatform, $package, $this->getReleaseAssetsForPackage($package, $authHelper, $httpDownloader), + $possibleReleaseAssetNames, ); return $releaseAsset['browser_download_url']; } - /** @return non-empty-list */ - private function expectedWindowsAssetNames(TargetPlatform $targetPlatform, Package $package): array - { - return WindowsExtensionAssetName::zipNames($targetPlatform, $package); - } - /** @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3734 */ // phpcs:disable Squiz.Commenting.FunctionComment.MissingParamName /** * @param list $releaseAssets + * @param non-empty-list $possibleReleaseAssetNames * * @return array{name: non-empty-string, browser_download_url: non-empty-string, ...} */ // phpcs:enable - private function selectMatchingReleaseAsset(TargetPlatform $targetPlatform, Package $package, array $releaseAssets): array - { - $expectedAssetNames = $this->expectedWindowsAssetNames($targetPlatform, $package); - + private function selectMatchingReleaseAsset( + Package $package, + array $releaseAssets, + array $possibleReleaseAssetNames, + ): array { foreach ($releaseAssets as $releaseAsset) { - if (in_array(strtolower($releaseAsset['name']), $expectedAssetNames, true)) { + if (in_array(strtolower($releaseAsset['name']), $possibleReleaseAssetNames, true)) { return $releaseAsset; } } - throw Exception\CouldNotFindReleaseAsset::forPackage($package, $expectedAssetNames); + throw Exception\CouldNotFindReleaseAsset::forPackage($package, $possibleReleaseAssetNames); } /** @return list */ @@ -74,16 +74,16 @@ private function getReleaseAssetsForPackage( AuthHelper $authHelper, HttpDownloader $httpDownloader, ): array { - Assert::notNull($package->downloadUrl); + Assert::notNull($package->downloadUrl()); try { $decodedRepsonse = $httpDownloader->get( - $this->githubApiBaseUrl . '/repos/' . $package->githubOrgAndRepository() . '/releases/tags/' . $package->version, + $this->githubApiBaseUrl . '/repos/' . $package->githubOrgAndRepository() . '/releases/tags/' . $package->version(), [ 'retry-auth-failure' => false, 'http' => [ 'method' => 'GET', - 'header' => $authHelper->addAuthenticationHeader([], $this->githubApiBaseUrl, $package->downloadUrl), + 'header' => $authHelper->addAuthenticationHeader([], $this->githubApiBaseUrl, $package->downloadUrl()), ], ], )->decodeJson(); diff --git a/src/Downloading/PackageReleaseAssets.php b/src/Downloading/PackageReleaseAssets.php index 5f79cc37..4e498858 100644 --- a/src/Downloading/PackageReleaseAssets.php +++ b/src/Downloading/PackageReleaseAssets.php @@ -12,11 +12,16 @@ /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ interface PackageReleaseAssets { - /** @return non-empty-string */ - public function findWindowsDownloadUrlForPackage( + /** + * @param non-empty-list $possibleReleaseAssetNames + * + * @return non-empty-string + */ + public function findMatchingReleaseAssetUrl( TargetPlatform $targetPlatform, Package $package, AuthHelper $authHelper, HttpDownloader $httpDownloader, + array $possibleReleaseAssetNames, ): string; } diff --git a/src/BinaryFile.php b/src/File/BinaryFile.php similarity index 50% rename from src/BinaryFile.php rename to src/File/BinaryFile.php index 54611187..db3ae942 100644 --- a/src/BinaryFile.php +++ b/src/File/BinaryFile.php @@ -2,8 +2,11 @@ declare(strict_types=1); -namespace Php\Pie; +namespace Php\Pie\File; +use Php\Pie\Util; + +use function file_exists; use function hash_file; /** @@ -33,4 +36,25 @@ public static function fromFileWithSha256Checksum(string $filePath): self hash_file(self::HASH_TYPE_SHA256, $filePath), ); } + + public function verify(): void + { + if (! file_exists($this->filePath)) { + throw Util\FileNotFound::fromFilename($this->filePath); + } + + self::verifyAgainstOther(self::fromFileWithSha256Checksum($this->filePath)); + } + + /** @throws BinaryFileFailedVerification */ + public function verifyAgainstOther(self $other): void + { + if ($this->filePath !== $other->filePath) { + throw BinaryFileFailedVerification::fromFilenameMismatch($this, $other); + } + + if ($other->checksum !== $this->checksum) { + throw BinaryFileFailedVerification::fromChecksumMismatch($this, $other); + } + } } diff --git a/src/File/BinaryFileFailedVerification.php b/src/File/BinaryFileFailedVerification.php new file mode 100644 index 00000000..7dd141bb --- /dev/null +++ b/src/File/BinaryFileFailedVerification.php @@ -0,0 +1,32 @@ +filePath, + $actual->filePath, + )); + } + + public static function fromChecksumMismatch(BinaryFile $expected, BinaryFile $actual): self + { + return new self(sprintf( + 'File "%s" failed checksum verification. Expected %s..., was %s...', + $expected->filePath, + substr($expected->checksum, 0, 8), + substr($actual->checksum, 0, 8), + )); + } +} diff --git a/src/File/FailedToWriteFile.php b/src/File/FailedToWriteFile.php new file mode 100644 index 00000000..b856ea48 --- /dev/null +++ b/src/File/FailedToWriteFile.php @@ -0,0 +1,26 @@ +find('sudo'); + + if ($sudo === null || $sudo === '') { + throw SudoNotFoundOnSystem::new(); + } + + self::$memoizedSudo = $sudo; + } + + return self::$memoizedSudo; + } + + public static function exists(): bool + { + try { + self::find(); + + return true; + } catch (Throwable) { + return false; + } + } +} diff --git a/src/File/SudoFilePut.php b/src/File/SudoFilePut.php new file mode 100644 index 00000000..9f869b55 --- /dev/null +++ b/src/File/SudoFilePut.php @@ -0,0 +1,60 @@ + file_put_contents($filename, $content), + $capturedErrors, + ); + + if ($writeSuccessful === false) { + throw FailedToWriteFile::fromFilePutContentErrors($filename, $capturedErrors); + } + + if (! $didChangePermissions || ! Sudo::exists()) { + return; + } + + Process::run([Sudo::find(), 'chmod', $previousPermissions, $filename]); + } + + private static function attemptToMakeFileEditable(string $filename): bool + { + if (! Sudo::exists()) { + return false; + } + + if (! is_writable($filename)) { + try { + Process::run([Sudo::find(), 'chmod', '0777', $filename]); + + return true; + } catch (ProcessFailedException) { + return false; + } + } + + return false; + } +} diff --git a/src/File/SudoNotFoundOnSystem.php b/src/File/SudoNotFoundOnSystem.php new file mode 100644 index 00000000..e3818aa3 --- /dev/null +++ b/src/File/SudoNotFoundOnSystem.php @@ -0,0 +1,15 @@ +filePath, + )); + } +} diff --git a/src/Installing/Ini/AddExtensionToTheIniFile.php b/src/Installing/Ini/AddExtensionToTheIniFile.php index 73469e9e..f4cece32 100644 --- a/src/Installing/Ini/AddExtensionToTheIniFile.php +++ b/src/Installing/Ini/AddExtensionToTheIniFile.php @@ -63,7 +63,7 @@ public function __invoke( $output->writeln( sprintf( 'Enabled extension %s in the INI file %s', - $package->extensionName->name(), + $package->extensionName()->name(), $ini, ), OutputInterface::VERBOSITY_VERBOSE, @@ -73,7 +73,7 @@ public function __invoke( return false; } - $phpBinaryPath->assertExtensionIsLoadedInRuntime($package->extensionName, $output); + $phpBinaryPath->assertExtensionIsLoadedInRuntime($package->extensionName(), $output); return true; } catch (Throwable $anything) { @@ -81,7 +81,7 @@ public function __invoke( $output->writeln(sprintf( 'Something went wrong enabling the %s extension: %s', - $package->extensionName->name(), + $package->extensionName()->name(), $anything->getMessage(), )); @@ -93,10 +93,10 @@ public function __invoke( private function iniFileContent(Package $package): string { return PHP_EOL - . '; PIE automatically added this to enable the ' . $package->name . ' extension' . PHP_EOL - . '; priority=' . $package->priority . PHP_EOL - . ($package->extensionType === ExtensionType::PhpModule ? 'extension' : 'zend_extension') + . '; PIE automatically added this to enable the ' . $package->name() . ' extension' . PHP_EOL + . '; priority=' . $package->priority() . PHP_EOL + . ($package->extensionType() === ExtensionType::PhpModule ? 'extension' : 'zend_extension') . '=' - . $package->extensionName->name() . PHP_EOL; + . $package->extensionName()->name() . PHP_EOL; } } diff --git a/src/Installing/Ini/CheckAndAddExtensionToIniIfNeeded.php b/src/Installing/Ini/CheckAndAddExtensionToIniIfNeeded.php index eec39db0..1c3a57e7 100644 --- a/src/Installing/Ini/CheckAndAddExtensionToIniIfNeeded.php +++ b/src/Installing/Ini/CheckAndAddExtensionToIniIfNeeded.php @@ -45,7 +45,7 @@ public function __invoke( return false; } - if (($this->isExtensionAlreadyInTheIniFile)($iniFile, $downloadedPackage->package->extensionName)) { + if (($this->isExtensionAlreadyInTheIniFile)($iniFile, $downloadedPackage->package->extensionName())) { $output->writeln( sprintf( 'Extension is already enabled in the INI file %s', @@ -59,13 +59,13 @@ public function __invoke( } try { - $targetPlatform->phpBinaryPath->assertExtensionIsLoadedInRuntime($downloadedPackage->package->extensionName, $output); + $targetPlatform->phpBinaryPath->assertExtensionIsLoadedInRuntime($downloadedPackage->package->extensionName(), $output); return true; } catch (Throwable $anything) { $output->writeln(sprintf( 'Something went wrong verifying the %s extension is enabled: %s', - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionName()->name(), $anything->getMessage(), )); diff --git a/src/Installing/Ini/DockerPhpExtEnable.php b/src/Installing/Ini/DockerPhpExtEnable.php index 2878af4d..588a5e02 100644 --- a/src/Installing/Ini/DockerPhpExtEnable.php +++ b/src/Installing/Ini/DockerPhpExtEnable.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing\Ini; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPhp\Exception\ExtensionIsNotLoaded; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Util\Process; @@ -41,12 +41,12 @@ public function setup( } try { - $enableOutput = Process::run([$dockerPhpExtEnable, $downloadedPackage->package->extensionName->name()]); + $enableOutput = Process::run([$dockerPhpExtEnable, $downloadedPackage->package->extensionName()->name()]); } catch (ProcessFailedException $processFailed) { $output->writeln( sprintf( 'Could not enable extension %s using %s. Exception was: %s', - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionName()->name(), $this->dockerPhpExtEnableName, $processFailed->getMessage(), ), @@ -58,7 +58,7 @@ public function setup( try { $targetPlatform->phpBinaryPath->assertExtensionIsLoadedInRuntime( - $downloadedPackage->package->extensionName, + $downloadedPackage->package->extensionName(), $output, ); @@ -67,7 +67,7 @@ public function setup( $output->writeln( sprintf( 'Asserting that extension %s was enabled using %s failed. Output was: %s', - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionName()->name(), $this->dockerPhpExtEnableName, $enableOutput !== '' ? $enableOutput : '(empty)', ), diff --git a/src/Installing/Ini/OndrejPhpenmod.php b/src/Installing/Ini/OndrejPhpenmod.php index 051dfb65..92bb5ebc 100644 --- a/src/Installing/Ini/OndrejPhpenmod.php +++ b/src/Installing/Ini/OndrejPhpenmod.php @@ -5,8 +5,8 @@ namespace Php\Pie\Installing\Ini; use Composer\Util\Platform; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Util\Process; use Symfony\Component\Console\Output\OutputInterface; @@ -119,7 +119,7 @@ public function setup( '%s%s%s.ini', rtrim($expectedModsAvailablePath, DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR, - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionName()->name(), ); $pieCreatedTheIniFile = false; @@ -148,7 +148,7 @@ static function () use ($phpenmodPath, $targetPlatform, $downloadedPackage, $out $targetPlatform->phpBinaryPath->majorMinorVersion(), '-s', 'ALL', - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionName()->name(), ]); return true; @@ -157,7 +157,7 @@ static function () use ($phpenmodPath, $targetPlatform, $downloadedPackage, $out sprintf( 'Failed to use %s to enable %s for PHP %s: %s', $phpenmodPath, - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionName()->name(), $targetPlatform->phpBinaryPath->majorMinorVersion(), $processFailedException->getMessage(), ), diff --git a/src/Installing/Ini/PickBestSetupIniApproach.php b/src/Installing/Ini/PickBestSetupIniApproach.php index 83c719d3..a122491e 100644 --- a/src/Installing/Ini/PickBestSetupIniApproach.php +++ b/src/Installing/Ini/PickBestSetupIniApproach.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing\Ini; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use ReflectionClass; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php b/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php index 89f36819..23c8b0ce 100644 --- a/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php +++ b/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing\Ini; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPhp\Exception\ExtensionIsNotLoaded; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Output\OutputInterface; @@ -26,7 +26,7 @@ public function setup( ): bool { try { $targetPlatform->phpBinaryPath->assertExtensionIsLoadedInRuntime( - $downloadedPackage->package->extensionName, + $downloadedPackage->package->extensionName(), $output, ); diff --git a/src/Installing/Ini/RemoveIniEntry.php b/src/Installing/Ini/RemoveIniEntry.php new file mode 100644 index 00000000..347c9168 --- /dev/null +++ b/src/Installing/Ini/RemoveIniEntry.php @@ -0,0 +1,16 @@ + Returns a list of INI files that were updated to remove the extension */ + public function __invoke(Package $package, TargetPlatform $targetPlatform, OutputInterface $output): array; +} diff --git a/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php b/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php new file mode 100644 index 00000000..6b4235c5 --- /dev/null +++ b/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php @@ -0,0 +1,106 @@ + Returns a list of INI files that were updated to remove the extension */ + public function __invoke(Package $package, TargetPlatform $targetPlatform, OutputInterface $output): array + { + $allIniFiles = []; + + $mainIni = $targetPlatform->phpBinaryPath->loadedIniConfigurationFile(); + if ($mainIni !== null) { + $allIniFiles[] = $mainIni; + } + + $additionalIniDirectory = $targetPlatform->phpBinaryPath->additionalIniDirectory(); + if ($additionalIniDirectory !== null) { + $allIniFiles = array_merge( + array_map( + static function (string $path) use ($additionalIniDirectory): string { + return $additionalIniDirectory . DIRECTORY_SEPARATOR . $path; + }, + array_filter( + scandir($additionalIniDirectory), + static function (string $path) use ($additionalIniDirectory): bool { + if (in_array($path, ['.', '..'])) { + return false; + } + + return file_exists($additionalIniDirectory . DIRECTORY_SEPARATOR . $path); + }, + ), + ), + $allIniFiles, + ); + } + + $regex = sprintf( + '/^(%s\w*=\w*%s)/m', + $package->extensionType() === ExtensionType::PhpModule ? 'extension' : 'zend_extension', + $package->extensionName()->name(), + ); + + $updatedIniFiles = []; + array_walk( + $allIniFiles, + static function (string $iniFile) use (&$updatedIniFiles, $regex, $package, $output): void { + $currentContent = file_get_contents($iniFile); + + if ($currentContent === false || $currentContent === '') { + return; + } + + $replacedContent = preg_replace( + $regex, + '; $1 ; removed by PIE', + $currentContent, + ); + + if ($replacedContent === null || $replacedContent === $currentContent) { + return; + } + + try { + SudoFilePut::contents($iniFile, $replacedContent); + } catch (FailedToWriteFile) { + $output->writeln(sprintf( + 'Failed to remove extension "%s" from INI file "%s"', + $package->extensionName()->name(), + $iniFile, + )); + + return; + } + + $updatedIniFiles[] = $iniFile; + }, + ); + + return $updatedIniFiles; + } +} diff --git a/src/Installing/Ini/SetupIniApproach.php b/src/Installing/Ini/SetupIniApproach.php index 37870e90..0e003e0d 100644 --- a/src/Installing/Ini/SetupIniApproach.php +++ b/src/Installing/Ini/SetupIniApproach.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing\Ini; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Installing/Ini/StandardAdditionalPhpIniDirectory.php b/src/Installing/Ini/StandardAdditionalPhpIniDirectory.php index 84a4b505..3202450e 100644 --- a/src/Installing/Ini/StandardAdditionalPhpIniDirectory.php +++ b/src/Installing/Ini/StandardAdditionalPhpIniDirectory.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing\Ini; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Output\OutputInterface; @@ -60,8 +60,8 @@ public function setup( '%s%s%d-%s.ini', rtrim($additionalIniFilesPath, DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR, - $downloadedPackage->package->priority, - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->priority(), + $downloadedPackage->package->extensionName()->name(), ); $pieCreatedTheIniFile = false; diff --git a/src/Installing/Ini/StandardSinglePhpIni.php b/src/Installing/Ini/StandardSinglePhpIni.php index 0eb046e8..decaf6da 100644 --- a/src/Installing/Ini/StandardSinglePhpIni.php +++ b/src/Installing/Ini/StandardSinglePhpIni.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing\Ini; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Installing/Install.php b/src/Installing/Install.php index 17982b41..0e1b3fee 100644 --- a/src/Installing/Install.php +++ b/src/Installing/Install.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Installing/PackageMetadataMissing.php b/src/Installing/PackageMetadataMissing.php new file mode 100644 index 00000000..50c5ff7a --- /dev/null +++ b/src/Installing/PackageMetadataMissing.php @@ -0,0 +1,33 @@ + $actualMetadata + * @param list $wantedKeys + */ + public static function duringUninstall(Package $package, array $actualMetadata, array $wantedKeys): self + { + $missingKeys = array_diff($wantedKeys, array_keys($actualMetadata)); + + return new self(sprintf( + 'PIE metadata was missing for package %s. Missing metadata key%s: %s', + $package->name(), + count($missingKeys) === 1 ? '' : 's', + implode(', ', $missingKeys), + )); + } +} diff --git a/src/Installing/SetupIniFile.php b/src/Installing/SetupIniFile.php index d6ca58bb..e169246c 100644 --- a/src/Installing/SetupIniFile.php +++ b/src/Installing/SetupIniFile.php @@ -4,9 +4,9 @@ namespace Php\Pie\Installing; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\SetupIniApproach; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Output\OutputInterface; @@ -45,8 +45,8 @@ public function __invoke( $output->writeln('⚠️ Extension has NOT been automatically enabled.'); $output->writeln(sprintf( 'You must now add "%s=%s" to your php.ini', - $downloadedPackage->package->extensionType === ExtensionType::PhpModule ? 'extension' : 'zend_extension', - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionType() === ExtensionType::PhpModule ? 'extension' : 'zend_extension', + $downloadedPackage->package->extensionName()->name(), )); } } diff --git a/src/Installing/Uninstall.php b/src/Installing/Uninstall.php new file mode 100644 index 00000000..1c72af83 --- /dev/null +++ b/src/Installing/Uninstall.php @@ -0,0 +1,14 @@ +composerPackage()); + + if ( + ! array_key_exists(PieInstalledJsonMetadataKeys::InstalledBinary->value, $pieMetadata) + || ! array_key_exists(PieInstalledJsonMetadataKeys::BinaryChecksum->value, $pieMetadata) + ) { + throw PackageMetadataMissing::duringUninstall( + $package, + $pieMetadata, + [ + PieInstalledJsonMetadataKeys::InstalledBinary->value, + PieInstalledJsonMetadataKeys::BinaryChecksum->value, + ], + ); + } + + $expectedBinaryFile = new BinaryFile( + $pieMetadata[PieInstalledJsonMetadataKeys::InstalledBinary->value], + $pieMetadata[PieInstalledJsonMetadataKeys::BinaryChecksum->value], + ); + + $expectedBinaryFile->verify(); + + // If the target directory isn't writable, or a .so file already exists and isn't writable, try to use sudo + if (file_exists($expectedBinaryFile->filePath) && ! is_writable($expectedBinaryFile->filePath)) { + Process::run(['sudo', 'rm', $expectedBinaryFile->filePath]); + + // Removal worked, bail out + if (! file_exists($expectedBinaryFile->filePath)) { + return $expectedBinaryFile; + } + } + + if (file_exists($expectedBinaryFile->filePath) && ! unlink($expectedBinaryFile->filePath)) { + throw FailedToRemoveExtension::withFilename($expectedBinaryFile); + } + + return $expectedBinaryFile; + } +} diff --git a/src/Installing/UnixInstall.php b/src/Installing/UnixInstall.php index da831eed..fa179ffb 100644 --- a/src/Installing/UnixInstall.php +++ b/src/Installing/UnixInstall.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Util\Process; use RuntimeException; @@ -33,7 +33,7 @@ public function __invoke( ): BinaryFile { $targetExtensionPath = $targetPlatform->phpBinaryPath->extensionPath(); - $sharedObjectName = $downloadedPackage->package->extensionName->name() . '.so'; + $sharedObjectName = $downloadedPackage->package->extensionName()->name() . '.so'; $expectedSharedObjectLocation = sprintf( '%s/%s', $targetExtensionPath, diff --git a/src/Installing/WindowsInstall.php b/src/Installing/WindowsInstall.php index 7ff5ef1d..23c8f2a9 100644 --- a/src/Installing/WindowsInstall.php +++ b/src/Installing/WindowsInstall.php @@ -4,9 +4,9 @@ namespace Php\Pie\Installing; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Platform\WindowsExtensionAssetName; use RecursiveDirectoryIterator; @@ -86,8 +86,8 @@ public function __invoke( */ $output->writeln(sprintf( 'You must now add "%s=%s" to your php.ini', - $downloadedPackage->package->extensionType === ExtensionType::PhpModule ? 'extension' : 'zend_extension', - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionType() === ExtensionType::PhpModule ? 'extension' : 'zend_extension', + $downloadedPackage->package->extensionName()->name(), )); $binaryFile = BinaryFile::fromFileWithSha256Checksum($destinationDllName); @@ -122,7 +122,7 @@ private function normalisedPathsMatch(string $first, string $second): bool private function copyExtensionDll(TargetPlatform $targetPlatform, DownloadedPackage $downloadedPackage, string $sourceDllName): string { $destinationDllName = $targetPlatform->phpBinaryPath->extensionPath() . DIRECTORY_SEPARATOR - . 'php_' . $downloadedPackage->package->extensionName->name() . '.dll'; + . 'php_' . $downloadedPackage->package->extensionName()->name() . '.dll'; if (! copy($sourceDllName, $destinationDllName) || ! file_exists($destinationDllName) && ! is_file($destinationDllName)) { throw new RuntimeException('Failed to install DLL to ' . $destinationDllName); @@ -190,7 +190,7 @@ private function copyExtraFile(TargetPlatform $targetPlatform, DownloadedPackage { $destinationFullFilename = dirname($targetPlatform->phpBinaryPath->phpBinaryPath) . DIRECTORY_SEPARATOR . 'extras' . DIRECTORY_SEPARATOR - . $downloadedPackage->package->extensionName->name() . DIRECTORY_SEPARATOR + . $downloadedPackage->package->extensionName()->name() . DIRECTORY_SEPARATOR . substr($file->getPathname(), strlen($downloadedPackage->extractedSourcePath) + 1); $destinationPath = dirname($destinationFullFilename); diff --git a/src/Platform/InstalledPiePackages.php b/src/Platform/InstalledPiePackages.php new file mode 100644 index 00000000..d303d9a8 --- /dev/null +++ b/src/Platform/InstalledPiePackages.php @@ -0,0 +1,57 @@ + + */ +class InstalledPiePackages +{ + /** + * Returns a list of PIE packages according to PIE; this does NOT check if + * the extension is actually enabled in the target PHP. + * + * @return ListOfPiePackages + */ + public function allPiePackages(Composer $composer): array + { + $composerInstalledPackages = array_map( + static function (CompletePackageInterface $package): Package { + return Package::fromComposerCompletePackage($package); + }, + array_filter( + $composer + ->getRepositoryManager() + ->getLocalRepository() + ->getPackages(), + static function (BasePackage $basePackage): bool { + return $basePackage instanceof CompletePackageInterface; + }, + ), + ); + + return array_combine( + array_map( + /** @return non-empty-string */ + static function (Package $package): string { + return $package->extensionName()->name(); + }, + $composerInstalledPackages, + ), + $composerInstalledPackages, + ); + } +} diff --git a/src/Platform/PrePackagedSourceAssetName.php b/src/Platform/PrePackagedSourceAssetName.php new file mode 100644 index 00000000..3261b690 --- /dev/null +++ b/src/Platform/PrePackagedSourceAssetName.php @@ -0,0 +1,36 @@ + */ + public static function packageNames(Package $package): array + { + return [ + strtolower(sprintf( + 'php_%s-%s-src.tgz', + $package->extensionName()->name(), + $package->version(), + )), + strtolower(sprintf( + 'php_%s-%s-src.zip', + $package->extensionName()->name(), + $package->version(), + )), + ]; + } +} diff --git a/src/Platform/WindowsExtensionAssetName.php b/src/Platform/WindowsExtensionAssetName.php index f2383c7e..9fe38f7f 100644 --- a/src/Platform/WindowsExtensionAssetName.php +++ b/src/Platform/WindowsExtensionAssetName.php @@ -36,8 +36,8 @@ private static function assetNames(TargetPlatform $targetPlatform, Package $pack return [ strtolower(sprintf( 'php_%s-%s-%s-%s-%s-%s.%s', - $package->extensionName->name(), - $package->version, + $package->extensionName()->name(), + $package->version(), $targetPlatform->phpBinaryPath->majorMinorVersion(), $targetPlatform->threadSafety->asShort(), strtolower($targetPlatform->windowsCompiler->name), @@ -46,8 +46,8 @@ private static function assetNames(TargetPlatform $targetPlatform, Package $pack )), strtolower(sprintf( 'php_%s-%s-%s-%s-%s-%s.%s', - $package->extensionName->name(), - $package->version, + $package->extensionName()->name(), + $package->version(), $targetPlatform->phpBinaryPath->majorMinorVersion(), strtolower($targetPlatform->windowsCompiler->name), $targetPlatform->threadSafety->asShort(), diff --git a/src/Util/CaptureErrors.php b/src/Util/CaptureErrors.php new file mode 100644 index 00000000..4d706a48 --- /dev/null +++ b/src/Util/CaptureErrors.php @@ -0,0 +1,44 @@ + + */ +final class CaptureErrors +{ + /** + * @param callable():T $code + * @param CapturedErrorList $captured + * + * @return T + * + * @template T + */ + public static function for(callable $code, array &$captured): mixed + { + set_error_handler(static function (int $level, string $message, string $filename, int $line) use (&$captured): bool { + $captured[] = [ + 'level' => $level, + 'message' => $message, + 'filename' => $filename, + 'line' => $line, + ]; + + return true; + }); + + $returnValue = $code(); + + restore_error_handler(); + + return $returnValue; + } +} diff --git a/src/Util/FileNotFound.php b/src/Util/FileNotFound.php new file mode 100644 index 00000000..445bfe50 --- /dev/null +++ b/src/Util/FileNotFound.php @@ -0,0 +1,20 @@ +runPieCommand(['install', 'asgrim/example-pie-extension']); } + #[When('I run a command to uninstall an extension')] + public function iRunACommandToUninstallAnExtension(): void + { + $this->runPieCommand(['uninstall', 'asgrim/example-pie-extension']); + } + + #[Then('the extension should not be installed anymore')] + public function theExtensionShouldNotBeInstalled(): void + { + $this->assertCommandSuccessful(); + + if (Platform::isWindows()) { + Assert::regex($this->output, '#👋 Removed extension: [-\\\_:.a-zA-Z0-9]+\\\php_example_pie_extension.dll#'); + } else { + Assert::regex($this->output, '#👋 Removed extension: [-_a-zA-Z0-9/]+/example_pie_extension.so#'); + } + + $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("example_pie_extension")?"yes":"no";'])) + ->mustRun() + ->getOutput(); + + Assert::same($isExtEnabled, 'no'); + } + #[Then('the extension should have been installed')] public function theExtensionShouldHaveBeenInstalled(): void { @@ -138,7 +163,7 @@ public function theExtensionShouldHaveBeenInstalled(): void ->mustRun() ->getOutput(); - Assert::same('yes', $isExtEnabled); + Assert::same($isExtEnabled, 'yes'); } #[Given('I have an invalid extension installed')] diff --git a/test/integration/Building/UnixBuildTest.php b/test/integration/Building/UnixBuildTest.php index 976ca7a0..1d6daa6c 100644 --- a/test/integration/Building/UnixBuildTest.php +++ b/test/integration/Building/UnixBuildTest.php @@ -8,7 +8,6 @@ use Composer\Util\Platform; use Php\Pie\Building\ExtensionBinaryNotFound; use Php\Pie\Building\UnixBuild; -use Php\Pie\ConfigureOption; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; @@ -44,13 +43,6 @@ public function testUnixBuildCanBuildExtension(): void 'pie_test_ext', '0.1.0', null, - [ConfigureOption::fromComposerJsonDefinition(['name' => 'enable-pie_test_ext'])], - true, - true, - null, - null, - null, - 99, ), self::TEST_EXTENSION_PATH, ); @@ -100,13 +92,6 @@ public function testUnixBuildWillThrowExceptionWhenExpectedBinaryNameMismatches( 'pie_test_ext', '0.1.0', null, - [ConfigureOption::fromComposerJsonDefinition(['name' => 'enable-pie_test_ext'])], - true, - true, - null, - null, - null, - 99, ), self::TEST_EXTENSION_PATH, ); @@ -136,22 +121,14 @@ public function testUnixBuildCanBuildExtensionWithBuildPath(): void $output = new BufferedOutput(); + $composerPackage = $this->createMock(CompletePackage::class); + $composerPackage->method('getPrettyName')->willReturn('myvendor/pie_test_ext'); + $composerPackage->method('getPrettyVersion')->willReturn('0.1.0'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn(['build-path' => 'pie_test_ext']); + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( - new Package( - $this->createMock(CompletePackage::class), - ExtensionType::PhpModule, - ExtensionName::normaliseFromString('pie_test_ext'), - 'pie_test_ext', - '0.1.0', - null, - [ConfigureOption::fromComposerJsonDefinition(['name' => 'enable-pie_test_ext'])], - true, - true, - 'pie_test_ext', - null, - null, - 99, - ), + Package::fromComposerCompletePackage($composerPackage), dirname(self::TEST_EXTENSION_PATH), ); @@ -204,13 +181,6 @@ public function testCleanupDoesNotCleanWhenConfigureIsMissing(): void 'pie_test_ext', '0.1.0', null, - [], - true, - true, - null, - null, - null, - 99, ), self::TEST_EXTENSION_PATH, ); @@ -249,13 +219,6 @@ public function testVerboseOutputShowsCleanupMessages(): void 'pie_test_ext', '0.1.0', null, - [ConfigureOption::fromComposerJsonDefinition(['name' => 'enable-pie_test_ext'])], - true, - true, - null, - null, - null, - 99, ), self::TEST_EXTENSION_PATH, ); diff --git a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php index 43d5799e..c250fbf5 100644 --- a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -98,8 +98,8 @@ public function testDependenciesAreResolvedToExpectedVersions( false, ); - self::assertSame($expectedVersion, $package->version); - self::assertNotNull($package->downloadUrl); - self::assertStringMatchesFormat($expectedDownloadUrl, $package->downloadUrl); + self::assertSame($expectedVersion, $package->version()); + self::assertNotNull($package->downloadUrl()); + self::assertStringMatchesFormat($expectedDownloadUrl, $package->downloadUrl()); } } diff --git a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php index 1bd9d56a..4dd883c9 100644 --- a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php @@ -20,6 +20,7 @@ use Php\Pie\Platform\TargetPlatform; use Php\Pie\Platform\ThreadSafetyMode; use Php\Pie\Platform\WindowsCompiler; +use Php\Pie\Platform\WindowsExtensionAssetName; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -53,13 +54,6 @@ public function testDeterminingReleaseAssetUrlForWindows(): void 'asgrim/example-pie-extension', '2.0.2', 'https://api.github.com/repos/asgrim/example-pie-extension/zipball/f9ed13ea95dada34c6cc5a052da258dbda059d27', - [], - true, - true, - null, - null, - null, - 99, ); $io = $this->createMock(IOInterface::class); @@ -77,11 +71,15 @@ public function testDeterminingReleaseAssetUrlForWindows(): void self::assertSame( 'https://github.com/asgrim/example-pie-extension/releases/download/2.0.2/php_example_pie_extension-2.0.2-8.3-ts-vs16-x86_64.zip', (new GithubPackageReleaseAssets('https://api.github.com')) - ->findWindowsDownloadUrlForPackage( + ->findMatchingReleaseAssetUrl( $targetPlatform, $package, new AuthHelper($io, $config), new HttpDownloader($io, $config), + WindowsExtensionAssetName::zipNames( + $targetPlatform, + $package, + ), ), ); } diff --git a/test/integration/Installing/UnixInstallTest.php b/test/integration/Installing/UnixInstallTest.php index ca28ee0d..1156eb69 100644 --- a/test/integration/Installing/UnixInstallTest.php +++ b/test/integration/Installing/UnixInstallTest.php @@ -7,7 +7,6 @@ use Composer\Package\CompletePackage; use Composer\Util\Platform; use Php\Pie\Building\UnixBuild; -use Php\Pie\ConfigureOption; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; @@ -89,13 +88,6 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void 'pie_test_ext', '0.1.0', null, - [ConfigureOption::fromComposerJsonDefinition(['name' => 'enable-pie_test_ext'])], - true, - true, - null, - null, - null, - 99, ), self::TEST_EXTENSION_PATH, ); diff --git a/test/integration/Installing/WindowsInstallTest.php b/test/integration/Installing/WindowsInstallTest.php index 4d043415..58cb0653 100644 --- a/test/integration/Installing/WindowsInstallTest.php +++ b/test/integration/Installing/WindowsInstallTest.php @@ -53,13 +53,6 @@ public function testWindowsInstallCanInstallExtension(): void 'php/pie-test-ext', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ), self::TEST_EXTENSION_PATH, ); diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index f70eb932..73f80d77 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -14,11 +14,8 @@ use Composer\Util\Platform; use InvalidArgumentException; use Php\Pie\Command\CommandHelper; -use Php\Pie\ConfigureOption; use Php\Pie\DependencyResolver\Package; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; -use Php\Pie\ExtensionName; -use Php\Pie\ExtensionType; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily; @@ -102,27 +99,21 @@ public function testBindingConfigurationOptionsFromPackage(): void public function testProcessingConfigureOptionsFromInput(): void { - $package = new Package( - $this->createMock(CompletePackage::class), - ExtensionType::PhpModule, - ExtensionName::normaliseFromString('lolz'), - 'foo/bar', - '1.0.0', - null, - [ - ConfigureOption::fromComposerJsonDefinition([ + $composerPackage = $this->createMock(CompletePackage::class); + $composerPackage->method('getPrettyName')->willReturn('foo/bar'); + $composerPackage->method('getPrettyVersion')->willReturn('1.0.0'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn([ + 'configure-options' => [ + [ 'name' => 'with-stuff', 'needs-value' => true, - ]), - ConfigureOption::fromComposerJsonDefinition(['name' => 'enable-thing']), + ], + ['name' => 'enable-thing'], ], - true, - true, - null, - null, - null, - 99, - ); + ]); + $package = Package::fromComposerCompletePackage($composerPackage); + $inputDefinition = new InputDefinition(); $inputDefinition->addOption(new InputOption('with-stuff', null, InputOption::VALUE_REQUIRED)); $inputDefinition->addOption(new InputOption('enable-thing', null, InputOption::VALUE_NONE)); diff --git a/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php b/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php index a0137f5b..5d3f5a94 100644 --- a/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php +++ b/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php @@ -6,13 +6,13 @@ use Composer\Package\CompletePackage; use Composer\PartialComposer; -use Php\Pie\BinaryFile; use Php\Pie\Building\Build; use Php\Pie\ComposerIntegration\InstallAndBuildProcess; use Php\Pie\ComposerIntegration\InstalledJsonMetadata; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Install; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; diff --git a/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php b/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php index 48790e74..a4df418d 100644 --- a/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php +++ b/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php @@ -8,11 +8,11 @@ use Composer\Package\CompletePackage; use Composer\Repository\InstalledArrayRepository; use Composer\Repository\RepositoryManager; -use Php\Pie\BinaryFile; use Php\Pie\ComposerIntegration\InstalledJsonMetadata; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\OperatingSystemFamily; diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php new file mode 100644 index 00000000..859bdb9a --- /dev/null +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -0,0 +1,399 @@ +composer = $this->createMock(Composer::class); + $this->io = $this->createMock(IOInterface::class); + $this->container = $this->createMock(ContainerInterface::class); + } + + public function testEventListenerRegistration(): void + { + $eventDispatcher = $this->createMock(EventDispatcher::class); + $eventDispatcher + ->expects(self::once()) + ->method('addListener') + ->with( + InstallerEvents::PRE_OPERATIONS_EXEC, + self::isInstanceOf(OverrideDownloadUrlInstallListener::class), + ); + + $this->composer + ->expects(self::once()) + ->method('getEventDispatcher') + ->willReturn($eventDispatcher); + + OverrideDownloadUrlInstallListener::selfRegister( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(OutputInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ); + } + + public function testNonInstallOperationsAreIgnored(): void + { + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + + /** + * @psalm-suppress InternalClass + * @psalm-suppress InternalMethod + */ + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([$composerPackage], []), + ); + + $this->container + ->expects(self::never()) + ->method('get'); + + (new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(OutputInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ))($installerEvent); + } + + public function testNonCompletePackagesAreIgnored(): void + { + $composerPackage = new Package('foo/bar', '1.2.3.0', '1.2.3'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + + /** + * @psalm-suppress InternalClass + * @psalm-suppress InternalMethod + */ + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([], [$composerPackage]), + ); + + $this->container + ->expects(self::never()) + ->method('get'); + + (new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(OutputInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ))($installerEvent); + } + + public function testInstallOperationsForDifferentPackagesAreIgnored(): void + { + $composerPackage = new CompletePackage('different/package', '1.2.3.0', '1.2.3'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + + /** + * @psalm-suppress InternalClass + * @psalm-suppress InternalMethod + */ + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([], [$composerPackage]), + ); + + $this->container + ->expects(self::never()) + ->method('get'); + + (new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(OutputInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ))($installerEvent); + } + + public function testWindowsUrlInstallerDoesNotRunOnNonWindows(): void + { + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + + /** + * @psalm-suppress InternalClass + * @psalm-suppress InternalMethod + */ + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([], [$composerPackage]), + ); + + $this->container + ->expects(self::never()) + ->method('get'); + + (new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(OutputInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ))($installerEvent); + + self::assertSame( + 'https://example.com/git-archive-zip-url', + $composerPackage->getDistUrl(), + ); + } + + public function testDistUrlIsUpdatedForWindowsInstallers(): void + { + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + + /** + * @psalm-suppress InternalClass + * @psalm-suppress InternalMethod + */ + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([], [$composerPackage]), + ); + + $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); + $packageReleaseAssets + ->expects(self::once()) + ->method('findMatchingReleaseAssetUrl') + ->willReturn('https://example.com/windows-download-url'); + + $this->container + ->method('get') + ->with(PackageReleaseAssets::class) + ->willReturn($packageReleaseAssets); + + (new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(OutputInterface::class), + new TargetPlatform( + OperatingSystem::Windows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ))($installerEvent); + + self::assertSame( + 'https://example.com/windows-download-url', + $composerPackage->getDistUrl(), + ); + } + + public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void + { + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + $composerPackage->setDistType('zip'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + $composerPackage->setPhpExt([ + 'extension-name' => 'foobar', + 'download-url-method' => 'pre-packaged-source', + ]); + + /** + * @psalm-suppress InternalClass + * @psalm-suppress InternalMethod + */ + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([], [$composerPackage]), + ); + + $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); + $packageReleaseAssets + ->expects(self::once()) + ->method('findMatchingReleaseAssetUrl') + ->willReturn('https://example.com/pre-packaged-source-download-url.tgz'); + + $this->container + ->method('get') + ->with(PackageReleaseAssets::class) + ->willReturn($packageReleaseAssets); + + (new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(OutputInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ))($installerEvent); + + self::assertSame( + 'https://example.com/pre-packaged-source-download-url.tgz', + $composerPackage->getDistUrl(), + ); + self::assertSame('tar', $composerPackage->getDistType()); + } +} diff --git a/test/unit/ComposerIntegration/RemoveUnrelatedInstallOperationsTest.php b/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php similarity index 97% rename from test/unit/ComposerIntegration/RemoveUnrelatedInstallOperationsTest.php rename to test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php index 0703e3b0..f251c42a 100644 --- a/test/unit/ComposerIntegration/RemoveUnrelatedInstallOperationsTest.php +++ b/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\PieUnitTest\ComposerIntegration; +namespace Php\PieUnitTest\ComposerIntegration\Listeners; use Composer\Composer; use Composer\DependencyResolver\Operation\InstallOperation; @@ -13,9 +13,9 @@ use Composer\Installer\InstallerEvents; use Composer\IO\IOInterface; use Composer\Package\CompletePackage; +use Php\Pie\ComposerIntegration\Listeners\RemoveUnrelatedInstallOperations; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; -use Php\Pie\ComposerIntegration\RemoveUnrelatedInstallOperations; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; diff --git a/test/unit/ComposerIntegration/OverrideWindowsUrlInstallListenerTest.php b/test/unit/ComposerIntegration/OverrideWindowsUrlInstallListenerTest.php deleted file mode 100644 index 8e9d2697..00000000 --- a/test/unit/ComposerIntegration/OverrideWindowsUrlInstallListenerTest.php +++ /dev/null @@ -1,196 +0,0 @@ -composer = $this->createMock(Composer::class); - $this->io = $this->createMock(IOInterface::class); - $this->container = $this->createMock(ContainerInterface::class); - } - - public function testEventListenerRegistration(): void - { - $eventDispatcher = $this->createMock(EventDispatcher::class); - $eventDispatcher - ->expects(self::once()) - ->method('addListener') - ->with( - InstallerEvents::PRE_OPERATIONS_EXEC, - self::isInstanceOf(OverrideWindowsUrlInstallListener::class), - ); - - $this->composer - ->expects(self::once()) - ->method('getEventDispatcher') - ->willReturn($eventDispatcher); - - OverrideWindowsUrlInstallListener::selfRegister( - $this->composer, - $this->io, - $this->container, - new PieComposerRequest( - $this->createMock(OutputInterface::class), - new TargetPlatform( - OperatingSystem::NonWindows, - OperatingSystemFamily::Linux, - PhpBinaryPath::fromCurrentProcess(), - Architecture::x86_64, - ThreadSafetyMode::NonThreadSafe, - 1, - WindowsCompiler::VC15, - ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), - PieOperation::Install, - [], - null, - false, - ), - ); - } - - public function testWindowsUrlInstallerDoesNotRunOnNonWindows(): void - { - $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); - $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); - - /** - * @psalm-suppress InternalClass - * @psalm-suppress InternalMethod - */ - $installerEvent = new InstallerEvent( - InstallerEvents::PRE_OPERATIONS_EXEC, - $this->composer, - $this->io, - false, - true, - new Transaction([], [$composerPackage]), - ); - - $this->container - ->expects(self::never()) - ->method('get'); - - (new OverrideWindowsUrlInstallListener( - $this->composer, - $this->io, - $this->container, - new PieComposerRequest( - $this->createMock(OutputInterface::class), - new TargetPlatform( - OperatingSystem::NonWindows, - OperatingSystemFamily::Linux, - PhpBinaryPath::fromCurrentProcess(), - Architecture::x86_64, - ThreadSafetyMode::NonThreadSafe, - 1, - WindowsCompiler::VC15, - ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), - PieOperation::Install, - [], - null, - false, - ), - ))($installerEvent); - - self::assertSame( - 'https://example.com/git-archive-zip-url', - $composerPackage->getDistUrl(), - ); - } - - public function testDistUrlIsUpdatedForWindowsInstallers(): void - { - $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); - $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); - - /** - * @psalm-suppress InternalClass - * @psalm-suppress InternalMethod - */ - $installerEvent = new InstallerEvent( - InstallerEvents::PRE_OPERATIONS_EXEC, - $this->composer, - $this->io, - false, - true, - new Transaction([], [$composerPackage]), - ); - - $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); - $packageReleaseAssets - ->expects(self::once()) - ->method('findWindowsDownloadUrlForPackage') - ->willReturn('https://example.com/windows-download-url'); - - $this->container - ->method('get') - ->with(PackageReleaseAssets::class) - ->willReturn($packageReleaseAssets); - - (new OverrideWindowsUrlInstallListener( - $this->composer, - $this->io, - $this->container, - new PieComposerRequest( - $this->createMock(OutputInterface::class), - new TargetPlatform( - OperatingSystem::Windows, - OperatingSystemFamily::Linux, - PhpBinaryPath::fromCurrentProcess(), - Architecture::x86_64, - ThreadSafetyMode::NonThreadSafe, - 1, - WindowsCompiler::VC15, - ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), - PieOperation::Install, - [], - null, - false, - ), - ))($installerEvent); - - self::assertSame( - 'https://example.com/windows-download-url', - $composerPackage->getDistUrl(), - ); - } -} diff --git a/test/unit/ComposerIntegration/PieJsonEditorTest.php b/test/unit/ComposerIntegration/PieJsonEditorTest.php index 29edc044..164ad9e2 100644 --- a/test/unit/ComposerIntegration/PieJsonEditorTest.php +++ b/test/unit/ComposerIntegration/PieJsonEditorTest.php @@ -53,6 +53,12 @@ public function testCanAddRequire(): void EOF), $this->normaliseJson(file_get_contents($testPieJson)), ); + + $editor->removeRequire('foo/bar'); + self::assertSame( + $this->normaliseJson('{}'), + $this->normaliseJson(file_get_contents($testPieJson)), + ); } public function testCanRevert(): void diff --git a/test/unit/ComposerIntegration/PieOperationTest.php b/test/unit/ComposerIntegration/PieOperationTest.php index 8387800a..a6f00dd2 100644 --- a/test/unit/ComposerIntegration/PieOperationTest.php +++ b/test/unit/ComposerIntegration/PieOperationTest.php @@ -17,6 +17,7 @@ public function testShouldBuild(): void self::assertFalse(PieOperation::Download->shouldBuild()); self::assertTrue(PieOperation::Build->shouldBuild()); self::assertTrue(PieOperation::Install->shouldBuild()); + self::assertFalse(PieOperation::Uninstall->shouldBuild()); } public function testShouldInstall(): void @@ -25,5 +26,6 @@ public function testShouldInstall(): void self::assertFalse(PieOperation::Download->shouldInstall()); self::assertFalse(PieOperation::Build->shouldInstall()); self::assertTrue(PieOperation::Install->shouldInstall()); + self::assertFalse(PieOperation::Uninstall->shouldBuild()); } } diff --git a/test/unit/DependencyResolver/PackageTest.php b/test/unit/DependencyResolver/PackageTest.php index fc99924a..47b3b10e 100644 --- a/test/unit/DependencyResolver/PackageTest.php +++ b/test/unit/DependencyResolver/PackageTest.php @@ -23,12 +23,12 @@ public function testFromComposerCompletePackage(): void new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'), ); - self::assertSame('foo', $package->extensionName->name()); - self::assertSame('vendor/foo', $package->name); - self::assertSame('1.2.3', $package->version); + self::assertSame('foo', $package->extensionName()->name()); + self::assertSame('vendor/foo', $package->name()); + self::assertSame('1.2.3', $package->version()); self::assertSame('vendor/foo:1.2.3', $package->prettyNameAndVersion()); - self::assertNull($package->downloadUrl); - self::assertNull($package->buildPath); + self::assertNull($package->downloadUrl()); + self::assertNull($package->buildPath()); } public function testFromComposerCompletePackageWithExtensionName(): void @@ -38,11 +38,11 @@ public function testFromComposerCompletePackageWithExtensionName(): void $package = Package::fromComposerCompletePackage($composerCompletePackage); - self::assertSame('something_else', $package->extensionName->name()); - self::assertSame('vendor/foo', $package->name); - self::assertSame('1.2.3', $package->version); + self::assertSame('something_else', $package->extensionName()->name()); + self::assertSame('vendor/foo', $package->name()); + self::assertSame('1.2.3', $package->version()); self::assertSame('vendor/foo:1.2.3', $package->prettyNameAndVersion()); - self::assertNull($package->downloadUrl); + self::assertNull($package->downloadUrl()); } public function testFromComposerCompletePackageWithExcludedOsFamilies(): void @@ -52,11 +52,11 @@ public function testFromComposerCompletePackageWithExcludedOsFamilies(): void $package = Package::fromComposerCompletePackage($composerCompletePackage); - self::assertSame([OperatingSystemFamily::Windows, OperatingSystemFamily::Darwin], $package->incompatibleOsFamilies); - self::assertSame('vendor/foo', $package->name); - self::assertSame('1.2.3', $package->version); + self::assertSame([OperatingSystemFamily::Windows, OperatingSystemFamily::Darwin], $package->incompatibleOsFamilies()); + self::assertSame('vendor/foo', $package->name()); + self::assertSame('1.2.3', $package->version()); self::assertSame('vendor/foo:1.2.3', $package->prettyNameAndVersion()); - self::assertNull($package->downloadUrl); + self::assertNull($package->downloadUrl()); } public function testFromComposerCompletePackageWithOsFamilies(): void @@ -66,12 +66,12 @@ public function testFromComposerCompletePackageWithOsFamilies(): void $package = Package::fromComposerCompletePackage($composerCompletePackage); - self::assertEmpty($package->incompatibleOsFamilies); - self::assertSame([OperatingSystemFamily::Windows, OperatingSystemFamily::Darwin], $package->compatibleOsFamilies); - self::assertSame('vendor/foo', $package->name); - self::assertSame('1.2.3', $package->version); + self::assertEmpty($package->incompatibleOsFamilies()); + self::assertSame([OperatingSystemFamily::Windows, OperatingSystemFamily::Darwin], $package->compatibleOsFamilies()); + self::assertSame('vendor/foo', $package->name()); + self::assertSame('1.2.3', $package->version()); self::assertSame('vendor/foo:1.2.3', $package->prettyNameAndVersion()); - self::assertNull($package->downloadUrl); + self::assertNull($package->downloadUrl()); } public function testFromComposerCompletePackageWithBothOsFamiliesAndExcludedOsFamiliesThrows(): void @@ -134,13 +134,6 @@ public function testGithubOrgAndRepo(string $composerPackageName, string|null $d $composerPackageName, '1.2.3', $downloadUrl, - [], - true, - true, - null, - null, - null, - 99, ); self::assertSame($expectedGithubOrgAndRepo, $package->githubOrgAndRepository()); @@ -154,6 +147,6 @@ public function testFromComposerCompletePackageWithBuildPath(): void $package = Package::fromComposerCompletePackage($composerCompletePackage); self::assertSame('vendor/foo:1.2.3', $package->prettyNameAndVersion()); - self::assertSame('some/subdirectory/path/', $package->buildPath); + self::assertSame('some/subdirectory/path/', $package->buildPath()); } } diff --git a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php index 6cdd9a4b..9e9f828e 100644 --- a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -66,8 +66,8 @@ public function testPackageThatCanBeResolved(): void $this->createMock(QuieterConsoleIO::class), ))($this->composer, $targetPlatform, new RequestedPackageAndVersion('asgrim/example-pie-extension', '^1.0'), false); - self::assertSame('asgrim/example-pie-extension', $package->name); - self::assertStringStartsWith('1.', $package->version); + self::assertSame('asgrim/example-pie-extension', $package->name()); + self::assertStringStartsWith('1.', $package->version()); } /** @@ -159,8 +159,8 @@ public function testUnresolvedPackageCanBeInstalledWithForceOption(array $platfo true, ); - self::assertSame('asgrim/example-pie-extension', $package->name); - self::assertStringStartsWith('1.', $package->version); + self::assertSame('asgrim/example-pie-extension', $package->name()); + self::assertStringStartsWith('1.', $package->version()); } public function testZtsOnlyPackageCannotBeInstalledOnNtsSystem(): void diff --git a/test/unit/Downloading/DownloadUrlMethodTest.php b/test/unit/Downloading/DownloadUrlMethodTest.php new file mode 100644 index 00000000..f48b8e86 --- /dev/null +++ b/test/unit/Downloading/DownloadUrlMethodTest.php @@ -0,0 +1,125 @@ +createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'foo/foo', + '1.2.3', + null, + ); + + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath + ->method('majorMinorVersion') + ->willReturn('8.1'); + + $targetPlatform = new TargetPlatform( + OperatingSystem::Windows, + OperatingSystemFamily::Windows, + $phpBinaryPath, + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ); + + $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); + + self::assertSame(DownloadUrlMethod::WindowsBinaryDownload, $downloadUrlMethod); + + self::assertSame( + [ + 'php_foo-1.2.3-8.1-nts-vc15-x86_64.zip', + 'php_foo-1.2.3-8.1-vc15-nts-x86_64.zip', + ], + $downloadUrlMethod->possibleAssetNames($package, $targetPlatform), + ); + } + + public function testPrePackagedSourceDownloads(): void + { + $composerPackage = $this->createMock(CompletePackage::class); + $composerPackage->method('getPrettyName')->willReturn('foo/bar'); + $composerPackage->method('getPrettyVersion')->willReturn('1.2.3'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn(['download-url-method' => 'pre-packaged-source']); + + $package = Package::fromComposerCompletePackage($composerPackage); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ); + + $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); + + self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, $downloadUrlMethod); + + self::assertSame( + [ + 'php_bar-1.2.3-src.tgz', + 'php_bar-1.2.3-src.zip', + ], + $downloadUrlMethod->possibleAssetNames($package, $targetPlatform), + ); + } + + public function testComposerDefaultDownload(): void + { + $package = new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'foo/foo', + '1.2.3', + null, + ); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ); + + $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); + + self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, $downloadUrlMethod); + + self::assertNull($downloadUrlMethod->possibleAssetNames($package, $targetPlatform)); + } +} diff --git a/test/unit/Downloading/DownloadedPackageTest.php b/test/unit/Downloading/DownloadedPackageTest.php index c4edf0b2..dcdc5dd6 100644 --- a/test/unit/Downloading/DownloadedPackageTest.php +++ b/test/unit/Downloading/DownloadedPackageTest.php @@ -29,13 +29,6 @@ public function testFromPackageAndExtractedPath(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ); $extractedSourcePath = uniqid('/path/to/downloaded/package', true); @@ -48,21 +41,13 @@ public function testFromPackageAndExtractedPath(): void public function testFromPackageAndExtractedPathWithBuildPath(): void { - $package = new Package( - $this->createMock(CompletePackage::class), - ExtensionType::PhpModule, - ExtensionName::normaliseFromString('foo'), - 'foo/bar', - '1.2.3', - null, - [], - true, - true, - 'Downloading', - null, - null, - 99, - ); + $composerPackage = $this->createMock(CompletePackage::class); + $composerPackage->method('getPrettyName')->willReturn('foo/bar'); + $composerPackage->method('getPrettyVersion')->willReturn('1.2.3'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn(['build-path' => 'Downloading']); + + $package = Package::fromComposerCompletePackage($composerPackage); $extractedSourcePath = realpath(__DIR__ . '/../'); @@ -71,4 +56,22 @@ public function testFromPackageAndExtractedPathWithBuildPath(): void self::assertSame($extractedSourcePath . DIRECTORY_SEPARATOR . 'Downloading', $downloadedPackage->extractedSourcePath); self::assertSame($package, $downloadedPackage->package); } + + public function testFromPackageAndExtractedPathWithBuildPathWithVersionTemplate(): void + { + $composerPackage = $this->createMock(CompletePackage::class); + $composerPackage->method('getPrettyName')->willReturn('foo/bar'); + $composerPackage->method('getPrettyVersion')->willReturn('1.2.3'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn(['build-path' => 'package-{version}']); + + $package = Package::fromComposerCompletePackage($composerPackage); + + $extractedSourcePath = realpath(__DIR__ . '/../../assets'); + + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath($package, $extractedSourcePath); + + self::assertSame($extractedSourcePath . DIRECTORY_SEPARATOR . 'package-1.2.3', $downloadedPackage->extractedSourcePath); + self::assertSame($package, $downloadedPackage->package); + } } diff --git a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php index e25bcecb..86718a89 100644 --- a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php +++ b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php @@ -30,13 +30,6 @@ public function testForPackage(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ); $exception = CouldNotFindReleaseAsset::forPackage($package, ['something.zip', 'something2.zip']); @@ -53,13 +46,6 @@ public function testForPackageWithMissingTag(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ); $exception = CouldNotFindReleaseAsset::forPackageWithMissingTag($package); diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index 34d8e25a..58b99b16 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -21,6 +21,7 @@ use Php\Pie\Platform\TargetPlatform; use Php\Pie\Platform\ThreadSafetyMode; use Php\Pie\Platform\WindowsCompiler; +use Php\Pie\Platform\WindowsExtensionAssetName; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -78,18 +79,23 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void 'asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true), - [], - true, - true, - null, - null, - null, - 99, ); $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); - self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package, $authHelper, $httpDownloader)); + self::assertSame( + 'actual_download_url', + $releaseAssets->findMatchingReleaseAssetUrl( + $targetPlatform, + $package, + $authHelper, + $httpDownloader, + WindowsExtensionAssetName::zipNames( + $targetPlatform, + $package, + ), + ), + ); } public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThreadSafetySwapped(): void @@ -141,18 +147,23 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThr 'asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true), - [], - true, - true, - null, - null, - null, - 99, ); $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); - self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package, $authHelper, $httpDownloader)); + self::assertSame( + 'actual_download_url', + $releaseAssets->findMatchingReleaseAssetUrl( + $targetPlatform, + $package, + $authHelper, + $httpDownloader, + WindowsExtensionAssetName::zipNames( + $targetPlatform, + $package, + ), + ), + ); } public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotFound(): void @@ -185,18 +196,20 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF 'asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true), - [], - true, - true, - null, - null, - null, - 99, ); $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); $this->expectException(CouldNotFindReleaseAsset::class); - $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package, $authHelper, $httpDownloader); + $releaseAssets->findMatchingReleaseAssetUrl( + $targetPlatform, + $package, + $authHelper, + $httpDownloader, + WindowsExtensionAssetName::zipNames( + $targetPlatform, + $package, + ), + ); } } diff --git a/test/unit/File/BinaryFileTest.php b/test/unit/File/BinaryFileTest.php new file mode 100644 index 00000000..0b7fc210 --- /dev/null +++ b/test/unit/File/BinaryFileTest.php @@ -0,0 +1,67 @@ +expectNotToPerformAssertions(); + $expectation->verify(); + } + + public function testVerifyFailsWithFileThatDoesNotExist(): void + { + $expectation = new BinaryFile( + '/path/to/a/file/that/does/not/exist', + self::TEST_FILE_HASH, + ); + + $this->expectException(FileNotFound::class); + $expectation->verify(); + } + + public function testVerifyFailsWithWrongHash(): void + { + $expectation = new BinaryFile( + self::TEST_FILE, + 'another hash that is wrong', + ); + + $this->expectException(BinaryFileFailedVerification::class); + $this->expectExceptionMessageMatches('/File "[^"]+" failed checksum verification\. Expected [^\.]+\.\.\., was [^\.]+\.\.\./'); + $expectation->verify(); + } + + public function testVerifyFailsWithDifferentFile(): void + { + $expectation = new BinaryFile( + self::TEST_FILE, + self::TEST_FILE_HASH, + ); + + $this->expectException(BinaryFileFailedVerification::class); + $this->expectExceptionMessageMatches('/Expected file "[^"]+" but actual file was "[^"]+"/'); + $expectation->verifyAgainstOther(new BinaryFile( + __FILE__, + self::TEST_FILE_HASH, + )); + } +} diff --git a/test/unit/File/SudoFilePutTest.php b/test/unit/File/SudoFilePutTest.php new file mode 100644 index 00000000..64707640 --- /dev/null +++ b/test/unit/File/SudoFilePutTest.php @@ -0,0 +1,38 @@ +mockPhpBinary, $this->output, @@ -107,13 +100,6 @@ public function testReturnsFalseWhenExistingIniCouldNotBeRead(): void 'foo/bar', '1.0.0', null, - [], - true, - true, - null, - null, - null, - 99, ), $this->mockPhpBinary, $this->output, @@ -161,13 +147,6 @@ public function testReturnsFalseWhenExtensionWasAddedButPhpRuntimeDidNotLoadExte 'foo/bar', '1.0.0', null, - [], - true, - true, - null, - null, - null, - 99, ), $this->mockPhpBinary, $this->output, @@ -205,13 +184,6 @@ public function testReturnsTrueWhenExtensionAdded(): void 'foo/bar', '1.0.0', null, - [], - true, - true, - null, - null, - null, - 99, ), $this->mockPhpBinary, $this->output, @@ -221,7 +193,7 @@ public function testReturnsTrueWhenExtensionAdded(): void $iniContent = file_get_contents($iniFile); self::assertSame( PHP_EOL . '; PIE automatically added this to enable the foo/bar extension' . PHP_EOL - . '; priority=99' . PHP_EOL + . '; priority=80' . PHP_EOL . 'extension=foobar' . PHP_EOL, $iniContent, ); @@ -255,13 +227,6 @@ public function testReturnsTrueWhenExtensionAddedWithAdditionalStep(): void 'foo/bar', '1.0.0', null, - [], - true, - true, - null, - null, - null, - 99, ), $this->mockPhpBinary, $this->output, @@ -277,7 +242,7 @@ static function () use (&$additionalStepInvoked): bool { $iniContent = file_get_contents($iniFile); self::assertSame( PHP_EOL . '; PIE automatically added this to enable the foo/bar extension' . PHP_EOL - . '; priority=99' . PHP_EOL + . '; priority=80' . PHP_EOL . 'extension=foobar' . PHP_EOL, $iniContent, ); diff --git a/test/unit/Installing/Ini/CheckAndAddExtensionToIniIfNeededTest.php b/test/unit/Installing/Ini/CheckAndAddExtensionToIniIfNeededTest.php index ac31ecc2..b7ae9a8b 100644 --- a/test/unit/Installing/Ini/CheckAndAddExtensionToIniIfNeededTest.php +++ b/test/unit/Installing/Ini/CheckAndAddExtensionToIniIfNeededTest.php @@ -72,13 +72,6 @@ public function setUp(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 66, ), '/path/to/extracted/source', ); @@ -122,16 +115,16 @@ public function testExtensionIsAlreadyEnabledButExtensionDoesNotLoad(): void $this->isExtensionAlreadyInTheIniFile ->expects(self::once()) ->method('__invoke') - ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName()) ->willReturn(true); $this->mockPhpBinary ->expects(self::once()) ->method('assertExtensionIsLoadedInRuntime') - ->with($this->downloadedPackage->package->extensionName, $this->output) + ->with($this->downloadedPackage->package->extensionName(), $this->output) ->willThrowException(ExtensionIsNotLoaded::fromExpectedExtension( $this->mockPhpBinary, - $this->downloadedPackage->package->extensionName, + $this->downloadedPackage->package->extensionName(), )); $this->addExtensionToTheIniFile @@ -162,13 +155,13 @@ public function testExtensionIsAlreadyEnabledAndExtensionLoaded(): void $this->isExtensionAlreadyInTheIniFile ->expects(self::once()) ->method('__invoke') - ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName()) ->willReturn(true); $this->mockPhpBinary ->expects(self::once()) ->method('assertExtensionIsLoadedInRuntime') - ->with($this->downloadedPackage->package->extensionName, $this->output); + ->with($this->downloadedPackage->package->extensionName(), $this->output); $this->addExtensionToTheIniFile ->expects(self::never()) @@ -194,13 +187,13 @@ public function testExtensionIsAlreadyEnabledWithAdditionalStepAndExtensionLoade $this->isExtensionAlreadyInTheIniFile ->expects(self::once()) ->method('__invoke') - ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName()) ->willReturn(true); $this->mockPhpBinary ->expects(self::once()) ->method('assertExtensionIsLoadedInRuntime') - ->with($this->downloadedPackage->package->extensionName, $this->output); + ->with($this->downloadedPackage->package->extensionName(), $this->output); $this->addExtensionToTheIniFile ->expects(self::never()) @@ -233,7 +226,7 @@ public function testExtensionIsNotYetAdded(): void $this->isExtensionAlreadyInTheIniFile ->expects(self::once()) ->method('__invoke') - ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName()) ->willReturn(false); $this->mockPhpBinary @@ -265,7 +258,7 @@ public function testExtensionIsNotYetAddedButFailsToBeAdded(): void $this->isExtensionAlreadyInTheIniFile ->expects(self::once()) ->method('__invoke') - ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName()) ->willReturn(false); $this->mockPhpBinary diff --git a/test/unit/Installing/Ini/DockerPhpExtEnableTest.php b/test/unit/Installing/Ini/DockerPhpExtEnableTest.php index 410190a0..e118096d 100644 --- a/test/unit/Installing/Ini/DockerPhpExtEnableTest.php +++ b/test/unit/Installing/Ini/DockerPhpExtEnableTest.php @@ -5,11 +5,11 @@ namespace Php\PieUnitTest\Installing\Ini; use Composer\Package\CompletePackageInterface; -use Php\Pie\BinaryFile; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\DockerPhpExtEnable; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; @@ -70,13 +70,6 @@ public function setUp(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ); @@ -122,7 +115,7 @@ public function testReturnsTrueWhenDockerPhpExtEnableSuccessfullyEnablesExtensio $this->mockPhpBinary ->expects(self::once()) ->method('assertExtensionIsLoadedInRuntime') - ->with($this->downloadedPackage->package->extensionName, $this->output); + ->with($this->downloadedPackage->package->extensionName(), $this->output); self::assertTrue( (new DockerPhpExtEnable(self::GOOD_DOCKER_PHP_EXT_ENABLE)) @@ -157,10 +150,10 @@ public function testReturnsFalseWhenDockerPhpExtEnableFailsToAssertExtensionWasE $this->mockPhpBinary ->expects(self::once()) ->method('assertExtensionIsLoadedInRuntime') - ->with($this->downloadedPackage->package->extensionName, $this->output) + ->with($this->downloadedPackage->package->extensionName(), $this->output) ->willThrowException(ExtensionIsNotLoaded::fromExpectedExtension( $this->mockPhpBinary, - $this->downloadedPackage->package->extensionName, + $this->downloadedPackage->package->extensionName(), )); self::assertFalse( diff --git a/test/unit/Installing/Ini/OndrejPhpenmodTest.php b/test/unit/Installing/Ini/OndrejPhpenmodTest.php index 4fcecb4e..efa76863 100644 --- a/test/unit/Installing/Ini/OndrejPhpenmodTest.php +++ b/test/unit/Installing/Ini/OndrejPhpenmodTest.php @@ -5,11 +5,11 @@ namespace Php\PieUnitTest\Installing\Ini; use Composer\Package\CompletePackageInterface; -use Php\Pie\BinaryFile; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\CheckAndAddExtensionToIniIfNeeded; use Php\Pie\Installing\Ini\OndrejPhpenmod; use Php\Pie\Platform\Architecture; @@ -83,13 +83,6 @@ public function setUp(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ); diff --git a/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php b/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php index 07d4bd68..0786ee7f 100644 --- a/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php +++ b/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php @@ -5,11 +5,11 @@ namespace Php\PieUnitTest\Installing\Ini; use Composer\Package\CompletePackage; -use Php\Pie\BinaryFile; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\PickBestSetupIniApproach; use Php\Pie\Installing\Ini\SetupIniApproach; use Php\Pie\Platform\Architecture; @@ -97,13 +97,6 @@ public function testVerboseMessageIsEmittedSettingUpWithoutAnyApproaches(): void 'test-vendor/test-package', '1.2.3', 'https://test-uri/', - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ), @@ -139,13 +132,6 @@ public function testWorkingApproachIsUsed(): void 'test-vendor/test-package', '1.2.3', 'https://test-uri/', - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ), @@ -181,13 +167,6 @@ public function testSetupFailsWhenNoApproachesWork(): void 'test-vendor/test-package', '1.2.3', 'https://test-uri/', - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ), diff --git a/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php b/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php index de1795f3..69b71db3 100644 --- a/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php +++ b/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php @@ -5,11 +5,11 @@ namespace Php\PieUnitTest\Installing\Ini; use Composer\Package\CompletePackageInterface; -use Php\Pie\BinaryFile; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\PreCheckExtensionAlreadyLoaded; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; @@ -65,13 +65,6 @@ public function setUp(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ); @@ -93,7 +86,7 @@ public function testSetupReturnsTrueWhenExtAlreadyRuntimeLoaded(): void $this->mockPhpBinary ->expects(self::once()) ->method('assertExtensionIsLoadedInRuntime') - ->with($this->downloadedPackage->package->extensionName, $this->output); + ->with($this->downloadedPackage->package->extensionName(), $this->output); self::assertTrue($this->preCheckExtensionAlreadyLoaded->setup( $this->targetPlatform, @@ -108,10 +101,10 @@ public function testSetupReturnsFalseWhenExtIsNotRuntimeLoaded(): void $this->mockPhpBinary ->expects(self::once()) ->method('assertExtensionIsLoadedInRuntime') - ->with($this->downloadedPackage->package->extensionName, $this->output) + ->with($this->downloadedPackage->package->extensionName(), $this->output) ->willThrowException(ExtensionIsNotLoaded::fromExpectedExtension( $this->mockPhpBinary, - $this->downloadedPackage->package->extensionName, + $this->downloadedPackage->package->extensionName(), )); self::assertFalse($this->preCheckExtensionAlreadyLoaded->setup( diff --git a/test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php b/test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php new file mode 100644 index 00000000..d08fb494 --- /dev/null +++ b/test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php @@ -0,0 +1,128 @@ +iniFilePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_remove_ini_test', true); + mkdir($this->iniFilePath); + Assert::positiveInteger(file_put_contents( + $this->iniFilePath . DIRECTORY_SEPARATOR . 'with_commented_exts.ini', + self::INI_WITH_COMMENTED_EXTS, + )); + Assert::positiveInteger(file_put_contents( + $this->iniFilePath . DIRECTORY_SEPARATOR . 'with_active_exts.ini', + self::INI_WITH_ACTIVE_EXTS, + )); + } + + public function tearDown(): void + { + parent::tearDown(); + + (new Filesystem())->remove($this->iniFilePath); + } + + /** + * @return array + * + * @psalm-suppress PossiblyUnusedMethod https://github.com/psalm/psalm-plugin-phpunit/issues/131 + */ + public static function extensionTypeProvider(): array + { + return [ + 'phpModule' => [ExtensionType::PhpModule, "; extension=foobar ; removed by PIE\nzend_extension=foobar\n"], + 'zendExtension' => [ExtensionType::ZendExtension, "extension=foobar\n; zend_extension=foobar ; removed by PIE\n"], + ]; + } + + #[DataProvider('extensionTypeProvider')] + public function testRelevantIniFilesHaveExtensionRemoved(ExtensionType $extensionType, string $expectedActiveContent): void + { + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath + ->method('loadedIniConfigurationFile') + ->willReturn(null); + $phpBinaryPath + ->method('additionalIniDirectory') + ->willReturn($this->iniFilePath); + + $package = new Package( + $this->createMock(CompletePackageInterface::class), + $extensionType, + ExtensionName::normaliseFromString('foobar'), + 'foobar/foobar', + '1.2.3', + null, + ); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $phpBinaryPath, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + null, + ); + + $affectedFiles = (new RemoveIniEntryWithFileGetContents())( + $package, + $targetPlatform, + $this->createMock(OutputInterface::class), + ); + + self::assertSame( + [$this->iniFilePath . DIRECTORY_SEPARATOR . 'with_active_exts.ini'], + $affectedFiles, + ); + + self::assertSame( + self::INI_WITH_COMMENTED_EXTS, + file_get_contents($this->iniFilePath . DIRECTORY_SEPARATOR . 'with_commented_exts.ini'), + ); + + self::assertSame( + $expectedActiveContent, + file_get_contents($this->iniFilePath . DIRECTORY_SEPARATOR . 'with_active_exts.ini'), + ); + } +} diff --git a/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php b/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php index 83f5b9e4..8380d211 100644 --- a/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php +++ b/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php @@ -5,11 +5,11 @@ namespace Php\PieUnitTest\Installing\Ini; use Composer\Package\CompletePackageInterface; -use Php\Pie\BinaryFile; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\CheckAndAddExtensionToIniIfNeeded; use Php\Pie\Installing\Ini\StandardAdditionalPhpIniDirectory; use Php\Pie\Platform\Architecture; @@ -77,13 +77,6 @@ public function setUp(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ); @@ -163,7 +156,7 @@ public function testReturnsTrueWhenCheckAndAddExtensionIsInvoked(): void unlink($additionalPhpIniDirectory); mkdir($additionalPhpIniDirectory, recursive: true); - $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '99-foobar.ini'; + $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '80-foobar.ini'; $this->mockPhpBinary ->expects(self::once()) @@ -199,7 +192,7 @@ public function testReturnsFalseAndRemovesPieCreatedIniFileWhenCheckAndAddExtens unlink($additionalPhpIniDirectory); mkdir($additionalPhpIniDirectory, recursive: true); - $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '99-foobar.ini'; + $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '80-foobar.ini'; $this->mockPhpBinary ->expects(self::once()) @@ -234,7 +227,7 @@ public function testReturnsFalseAndLeavesNonPieCreatedIniFileWhenCheckAndAddExte unlink($additionalPhpIniDirectory); mkdir($additionalPhpIniDirectory, recursive: true); - $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '99-foobar.ini'; + $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '80-foobar.ini'; touch($expectedIniFile); $this->mockPhpBinary diff --git a/test/unit/Installing/Ini/StandardSinglePhpIniTest.php b/test/unit/Installing/Ini/StandardSinglePhpIniTest.php index 34b19daa..3d35fd0e 100644 --- a/test/unit/Installing/Ini/StandardSinglePhpIniTest.php +++ b/test/unit/Installing/Ini/StandardSinglePhpIniTest.php @@ -5,11 +5,11 @@ namespace Php\PieUnitTest\Installing\Ini; use Composer\Package\CompletePackageInterface; -use Php\Pie\BinaryFile; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\CheckAndAddExtensionToIniIfNeeded; use Php\Pie\Installing\Ini\StandardSinglePhpIni; use Php\Pie\Platform\Architecture; @@ -70,13 +70,6 @@ public function setUp(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ); diff --git a/test/unit/Installing/PackageMetadataMissingTest.php b/test/unit/Installing/PackageMetadataMissingTest.php new file mode 100644 index 00000000..5fdc5a78 --- /dev/null +++ b/test/unit/Installing/PackageMetadataMissingTest.php @@ -0,0 +1,43 @@ +createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + ); + + $exception = PackageMetadataMissing::duringUninstall( + $package, + [ + 'a' => 'something', + 'b' => 'something else', + ], + ['b', 'c', 'd'], + ); + + self::assertSame( + 'PIE metadata was missing for package foo/bar. Missing metadata keys: c, d', + $exception->getMessage(), + ); + } +} diff --git a/test/unit/Installing/UninstallUsingUnlinkTest.php b/test/unit/Installing/UninstallUsingUnlinkTest.php new file mode 100644 index 00000000..8b928d14 --- /dev/null +++ b/test/unit/Installing/UninstallUsingUnlinkTest.php @@ -0,0 +1,76 @@ +createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([]); + + $package = new Package( + $composerPackage, + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foobar/foobar', + '1.2.3', + null, + ); + + $this->expectException(PackageMetadataMissing::class); + $this->expectExceptionMessage('PIE metadata was missing for package foobar/foobar. Missing metadata keys: pie-installed-binary, pie-installed-binary-checksum'); + (new UninstallUsingUnlink())($package); + } + + public function testBinaryFileIsRemoved(): void + { + $testFilename = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_uninstall_binary_test_', true); + file_put_contents($testFilename, 'test content'); + $testHash = hash_file('sha256', $testFilename); + + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([ + PieInstalledJsonMetadataKeys::InstalledBinary->value => $testFilename, + PieInstalledJsonMetadataKeys::BinaryChecksum->value => $testHash, + ]); + + $package = new Package( + $composerPackage, + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foobar/foobar', + '1.2.3', + null, + ); + + $uninstalled = (new UninstallUsingUnlink())($package); + + self::assertSame($testFilename, $uninstalled->filePath); + self::assertFileDoesNotExist($testFilename); + } +} diff --git a/test/unit/Platform/InstalledPiePackagesTest.php b/test/unit/Platform/InstalledPiePackagesTest.php new file mode 100644 index 00000000..22e6357a --- /dev/null +++ b/test/unit/Platform/InstalledPiePackagesTest.php @@ -0,0 +1,42 @@ +createMock(InstalledRepositoryInterface::class); + $localRepo->method('getPackages')->willReturn([ + new CompletePackage('foo/bar1', '1.2.3.0', '1.2.3'), + new CompletePackage('foo/bar2', '1.2.3.0', '1.2.3'), + ]); + + $repoManager = $this->createMock(RepositoryManager::class); + $repoManager->method('getLocalRepository')->willReturn($localRepo); + + $composer = $this->createMock(Composer::class); + $composer->method('getRepositoryManager')->willReturn($repoManager); + + $packages = (new InstalledPiePackages())->allPiePackages($composer); + + self::assertArrayHasKey('bar1', $packages); + self::assertArrayHasKey('bar2', $packages); + + self::assertSame('bar1', $packages['bar1']->extensionName()->name()); + self::assertSame('foo/bar1', $packages['bar1']->name()); + self::assertSame('bar2', $packages['bar2']->extensionName()->name()); + self::assertSame('foo/bar2', $packages['bar2']->name()); + } +} diff --git a/test/unit/Platform/PrePackagedSourceAssetNameTest.php b/test/unit/Platform/PrePackagedSourceAssetNameTest.php new file mode 100644 index 00000000..5a47f146 --- /dev/null +++ b/test/unit/Platform/PrePackagedSourceAssetNameTest.php @@ -0,0 +1,37 @@ +createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + ), + ), + ); + } +} diff --git a/test/unit/Platform/WindowsExtensionAssetNameTest.php b/test/unit/Platform/WindowsExtensionAssetNameTest.php index 75e72a6b..77a77764 100644 --- a/test/unit/Platform/WindowsExtensionAssetNameTest.php +++ b/test/unit/Platform/WindowsExtensionAssetNameTest.php @@ -49,13 +49,6 @@ public function setUp(): void 'phpf/foo', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ); } diff --git a/test/unit/Util/CaptureErrorsTest.php b/test/unit/Util/CaptureErrorsTest.php new file mode 100644 index 00000000..a1cddea9 --- /dev/null +++ b/test/unit/Util/CaptureErrorsTest.php @@ -0,0 +1,38 @@ +