diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php
index 46bd27e0..0860038e 100644
--- a/src/Command/BuildCommand.php
+++ b/src/Command/BuildCommand.php
@@ -54,6 +54,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
PieOperation::Resolve,
[], // Configure options are not needed for resolve only
null,
+ false, // setting up INI not needed for build
),
);
@@ -74,6 +75,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
PieOperation::Build,
$configureOptionsValues,
CommandHelper::determinePhpizePathFromInputs($input),
+ false, // setting up INI not needed for build
),
);
diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php
index 68b55d2c..8e25165b 100644
--- a/src/Command/CommandHelper.php
+++ b/src/Command/CommandHelper.php
@@ -38,6 +38,7 @@ final class CommandHelper
private const OPTION_WITH_PHP_PATH = 'with-php-path';
private const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path';
private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs';
+ private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension';
/** @psalm-suppress UnusedConstructor */
private function __construct()
@@ -79,6 +80,12 @@ public static function configureDownloadBuildInstallOptions(Command $command): v
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,
+ InputOption::VALUE_NONE,
+ 'Specify this to skip attempting to enable the extension in php.ini',
+ );
self::configurePhpConfigOptions($command);
@@ -154,6 +161,11 @@ public static function determineTargetPlatformFromInputs(InputInterface $input,
return $targetPlatform;
}
+ public static function determineAttemptToSetupIniFile(InputInterface $input): bool
+ {
+ return ! $input->hasOption(self::OPTION_SKIP_ENABLE_EXTENSION) || ! $input->getOption(self::OPTION_SKIP_ENABLE_EXTENSION);
+ }
+
public static function determinePhpizePathFromInputs(InputInterface $input): PhpizePath|null
{
if ($input->hasOption(self::OPTION_WITH_PHPIZE_PATH)) {
diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php
index a6c237d4..f6616290 100644
--- a/src/Command/DownloadCommand.php
+++ b/src/Command/DownloadCommand.php
@@ -56,6 +56,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
PieOperation::Download,
[], // Configure options are not needed for download only
null,
+ false, // setting up INI not needed for download
),
);
diff --git a/src/Command/InfoCommand.php b/src/Command/InfoCommand.php
index 0d3a6cc3..9f1f806f 100644
--- a/src/Command/InfoCommand.php
+++ b/src/Command/InfoCommand.php
@@ -54,6 +54,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
PieOperation::Resolve,
[], // Configure options are not needed for resolve only
null,
+ false, // setting up INI not needed for info
),
);
diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php
index ca293165..aca34af2 100644
--- a/src/Command/InstallCommand.php
+++ b/src/Command/InstallCommand.php
@@ -59,6 +59,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
PieOperation::Resolve,
[], // Configure options are not needed for resolve only
null,
+ false, // setting up INI not needed for resolve step
),
);
@@ -79,6 +80,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
PieOperation::Install,
$configureOptionsValues,
CommandHelper::determinePhpizePathFromInputs($input),
+ CommandHelper::determineAttemptToSetupIniFile($input),
),
);
diff --git a/src/ComposerIntegration/InstallAndBuildProcess.php b/src/ComposerIntegration/InstallAndBuildProcess.php
index c6944720..39e62fee 100644
--- a/src/ComposerIntegration/InstallAndBuildProcess.php
+++ b/src/ComposerIntegration/InstallAndBuildProcess.php
@@ -77,6 +77,7 @@ public function __invoke(
$downloadedPackage,
$composerRequest->targetPlatform,
$output,
+ $composerRequest->attemptToSetupIniFile,
),
);
}
diff --git a/src/ComposerIntegration/PieComposerRequest.php b/src/ComposerIntegration/PieComposerRequest.php
index 536a3471..2679bdd9 100644
--- a/src/ComposerIntegration/PieComposerRequest.php
+++ b/src/ComposerIntegration/PieComposerRequest.php
@@ -24,6 +24,7 @@ public function __construct(
public readonly PieOperation $operation,
public readonly array $configureOptions,
public readonly PhpizePath|null $phpizePath,
+ public readonly bool $attemptToSetupIniFile,
) {
}
}
diff --git a/src/Container.php b/src/Container.php
index 263fd418..1c181a9a 100644
--- a/src/Container.php
+++ b/src/Container.php
@@ -19,6 +19,7 @@
use Php\Pie\DependencyResolver\ResolveDependencyWithComposer;
use Php\Pie\Downloading\GithubPackageReleaseAssets;
use Php\Pie\Downloading\PackageReleaseAssets;
+use Php\Pie\Installing\Ini;
use Php\Pie\Installing\Install;
use Php\Pie\Installing\UnixInstall;
use Php\Pie\Installing\WindowsInstall;
@@ -71,6 +72,19 @@ static function (ContainerInterface $container): Build {
},
);
+ $container->singleton(
+ Ini\SetupIniApproach::class,
+ static function (ContainerInterface $container): Ini\SetupIniApproach {
+ return new Ini\PickBestSetupIniApproach([
+ $container->get(Ini\PreCheckExtensionAlreadyLoaded::class),
+ $container->get(Ini\OndrejPhpenmod::class),
+ $container->get(Ini\DockerPhpExtEnable::class),
+ $container->get(Ini\StandardAdditionalPhpIniDirectory::class),
+ $container->get(Ini\StandardSinglePhpIni::class),
+ ]);
+ },
+ );
+
$container->singleton(
Install::class,
static function (ContainerInterface $container): Install {
diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php
index b4d3f7c7..d09a8a0d 100644
--- a/src/DependencyResolver/Package.php
+++ b/src/DependencyResolver/Package.php
@@ -47,6 +47,7 @@ public function __construct(
public readonly string|null $buildPath,
public readonly array|null $compatibleOsFamilies,
public readonly array|null $incompatibleOsFamilies,
+ public readonly int $priority,
) {
}
@@ -93,6 +94,7 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com
$buildPath,
self::convertInputStringsToOperatingSystemFamilies($compatibleOsFamilies),
self::convertInputStringsToOperatingSystemFamilies($incompatibleOsFamilies),
+ $phpExtOptions['priority'] ?? 80,
);
}
diff --git a/src/Installing/Ini/AddExtensionToTheIniFile.php b/src/Installing/Ini/AddExtensionToTheIniFile.php
new file mode 100644
index 00000000..73469e9e
--- /dev/null
+++ b/src/Installing/Ini/AddExtensionToTheIniFile.php
@@ -0,0 +1,102 @@
+writeln(
+ sprintf(
+ 'PHP is configured to use %s, but it is not writable by PIE.',
+ $ini,
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+
+ return false;
+ }
+
+ $originalIniContent = file_get_contents($ini);
+
+ if (! is_string($originalIniContent)) {
+ $output->writeln(
+ sprintf(
+ 'Tried making a backup of %s but could not read it, aborting enablement of extension',
+ $ini,
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+
+ return false;
+ }
+
+ try {
+ file_put_contents(
+ $ini,
+ $originalIniContent . $this->iniFileContent($package),
+ );
+ $output->writeln(
+ sprintf(
+ 'Enabled extension %s in the INI file %s',
+ $package->extensionName->name(),
+ $ini,
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+
+ if ($additionalEnableStep !== null && ! $additionalEnableStep()) {
+ return false;
+ }
+
+ $phpBinaryPath->assertExtensionIsLoadedInRuntime($package->extensionName, $output);
+
+ return true;
+ } catch (Throwable $anything) {
+ file_put_contents($ini, $originalIniContent);
+
+ $output->writeln(sprintf(
+ 'Something went wrong enabling the %s extension: %s',
+ $package->extensionName->name(),
+ $anything->getMessage(),
+ ));
+
+ return false;
+ }
+ }
+
+ /** @return non-empty-string */
+ 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')
+ . '='
+ . $package->extensionName->name() . PHP_EOL;
+ }
+}
diff --git a/src/Installing/Ini/CheckAndAddExtensionToIniIfNeeded.php b/src/Installing/Ini/CheckAndAddExtensionToIniIfNeeded.php
new file mode 100644
index 00000000..eec39db0
--- /dev/null
+++ b/src/Installing/Ini/CheckAndAddExtensionToIniIfNeeded.php
@@ -0,0 +1,84 @@
+writeln(
+ sprintf(
+ 'PHP is configured to use %s, but it did not exist, or is not readable by PIE.',
+ $iniFile,
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+
+ return false;
+ }
+
+ if (($this->isExtensionAlreadyInTheIniFile)($iniFile, $downloadedPackage->package->extensionName)) {
+ $output->writeln(
+ sprintf(
+ 'Extension is already enabled in the INI file %s',
+ $iniFile,
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+
+ if ($additionalEnableStep !== null && ! $additionalEnableStep()) {
+ return false;
+ }
+
+ try {
+ $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(),
+ $anything->getMessage(),
+ ));
+
+ return false;
+ }
+ }
+
+ return ($this->addExtensionToTheIniFile)(
+ $iniFile,
+ $downloadedPackage->package,
+ $targetPlatform->phpBinaryPath,
+ $output,
+ $additionalEnableStep,
+ );
+ }
+}
diff --git a/src/Installing/Ini/DockerPhpExtEnable.php b/src/Installing/Ini/DockerPhpExtEnable.php
new file mode 100644
index 00000000..2878af4d
--- /dev/null
+++ b/src/Installing/Ini/DockerPhpExtEnable.php
@@ -0,0 +1,89 @@
+dockerPhpExtEnablePath() !== null;
+ }
+
+ public function setup(
+ TargetPlatform $targetPlatform,
+ DownloadedPackage $downloadedPackage,
+ BinaryFile $binaryFile,
+ OutputInterface $output,
+ ): bool {
+ $dockerPhpExtEnable = $this->dockerPhpExtEnablePath();
+
+ if ($dockerPhpExtEnable === null) {
+ return false;
+ }
+
+ try {
+ $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(),
+ $this->dockerPhpExtEnableName,
+ $processFailed->getMessage(),
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+
+ return false;
+ }
+
+ try {
+ $targetPlatform->phpBinaryPath->assertExtensionIsLoadedInRuntime(
+ $downloadedPackage->package->extensionName,
+ $output,
+ );
+
+ return true;
+ } catch (ExtensionIsNotLoaded) {
+ $output->writeln(
+ sprintf(
+ 'Asserting that extension %s was enabled using %s failed. Output was: %s',
+ $downloadedPackage->package->extensionName->name(),
+ $this->dockerPhpExtEnableName,
+ $enableOutput !== '' ? $enableOutput : '(empty)',
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+
+ return false;
+ }
+ }
+
+ private function dockerPhpExtEnablePath(): string|null
+ {
+ try {
+ return Process::run(['which', $this->dockerPhpExtEnableName]);
+ } catch (ProcessFailedException) {
+ return null;
+ }
+ }
+}
diff --git a/src/Installing/Ini/IsExtensionAlreadyInTheIniFile.php b/src/Installing/Ini/IsExtensionAlreadyInTheIniFile.php
new file mode 100644
index 00000000..cfe2c8bf
--- /dev/null
+++ b/src/Installing/Ini/IsExtensionAlreadyInTheIniFile.php
@@ -0,0 +1,52 @@
+readIniFile($iniFilePath);
+
+ return in_array($extensionName->name(), array_merge($loadedExts['extensions'], $loadedExts['zend_extensions']));
+ }
+
+ /** @return array{extensions: list, zend_extensions: list} */
+ private function readIniFile(string $iniFilePath): array
+ {
+ $iniFileContentLines = file($iniFilePath);
+
+ $extensions = [];
+ $zendExtensions = [];
+ foreach ($iniFileContentLines as $line) {
+ $lineIni = parse_ini_string($line);
+
+ if (array_key_exists('extension', $lineIni) && is_string($lineIni['extension']) && $lineIni['extension'] !== '') {
+ $extensions[] = $lineIni['extension'];
+ }
+
+ if (! array_key_exists('zend_extension', $lineIni) || ! is_string($lineIni['zend_extension']) || $lineIni['zend_extension'] === '') {
+ continue;
+ }
+
+ $zendExtensions[] = $lineIni['zend_extension'];
+ }
+
+ return [
+ 'extensions' => $extensions,
+ 'zend_extensions' => $zendExtensions,
+ ];
+ }
+}
diff --git a/src/Installing/Ini/OndrejPhpenmod.php b/src/Installing/Ini/OndrejPhpenmod.php
new file mode 100644
index 00000000..051dfb65
--- /dev/null
+++ b/src/Installing/Ini/OndrejPhpenmod.php
@@ -0,0 +1,194 @@
+phpenmodPath() !== null;
+ }
+
+ public function setup(
+ TargetPlatform $targetPlatform,
+ DownloadedPackage $downloadedPackage,
+ BinaryFile $binaryFile,
+ OutputInterface $output,
+ ): bool {
+ $phpenmodPath = $this->phpenmodPath();
+
+ /** In practice, this shouldn't happen since {@see canBeUsed()} checks this */
+ if ($phpenmodPath === null) {
+ return false;
+ }
+
+ // the Ondrej repo uses an additional php.ini directory, if this isn't set, we may not actually be using Ondrej repo for this particular PHP install
+ $additionalPhpIniPath = $targetPlatform->phpBinaryPath->additionalIniDirectory();
+
+ if ($additionalPhpIniPath === null) {
+ $output->writeln(
+ 'Additional INI file path was not set - may not be Ondrej PHP repo',
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+
+ return false;
+ }
+
+ // Cursory check for the expected PHP INI directory; this is another indication we're using the Ondrej repo
+ if (preg_match('#/etc/php/\d\.\d/[a-z-_]+/conf.d#', $additionalPhpIniPath) !== 1) {
+ $output->writeln(
+ sprintf(
+ 'Warning: additional INI file path was not in the expected format (/etc/php/{version}/{sapi}/conf.d). Path was: %s',
+ $additionalPhpIniPath,
+ ),
+ OutputInterface::VERBOSITY_VERY_VERBOSE,
+ );
+ }
+
+ $expectedModsAvailablePath = sprintf($this->modsAvailablePath, $targetPlatform->phpBinaryPath->majorMinorVersion());
+
+ if (! file_exists($expectedModsAvailablePath)) {
+ $output->writeln(
+ sprintf(
+ 'Mods available path %s does not exist',
+ $expectedModsAvailablePath,
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+
+ return false;
+ }
+
+ if (! is_dir($expectedModsAvailablePath)) {
+ $output->writeln(
+ sprintf(
+ 'Mods available path %s is not a directory',
+ $expectedModsAvailablePath,
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+
+ return false;
+ }
+
+ if (! is_writable($expectedModsAvailablePath)) {
+ $output->writeln(
+ sprintf(
+ 'Mods available path %s is not writable',
+ $expectedModsAvailablePath,
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+
+ return false;
+ }
+
+ $expectedIniFile = sprintf(
+ '%s%s%s.ini',
+ rtrim($expectedModsAvailablePath, DIRECTORY_SEPARATOR),
+ DIRECTORY_SEPARATOR,
+ $downloadedPackage->package->extensionName->name(),
+ );
+
+ $pieCreatedTheIniFile = false;
+ if (! file_exists($expectedIniFile)) {
+ $output->writeln(
+ sprintf(
+ 'Creating new INI file based on extension priority: %s',
+ $expectedIniFile,
+ ),
+ OutputInterface::VERBOSITY_VERY_VERBOSE,
+ );
+ $pieCreatedTheIniFile = true;
+ touch($expectedIniFile);
+ }
+
+ $addingExtensionWasSuccessful = ($this->checkAndAddExtensionToIniIfNeeded)(
+ $expectedIniFile,
+ $targetPlatform,
+ $downloadedPackage,
+ $output,
+ static function () use ($phpenmodPath, $targetPlatform, $downloadedPackage, $output): bool {
+ try {
+ Process::run([
+ $phpenmodPath,
+ '-v',
+ $targetPlatform->phpBinaryPath->majorMinorVersion(),
+ '-s',
+ 'ALL',
+ $downloadedPackage->package->extensionName->name(),
+ ]);
+
+ return true;
+ } catch (ProcessFailedException $processFailedException) {
+ $output->writeln(
+ sprintf(
+ 'Failed to use %s to enable %s for PHP %s: %s',
+ $phpenmodPath,
+ $downloadedPackage->package->extensionName->name(),
+ $targetPlatform->phpBinaryPath->majorMinorVersion(),
+ $processFailedException->getMessage(),
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+
+ return false;
+ }
+ },
+ );
+
+ if (! $addingExtensionWasSuccessful && $pieCreatedTheIniFile) {
+ unlink($expectedIniFile);
+ }
+
+ return $addingExtensionWasSuccessful;
+ }
+
+ /** @return non-empty-string|null */
+ private function phpenmodPath(): string|null
+ {
+ if (Platform::isWindows()) {
+ return null;
+ }
+
+ try {
+ $phpenmodPath = Process::run(['which', $this->phpenmod]);
+
+ return $phpenmodPath !== '' ? $phpenmodPath : null;
+ } catch (ProcessFailedException) {
+ return null;
+ }
+ }
+}
diff --git a/src/Installing/Ini/PickBestSetupIniApproach.php b/src/Installing/Ini/PickBestSetupIniApproach.php
new file mode 100644
index 00000000..83c719d3
--- /dev/null
+++ b/src/Installing/Ini/PickBestSetupIniApproach.php
@@ -0,0 +1,79 @@
+|null */
+ private array|null $memoizedApproachesThatCanBeUsed = null;
+
+ /** @param list $possibleApproaches */
+ public function __construct(
+ private readonly array $possibleApproaches,
+ ) {
+ }
+
+ /** @return list */
+ private function approachesThatCanBeUsed(TargetPlatform $targetPlatform): array
+ {
+ if ($this->memoizedApproachesThatCanBeUsed === null) {
+ $this->memoizedApproachesThatCanBeUsed = array_values(array_filter(
+ $this->possibleApproaches,
+ static fn (SetupIniApproach $approach) => $approach->canBeUsed($targetPlatform),
+ ));
+ }
+
+ return $this->memoizedApproachesThatCanBeUsed;
+ }
+
+ public function canBeUsed(TargetPlatform $targetPlatform): bool
+ {
+ return count($this->approachesThatCanBeUsed($targetPlatform)) > 0;
+ }
+
+ public function setup(
+ TargetPlatform $targetPlatform,
+ DownloadedPackage $downloadedPackage,
+ BinaryFile $binaryFile,
+ OutputInterface $output,
+ ): bool {
+ $approaches = $this->approachesThatCanBeUsed($targetPlatform);
+
+ if (count($approaches) === 0) {
+ $output->writeln('No INI setup approaches can be used on this platform.', OutputInterface::VERBOSITY_VERBOSE);
+
+ return false;
+ }
+
+ foreach ($approaches as $approach) {
+ $output->writeln(
+ sprintf(
+ 'Trying to enable extension using %s',
+ (new ReflectionClass($approach))->getShortName(),
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+ if ($approach->setup($targetPlatform, $downloadedPackage, $binaryFile, $output)) {
+ return true;
+ }
+ }
+
+ $output->writeln('None of the INI setup approaches succeeded.', OutputInterface::VERBOSITY_VERBOSE);
+
+ return false;
+ }
+}
diff --git a/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php b/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php
new file mode 100644
index 00000000..89f36819
--- /dev/null
+++ b/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php
@@ -0,0 +1,38 @@
+phpBinaryPath->assertExtensionIsLoadedInRuntime(
+ $downloadedPackage->package->extensionName,
+ $output,
+ );
+
+ return true;
+ } catch (ExtensionIsNotLoaded) {
+ return false;
+ }
+ }
+}
diff --git a/src/Installing/Ini/SetupIniApproach.php b/src/Installing/Ini/SetupIniApproach.php
new file mode 100644
index 00000000..37870e90
--- /dev/null
+++ b/src/Installing/Ini/SetupIniApproach.php
@@ -0,0 +1,31 @@
+phpBinaryPath->additionalIniDirectory() !== null;
+ }
+
+ public function setup(
+ TargetPlatform $targetPlatform,
+ DownloadedPackage $downloadedPackage,
+ BinaryFile $binaryFile,
+ OutputInterface $output,
+ ): bool {
+ $additionalIniFilesPath = $targetPlatform->phpBinaryPath->additionalIniDirectory();
+
+ /** In practice, this shouldn't happen since {@see canBeUsed()} checks this */
+ if ($additionalIniFilesPath === null) {
+ return false;
+ }
+
+ $expectedIniFile = sprintf(
+ '%s%s%d-%s.ini',
+ rtrim($additionalIniFilesPath, DIRECTORY_SEPARATOR),
+ DIRECTORY_SEPARATOR,
+ $downloadedPackage->package->priority,
+ $downloadedPackage->package->extensionName->name(),
+ );
+
+ $pieCreatedTheIniFile = false;
+ if (! file_exists($expectedIniFile)) {
+ $output->writeln(
+ sprintf(
+ 'Creating new INI file based on extension priority: %s',
+ $expectedIniFile,
+ ),
+ OutputInterface::VERBOSITY_VERY_VERBOSE,
+ );
+ $pieCreatedTheIniFile = true;
+ touch($expectedIniFile);
+ }
+
+ $addingExtensionWasSuccessful = ($this->checkAndAddExtensionToIniIfNeeded)(
+ $expectedIniFile,
+ $targetPlatform,
+ $downloadedPackage,
+ $output,
+ null,
+ );
+
+ if (! $addingExtensionWasSuccessful && $pieCreatedTheIniFile) {
+ unlink($expectedIniFile);
+ }
+
+ return $addingExtensionWasSuccessful;
+ }
+}
diff --git a/src/Installing/Ini/StandardSinglePhpIni.php b/src/Installing/Ini/StandardSinglePhpIni.php
new file mode 100644
index 00000000..0eb046e8
--- /dev/null
+++ b/src/Installing/Ini/StandardSinglePhpIni.php
@@ -0,0 +1,46 @@
+phpBinaryPath->loadedIniConfigurationFile() !== null;
+ }
+
+ public function setup(
+ TargetPlatform $targetPlatform,
+ DownloadedPackage $downloadedPackage,
+ BinaryFile $binaryFile,
+ OutputInterface $output,
+ ): bool {
+ $ini = $targetPlatform->phpBinaryPath->loadedIniConfigurationFile();
+
+ /** In practice, this shouldn't happen since {@see canBeUsed()} checks this */
+ if ($ini === null) {
+ return false;
+ }
+
+ return ($this->checkAndAddExtensionToIniIfNeeded)(
+ $ini,
+ $targetPlatform,
+ $downloadedPackage,
+ $output,
+ null,
+ );
+ }
+}
diff --git a/src/Installing/Install.php b/src/Installing/Install.php
index 71e2849d..17982b41 100644
--- a/src/Installing/Install.php
+++ b/src/Installing/Install.php
@@ -20,5 +20,6 @@ public function __invoke(
DownloadedPackage $downloadedPackage,
TargetPlatform $targetPlatform,
OutputInterface $output,
+ bool $attemptToSetupIniFile,
): BinaryFile;
}
diff --git a/src/Installing/SetupIniFile.php b/src/Installing/SetupIniFile.php
new file mode 100644
index 00000000..d6ca58bb
--- /dev/null
+++ b/src/Installing/SetupIniFile.php
@@ -0,0 +1,53 @@
+setupIniApproach->canBeUsed($targetPlatform)
+ && $this->setupIniApproach->setup($targetPlatform, $downloadedPackage, $binaryFile, $output)
+ ) {
+ $output->writeln(sprintf(
+ '✅ Extension is enabled and loaded in %s',
+ $targetPlatform->phpBinaryPath->phpBinaryPath,
+ ));
+ } else {
+ if (! $attemptToSetupIniFile) {
+ $output->writeln('Automatic extension enabling was skipped.', OutputInterface::VERBOSITY_VERBOSE);
+ }
+
+ $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(),
+ ));
+ }
+ }
+}
diff --git a/src/Installing/UnixInstall.php b/src/Installing/UnixInstall.php
index 6411e6b5..da831eed 100644
--- a/src/Installing/UnixInstall.php
+++ b/src/Installing/UnixInstall.php
@@ -6,7 +6,6 @@
use Php\Pie\BinaryFile;
use Php\Pie\Downloading\DownloadedPackage;
-use Php\Pie\ExtensionType;
use Php\Pie\Platform\TargetPlatform;
use Php\Pie\Util\Process;
use RuntimeException;
@@ -22,8 +21,16 @@ final class UnixInstall implements Install
{
private const MAKE_INSTALL_TIMEOUT_SECS = 60; // 1 minute
- public function __invoke(DownloadedPackage $downloadedPackage, TargetPlatform $targetPlatform, OutputInterface $output): BinaryFile
+ public function __construct(private readonly SetupIniFile $setupIniFile)
{
+ }
+
+ public function __invoke(
+ DownloadedPackage $downloadedPackage,
+ TargetPlatform $targetPlatform,
+ OutputInterface $output,
+ bool $attemptToSetupIniFile,
+ ): BinaryFile {
$targetExtensionPath = $targetPlatform->phpBinaryPath->extensionPath();
$sharedObjectName = $downloadedPackage->package->extensionName->name() . '.so';
@@ -63,17 +70,16 @@ public function __invoke(DownloadedPackage $downloadedPackage, TargetPlatform $t
$output->writeln('Install complete: ' . $expectedSharedObjectLocation);
- /**
- * @link https://github.com/php/pie/issues/20
- *
- * @todo this should be improved in future to try to automatically set up the ext
- */
- $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(),
- ));
-
- return BinaryFile::fromFileWithSha256Checksum($expectedSharedObjectLocation);
+ $binaryFile = BinaryFile::fromFileWithSha256Checksum($expectedSharedObjectLocation);
+
+ ($this->setupIniFile)(
+ $targetPlatform,
+ $downloadedPackage,
+ $binaryFile,
+ $output,
+ $attemptToSetupIniFile,
+ );
+
+ return $binaryFile;
}
}
diff --git a/src/Installing/WindowsInstall.php b/src/Installing/WindowsInstall.php
index a9f6c3c1..7ff5ef1d 100644
--- a/src/Installing/WindowsInstall.php
+++ b/src/Installing/WindowsInstall.php
@@ -31,8 +31,16 @@
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
final class WindowsInstall implements Install
{
- public function __invoke(DownloadedPackage $downloadedPackage, TargetPlatform $targetPlatform, OutputInterface $output): BinaryFile
+ public function __construct(private readonly SetupIniFile $setupIniFile)
{
+ }
+
+ public function __invoke(
+ DownloadedPackage $downloadedPackage,
+ TargetPlatform $targetPlatform,
+ OutputInterface $output,
+ bool $attemptToSetupIniFile,
+ ): BinaryFile {
$extractedSourcePath = $downloadedPackage->extractedSourcePath;
$sourceDllName = WindowsExtensionAssetName::determineDllName($targetPlatform, $downloadedPackage);
$sourcePdbName = str_replace('.dll', '.pdb', $sourceDllName);
@@ -82,7 +90,17 @@ public function __invoke(DownloadedPackage $downloadedPackage, TargetPlatform $t
$downloadedPackage->package->extensionName->name(),
));
- return BinaryFile::fromFileWithSha256Checksum($destinationDllName);
+ $binaryFile = BinaryFile::fromFileWithSha256Checksum($destinationDllName);
+
+ ($this->setupIniFile)(
+ $targetPlatform,
+ $downloadedPackage,
+ $binaryFile,
+ $output,
+ $attemptToSetupIniFile,
+ );
+
+ return $binaryFile;
}
/**
diff --git a/src/Platform/TargetPhp/Exception/ExtensionIsNotLoaded.php b/src/Platform/TargetPhp/Exception/ExtensionIsNotLoaded.php
new file mode 100644
index 00000000..7955186f
--- /dev/null
+++ b/src/Platform/TargetPhp/Exception/ExtensionIsNotLoaded.php
@@ -0,0 +1,23 @@
+name(),
+ $php->phpBinaryPath,
+ ));
+ }
+}
diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php
index 7589b2fa..f421b65a 100644
--- a/src/Platform/TargetPhp/PhpBinaryPath.php
+++ b/src/Platform/TargetPhp/PhpBinaryPath.php
@@ -6,22 +6,26 @@
use Composer\Semver\VersionParser;
use Composer\Util\Platform;
+use Php\Pie\ExtensionName;
use Php\Pie\Platform\Architecture;
use Php\Pie\Platform\OperatingSystem;
use Php\Pie\Platform\OperatingSystemFamily;
use Php\Pie\Util\Process;
use RuntimeException;
+use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\PhpExecutableFinder;
use Webmozart\Assert\Assert;
use function array_combine;
use function array_key_exists;
+use function array_keys;
use function array_map;
use function assert;
use function dirname;
use function explode;
use function file_exists;
use function implode;
+use function in_array;
use function is_dir;
use function is_executable;
use function preg_match;
@@ -112,6 +116,58 @@ public function extensionPath(): string
throw new RuntimeException('Could not determine extension path for ' . $this->phpBinaryPath);
}
+ public function assertExtensionIsLoadedInRuntime(ExtensionName $extension, OutputInterface|null $output = null): void
+ {
+ if (! in_array($extension->name(), array_keys($this->extensions()))) {
+ throw Exception\ExtensionIsNotLoaded::fromExpectedExtension(
+ $this,
+ $extension,
+ );
+ }
+
+ if ($output === null) {
+ return;
+ }
+
+ $output->writeln(
+ sprintf(
+ 'Successfully asserted that extension %s is loaded in runtime.',
+ $extension->name(),
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+ }
+
+ /** @return non-empty-string|null */
+ public function additionalIniDirectory(): string|null
+ {
+ if (
+ preg_match('/Scan this dir for additional \.ini files([ =>\t]*)(.*)/', $this->phpinfo(), $m)
+ && array_key_exists(2, $m)
+ && $m[2] !== ''
+ && $m[2] !== '(none)'
+ ) {
+ return $m[2];
+ }
+
+ return null;
+ }
+
+ /** @return non-empty-string|null */
+ public function loadedIniConfigurationFile(): string|null
+ {
+ if (
+ preg_match('/Loaded Configuration File([ =>\t]*)(.*)/', $this->phpinfo(), $m)
+ && array_key_exists(2, $m)
+ && $m[2] !== ''
+ && $m[2] !== '(none)'
+ ) {
+ return $m[2];
+ }
+
+ return null;
+ }
+
/**
* Returns a map where the key is the name of the extension and the value is the version ('0' if not defined)
*
diff --git a/test/assets/docker-php-ext-enable/bad b/test/assets/docker-php-ext-enable/bad
new file mode 100755
index 00000000..33514cfc
--- /dev/null
+++ b/test/assets/docker-php-ext-enable/bad
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+echo "something bad happened"
+exit 1
diff --git a/test/assets/docker-php-ext-enable/good b/test/assets/docker-php-ext-enable/good
new file mode 100755
index 00000000..5e2e73e3
--- /dev/null
+++ b/test/assets/docker-php-ext-enable/good
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+echo "hi"
+exit 0
diff --git a/test/assets/example_ini_files/with_commented_extension.ini b/test/assets/example_ini_files/with_commented_extension.ini
new file mode 100644
index 00000000..2beb79d2
--- /dev/null
+++ b/test/assets/example_ini_files/with_commented_extension.ini
@@ -0,0 +1 @@
+;extension=foobar
diff --git a/test/assets/example_ini_files/with_extension.ini b/test/assets/example_ini_files/with_extension.ini
new file mode 100644
index 00000000..40e7397e
--- /dev/null
+++ b/test/assets/example_ini_files/with_extension.ini
@@ -0,0 +1 @@
+extension=foobar
diff --git a/test/assets/example_ini_files/with_zend_extension.ini b/test/assets/example_ini_files/with_zend_extension.ini
new file mode 100644
index 00000000..d2b87581
--- /dev/null
+++ b/test/assets/example_ini_files/with_zend_extension.ini
@@ -0,0 +1 @@
+zend_extension=foobar
diff --git a/test/assets/example_ini_files/without_extension.ini b/test/assets/example_ini_files/without_extension.ini
new file mode 100644
index 00000000..131f894d
--- /dev/null
+++ b/test/assets/example_ini_files/without_extension.ini
@@ -0,0 +1 @@
+html_errors=1
diff --git a/test/assets/phpenmod/bad b/test/assets/phpenmod/bad
new file mode 100755
index 00000000..33514cfc
--- /dev/null
+++ b/test/assets/phpenmod/bad
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+echo "something bad happened"
+exit 1
diff --git a/test/assets/phpenmod/good b/test/assets/phpenmod/good
new file mode 100755
index 00000000..5e2e73e3
--- /dev/null
+++ b/test/assets/phpenmod/good
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+echo "hi"
+exit 0
diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php
index 16076023..801dbe9b 100644
--- a/test/behaviour/CliContext.php
+++ b/test/behaviour/CliContext.php
@@ -17,6 +17,7 @@
/** @psalm-api */
class CliContext implements Context
{
+ private const PHP_BINARY = 'php';
private string|null $output = null;
private int|null $exitCode = null;
/** @var list */
@@ -37,7 +38,7 @@ public function iRunACommandToDownloadSpecificVersionOfAnExtension(string $versi
/** @param list $command */
public function runPieCommand(array $command): void
{
- $pieCommand = array_merge(['php', ...$this->phpArguments, 'bin/pie'], $command);
+ $pieCommand = array_merge([self::PHP_BINARY, ...$this->phpArguments, 'bin/pie'], $command);
$proc = (new Process($pieCommand))->mustRun();
@@ -123,6 +124,8 @@ public function theExtensionShouldHaveBeenInstalled(): void
{
$this->assertCommandSuccessful();
+ Assert::contains($this->output, 'Extension is enabled and loaded');
+
if (Platform::isWindows()) {
Assert::regex($this->output, '#Copied DLL to: [-\\\_:.a-zA-Z0-9]+\\\php_example_pie_extension.dll#');
@@ -130,6 +133,12 @@ public function theExtensionShouldHaveBeenInstalled(): void
}
Assert::regex($this->output, '#Install complete: [-_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('yes', $isExtEnabled);
}
#[Given('I have an invalid extension installed')]
diff --git a/test/integration/Building/UnixBuildTest.php b/test/integration/Building/UnixBuildTest.php
index e8e8c61c..976ca7a0 100644
--- a/test/integration/Building/UnixBuildTest.php
+++ b/test/integration/Building/UnixBuildTest.php
@@ -50,6 +50,7 @@ public function testUnixBuildCanBuildExtension(): void
null,
null,
null,
+ 99,
),
self::TEST_EXTENSION_PATH,
);
@@ -105,6 +106,7 @@ public function testUnixBuildWillThrowExceptionWhenExpectedBinaryNameMismatches(
null,
null,
null,
+ 99,
),
self::TEST_EXTENSION_PATH,
);
@@ -148,6 +150,7 @@ public function testUnixBuildCanBuildExtensionWithBuildPath(): void
'pie_test_ext',
null,
null,
+ 99,
),
dirname(self::TEST_EXTENSION_PATH),
);
@@ -207,6 +210,7 @@ public function testCleanupDoesNotCleanWhenConfigureIsMissing(): void
null,
null,
null,
+ 99,
),
self::TEST_EXTENSION_PATH,
);
@@ -251,6 +255,7 @@ public function testVerboseOutputShowsCleanupMessages(): void
null,
null,
null,
+ 99,
),
self::TEST_EXTENSION_PATH,
);
diff --git a/test/integration/Command/InstallCommandTest.php b/test/integration/Command/InstallCommandTest.php
index c6ed049a..3257ae95 100644
--- a/test/integration/Command/InstallCommandTest.php
+++ b/test/integration/Command/InstallCommandTest.php
@@ -79,7 +79,11 @@ public function testInstallCommandWillInstallCompatibleExtensionNonWindows(strin
}
$this->commandTester->execute(
- ['requested-package-and-version' => self::TEST_PACKAGE, '--with-php-config' => $phpConfigPath],
+ [
+ 'requested-package-and-version' => self::TEST_PACKAGE,
+ '--with-php-config' => $phpConfigPath,
+ '--skip-enable-extension' => true,
+ ],
['verbosity' => BufferedOutput::VERBOSITY_VERY_VERBOSE],
);
@@ -112,7 +116,10 @@ public function testInstallCommandWillInstallCompatibleExtensionNonWindows(strin
#[RequiresOperatingSystemFamily('Windows')]
public function testInstallCommandWillInstallCompatibleExtensionWindows(): void
{
- $this->commandTester->execute(['requested-package-and-version' => self::TEST_PACKAGE]);
+ $this->commandTester->execute([
+ 'requested-package-and-version' => self::TEST_PACKAGE,
+ '--skip-enable-extension' => true,
+ ]);
$this->commandTester->assertCommandIsSuccessful();
diff --git a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php
index c708dff1..1477b39c 100644
--- a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php
+++ b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php
@@ -90,6 +90,7 @@ public function testDependenciesAreResolvedToExpectedVersions(
PieOperation::Resolve,
[],
null,
+ false,
),
),
$targetPlatform,
diff --git a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php
index df71559c..53cd9b9f 100644
--- a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php
+++ b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php
@@ -56,6 +56,7 @@ public function testDeterminingReleaseAssetUrlForWindows(): void
null,
null,
null,
+ 99,
);
$io = $this->createMock(IOInterface::class);
diff --git a/test/integration/Installing/UnixInstallTest.php b/test/integration/Installing/UnixInstallTest.php
index 049a7af4..ca28ee0d 100644
--- a/test/integration/Installing/UnixInstallTest.php
+++ b/test/integration/Installing/UnixInstallTest.php
@@ -12,6 +12,8 @@
use Php\Pie\Downloading\DownloadedPackage;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
+use Php\Pie\Installing\Ini\PickBestSetupIniApproach;
+use Php\Pie\Installing\SetupIniFile;
use Php\Pie\Installing\UnixInstall;
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
use Php\Pie\Platform\TargetPlatform;
@@ -93,6 +95,7 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void
null,
null,
null,
+ 99,
),
self::TEST_EXTENSION_PATH,
);
@@ -105,10 +108,11 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void
null,
);
- $installedSharedObject = (new UnixInstall())->__invoke(
+ $installedSharedObject = (new UnixInstall(new SetupIniFile(new PickBestSetupIniApproach([]))))->__invoke(
$downloadedPackage,
$targetPlatform,
$output,
+ true,
);
$outputString = $output->fetch();
diff --git a/test/integration/Installing/WindowsInstallTest.php b/test/integration/Installing/WindowsInstallTest.php
index b8664728..4d043415 100644
--- a/test/integration/Installing/WindowsInstallTest.php
+++ b/test/integration/Installing/WindowsInstallTest.php
@@ -9,6 +9,8 @@
use Php\Pie\Downloading\DownloadedPackage;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
+use Php\Pie\Installing\Ini\PickBestSetupIniApproach;
+use Php\Pie\Installing\SetupIniFile;
use Php\Pie\Installing\WindowsInstall;
use Php\Pie\Platform\Architecture;
use Php\Pie\Platform\OperatingSystem;
@@ -57,6 +59,7 @@ public function testWindowsInstallCanInstallExtension(): void
null,
null,
null,
+ 99,
),
self::TEST_EXTENSION_PATH,
);
@@ -73,9 +76,9 @@ public function testWindowsInstallCanInstallExtension(): void
$phpPath = dirname($targetPlatform->phpBinaryPath->phpBinaryPath);
$extensionPath = $targetPlatform->phpBinaryPath->extensionPath();
- $installer = new WindowsInstall();
+ $installer = new WindowsInstall(new SetupIniFile(new PickBestSetupIniApproach([])));
- $installedDll = $installer->__invoke($downloadedPackage, $targetPlatform, $output);
+ $installedDll = $installer->__invoke($downloadedPackage, $targetPlatform, $output, true);
self::assertSame($extensionPath . '\php_pie_test_ext.dll', $installedDll->filePath);
$outputString = $output->fetch();
diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php
index 04da3504..34183746 100644
--- a/test/unit/Command/CommandHelperTest.php
+++ b/test/unit/Command/CommandHelperTest.php
@@ -112,6 +112,7 @@ public function testProcessingConfigureOptionsFromInput(): void
null,
null,
null,
+ 99,
);
$inputDefinition = new InputDefinition();
$inputDefinition->addOption(new InputOption('with-stuff', null, InputOption::VALUE_REQUIRED));
diff --git a/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php b/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php
index 1b186ad5..a0137f5b 100644
--- a/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php
+++ b/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php
@@ -68,6 +68,7 @@ public function testDownloadWithoutBuildAndInstall(): void
PieOperation::Download,
['--foo', '--bar="yes"'],
null,
+ false,
);
$composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3');
$installPath = '/path/to/install';
@@ -109,6 +110,7 @@ public function testDownloadAndBuildWithoutInstall(): void
PieOperation::Build,
['--foo', '--bar="yes"'],
null,
+ false,
);
$composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3');
$installPath = '/path/to/install';
@@ -153,6 +155,7 @@ public function testDownloadBuildAndInstall(): void
PieOperation::Install,
['--foo', '--bar="yes"'],
null,
+ false,
);
$composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3');
$installPath = '/path/to/install';
diff --git a/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php b/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php
index 7151bb35..48790e74 100644
--- a/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php
+++ b/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php
@@ -64,6 +64,7 @@ public function testMetadataForDownloads(): void
PieOperation::Build,
['--foo', '--bar="yes"'],
null,
+ false,
),
clone $package,
);
@@ -102,6 +103,7 @@ public function testMetadataForBuilds(): void
PieOperation::Build,
['--foo', '--bar="yes"'],
new PhpizePath('/path/to/phpize'),
+ false,
),
clone $package,
new BinaryFile('/path/to/built', 'sha256-checksum-value'),
diff --git a/test/unit/ComposerIntegration/OverrideWindowsUrlInstallListenerTest.php b/test/unit/ComposerIntegration/OverrideWindowsUrlInstallListenerTest.php
index 4022d462..8e9d2697 100644
--- a/test/unit/ComposerIntegration/OverrideWindowsUrlInstallListenerTest.php
+++ b/test/unit/ComposerIntegration/OverrideWindowsUrlInstallListenerTest.php
@@ -80,6 +80,7 @@ public function testEventListenerRegistration(): void
PieOperation::Install,
[],
null,
+ false,
),
);
}
@@ -125,6 +126,7 @@ public function testWindowsUrlInstallerDoesNotRunOnNonWindows(): void
PieOperation::Install,
[],
null,
+ false,
),
))($installerEvent);
@@ -182,6 +184,7 @@ public function testDistUrlIsUpdatedForWindowsInstallers(): void
PieOperation::Install,
[],
null,
+ false,
),
))($installerEvent);
diff --git a/test/unit/DependencyResolver/PackageTest.php b/test/unit/DependencyResolver/PackageTest.php
index 79736362..fc99924a 100644
--- a/test/unit/DependencyResolver/PackageTest.php
+++ b/test/unit/DependencyResolver/PackageTest.php
@@ -140,6 +140,7 @@ public function testGithubOrgAndRepo(string $composerPackageName, string|null $d
null,
null,
null,
+ 99,
);
self::assertSame($expectedGithubOrgAndRepo, $package->githubOrgAndRepository());
diff --git a/test/unit/Downloading/DownloadedPackageTest.php b/test/unit/Downloading/DownloadedPackageTest.php
index 8cdc941c..c4edf0b2 100644
--- a/test/unit/Downloading/DownloadedPackageTest.php
+++ b/test/unit/Downloading/DownloadedPackageTest.php
@@ -35,6 +35,7 @@ public function testFromPackageAndExtractedPath(): void
null,
null,
null,
+ 99,
);
$extractedSourcePath = uniqid('/path/to/downloaded/package', true);
@@ -60,6 +61,7 @@ public function testFromPackageAndExtractedPathWithBuildPath(): void
'Downloading',
null,
null,
+ 99,
);
$extractedSourcePath = realpath(__DIR__ . '/../');
diff --git a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php
index 3eb2e69a..e25bcecb 100644
--- a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php
+++ b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php
@@ -36,6 +36,7 @@ public function testForPackage(): void
null,
null,
null,
+ 99,
);
$exception = CouldNotFindReleaseAsset::forPackage($package, ['something.zip', 'something2.zip']);
@@ -58,6 +59,7 @@ public function testForPackageWithMissingTag(): void
null,
null,
null,
+ 99,
);
$exception = CouldNotFindReleaseAsset::forPackageWithMissingTag($package);
diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php
index b618b927..34d8e25a 100644
--- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php
+++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php
@@ -84,6 +84,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void
null,
null,
null,
+ 99,
);
$releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation');
@@ -146,6 +147,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThr
null,
null,
null,
+ 99,
);
$releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation');
@@ -189,6 +191,7 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF
null,
null,
null,
+ 99,
);
$releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation');
diff --git a/test/unit/Installing/Ini/AddExtensionToTheIniFileTest.php b/test/unit/Installing/Ini/AddExtensionToTheIniFileTest.php
new file mode 100644
index 00000000..3f6f3f2e
--- /dev/null
+++ b/test/unit/Installing/Ini/AddExtensionToTheIniFileTest.php
@@ -0,0 +1,293 @@
+output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE);
+ $this->mockPhpBinary = $this->createMock(PhpBinaryPath::class);
+ }
+
+ public function testReturnsFalseWhenFileIsNotWritable(): void
+ {
+ if (TargetPlatform::isRunningAsRoot()) {
+ self::markTestSkipped('Test cannot be run as root, as root can always write files');
+ }
+
+ $unwritableFilename = tempnam(sys_get_temp_dir(), 'PIE_unwritable_ini_file');
+ touch($unwritableFilename);
+ chmod($unwritableFilename, 000);
+
+ try {
+ self::assertFalse((new AddExtensionToTheIniFile())(
+ $unwritableFilename,
+ new Package(
+ $this->createMock(CompletePackage::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foobar'),
+ 'foo/bar',
+ '1.0.0',
+ null,
+ [],
+ true,
+ true,
+ null,
+ null,
+ null,
+ 99,
+ ),
+ $this->mockPhpBinary,
+ $this->output,
+ null,
+ ));
+
+ self::assertStringContainsString(
+ sprintf('PHP is configured to use %s, but it is not writable by PIE.', $unwritableFilename),
+ $this->output->fetch(),
+ );
+ } finally {
+ chmod($unwritableFilename, 644);
+ unlink($unwritableFilename);
+ }
+ }
+
+ #[RequiresOperatingSystemFamily('Linux')]
+ public function testReturnsFalseWhenExistingIniCouldNotBeRead(): void
+ {
+ if (TargetPlatform::isRunningAsRoot()) {
+ self::markTestSkipped('Test cannot be run as root, as root can always read files');
+ }
+
+ $unreadableIniFile = tempnam(sys_get_temp_dir(), 'PIE_unreadable_ini_file');
+ touch($unreadableIniFile);
+ chmod($unreadableIniFile, 222);
+
+ try {
+ self::assertFalse((new AddExtensionToTheIniFile())(
+ $unreadableIniFile,
+ new Package(
+ $this->createMock(CompletePackage::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foobar'),
+ 'foo/bar',
+ '1.0.0',
+ null,
+ [],
+ true,
+ true,
+ null,
+ null,
+ null,
+ 99,
+ ),
+ $this->mockPhpBinary,
+ $this->output,
+ null,
+ ));
+
+ self::assertStringContainsString(
+ sprintf('Tried making a backup of %s but could not read it, aborting enablement of extension', $unreadableIniFile),
+ $this->output->fetch(),
+ );
+ } finally {
+ chmod($unreadableIniFile, 644);
+ unlink($unreadableIniFile);
+ }
+ }
+
+ public function testReturnsFalseWhenExtensionWasAddedButPhpRuntimeDidNotLoadExtension(): void
+ {
+ $extensionName = ExtensionName::normaliseFromString('foobar');
+ $originalIniContent = "; some comment\nerror_reporting=E_ALL\n";
+
+ $iniFile = tempnam(sys_get_temp_dir(), 'PIE_ini_file');
+ file_put_contents($iniFile, $originalIniContent);
+
+ /**
+ * @psalm-suppress PossiblyNullFunctionCall
+ * @psalm-suppress UndefinedThisPropertyAssignment
+ */
+ (fn () => $this->phpBinaryPath = '/path/to/php')
+ ->bindTo($this->mockPhpBinary, PhpBinaryPath::class)();
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('assertExtensionIsLoadedInRuntime')
+ ->willThrowException(
+ ExtensionIsNotLoaded::fromExpectedExtension($this->mockPhpBinary, $extensionName),
+ );
+
+ try {
+ self::assertFalse((new AddExtensionToTheIniFile())(
+ $iniFile,
+ new Package(
+ $this->createMock(CompletePackage::class),
+ ExtensionType::PhpModule,
+ $extensionName,
+ 'foo/bar',
+ '1.0.0',
+ null,
+ [],
+ true,
+ true,
+ null,
+ null,
+ null,
+ 99,
+ ),
+ $this->mockPhpBinary,
+ $this->output,
+ null,
+ ));
+
+ self::assertStringContainsString(
+ 'Something went wrong enabling the foobar extension: Expected extension foobar to be loaded in PHP /path/to/php, but it was not detected.',
+ $this->output->fetch(),
+ );
+
+ // Ensure the original INI file content was restored
+ self::assertSame($originalIniContent, file_get_contents($iniFile));
+ } finally {
+ unlink($iniFile);
+ }
+ }
+
+ public function testReturnsTrueWhenExtensionAdded(): void
+ {
+ $iniFile = tempnam(sys_get_temp_dir(), 'PIE_ini_file');
+ touch($iniFile);
+
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('assertExtensionIsLoadedInRuntime');
+
+ try {
+ self::assertTrue((new AddExtensionToTheIniFile())(
+ $iniFile,
+ new Package(
+ $this->createMock(CompletePackage::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foobar'),
+ 'foo/bar',
+ '1.0.0',
+ null,
+ [],
+ true,
+ true,
+ null,
+ null,
+ null,
+ 99,
+ ),
+ $this->mockPhpBinary,
+ $this->output,
+ null,
+ ));
+
+ $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
+ . 'extension=foobar' . PHP_EOL,
+ $iniContent,
+ );
+
+ self::assertStringContainsString(
+ sprintf('Enabled extension foobar in the INI file %s', $iniFile),
+ $this->output->fetch(),
+ );
+ } finally {
+ unlink($iniFile);
+ }
+ }
+
+ public function testReturnsTrueWhenExtensionAddedWithAdditionalStep(): void
+ {
+ $iniFile = tempnam(sys_get_temp_dir(), 'PIE_ini_file');
+ touch($iniFile);
+
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('assertExtensionIsLoadedInRuntime');
+
+ try {
+ $additionalStepInvoked = false;
+ self::assertTrue((new AddExtensionToTheIniFile())(
+ $iniFile,
+ new Package(
+ $this->createMock(CompletePackage::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foobar'),
+ 'foo/bar',
+ '1.0.0',
+ null,
+ [],
+ true,
+ true,
+ null,
+ null,
+ null,
+ 99,
+ ),
+ $this->mockPhpBinary,
+ $this->output,
+ static function () use (&$additionalStepInvoked): bool {
+ $additionalStepInvoked = true;
+
+ return true;
+ },
+ ));
+
+ self::assertTrue($additionalStepInvoked, 'Failed asserting that the additional step was invoked');
+
+ $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
+ . 'extension=foobar' . PHP_EOL,
+ $iniContent,
+ );
+
+ self::assertStringContainsString(
+ sprintf('Enabled extension foobar in the INI file %s', $iniFile),
+ $this->output->fetch(),
+ );
+ } finally {
+ unlink($iniFile);
+ }
+ }
+}
diff --git a/test/unit/Installing/Ini/CheckAndAddExtensionToIniIfNeededTest.php b/test/unit/Installing/Ini/CheckAndAddExtensionToIniIfNeededTest.php
new file mode 100644
index 00000000..ac31ecc2
--- /dev/null
+++ b/test/unit/Installing/Ini/CheckAndAddExtensionToIniIfNeededTest.php
@@ -0,0 +1,294 @@
+output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE);
+
+ $this->mockPhpBinary = $this->createMock(PhpBinaryPath::class);
+ /**
+ * @psalm-suppress PossiblyNullFunctionCall
+ * @psalm-suppress UndefinedThisPropertyAssignment
+ */
+ (fn () => $this->phpBinaryPath = '/path/to/php')
+ ->bindTo($this->mockPhpBinary, PhpBinaryPath::class)();
+
+ $this->isExtensionAlreadyInTheIniFile = $this->createMock(IsExtensionAlreadyInTheIniFile::class);
+ $this->addExtensionToTheIniFile = $this->createMock(AddExtensionToTheIniFile::class);
+
+ $this->targetPlatform = new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ $this->mockPhpBinary,
+ Architecture::x86_64,
+ ThreadSafetyMode::ThreadSafe,
+ 1,
+ null,
+ );
+
+ $this->downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath(
+ new Package(
+ $this->createMock(CompletePackageInterface::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foobar'),
+ 'foo/bar',
+ '1.2.3',
+ null,
+ [],
+ true,
+ true,
+ null,
+ null,
+ null,
+ 66,
+ ),
+ '/path/to/extracted/source',
+ );
+
+ $this->checkAndAddExtensionToIniIfNeeded = new CheckAndAddExtensionToIniIfNeeded(
+ $this->isExtensionAlreadyInTheIniFile,
+ $this->addExtensionToTheIniFile,
+ );
+ }
+
+ public function testReturnsFalseWhenIniFileDoesNotExist(): void
+ {
+ $this->isExtensionAlreadyInTheIniFile
+ ->expects(self::never())
+ ->method('__invoke');
+
+ $this->mockPhpBinary
+ ->expects(self::never())
+ ->method('assertExtensionIsLoadedInRuntime');
+
+ $this->addExtensionToTheIniFile
+ ->expects(self::never())
+ ->method('__invoke');
+
+ self::assertFalse($this->checkAndAddExtensionToIniIfNeeded->__invoke(
+ '/path/to/non/existent/php.ini',
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->output,
+ null,
+ ));
+
+ self::assertStringContainsString(
+ 'PHP is configured to use /path/to/non/existent/php.ini, but it did not exist, or is not readable by PIE.',
+ $this->output->fetch(),
+ );
+ }
+
+ public function testExtensionIsAlreadyEnabledButExtensionDoesNotLoad(): void
+ {
+ $this->isExtensionAlreadyInTheIniFile
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName)
+ ->willReturn(true);
+
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('assertExtensionIsLoadedInRuntime')
+ ->with($this->downloadedPackage->package->extensionName, $this->output)
+ ->willThrowException(ExtensionIsNotLoaded::fromExpectedExtension(
+ $this->mockPhpBinary,
+ $this->downloadedPackage->package->extensionName,
+ ));
+
+ $this->addExtensionToTheIniFile
+ ->expects(self::never())
+ ->method('__invoke');
+
+ self::assertFalse($this->checkAndAddExtensionToIniIfNeeded->__invoke(
+ self::INI_FILE,
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->output,
+ null,
+ ));
+
+ $output = $this->output->fetch();
+ self::assertStringContainsString(
+ 'Extension is already enabled in the INI file',
+ $output,
+ );
+ self::assertStringContainsString(
+ 'Something went wrong verifying the foobar extension is enabled: Expected extension foobar to be loaded in PHP /path/to/php, but it was not detected.',
+ $output,
+ );
+ }
+
+ public function testExtensionIsAlreadyEnabledAndExtensionLoaded(): void
+ {
+ $this->isExtensionAlreadyInTheIniFile
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName)
+ ->willReturn(true);
+
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('assertExtensionIsLoadedInRuntime')
+ ->with($this->downloadedPackage->package->extensionName, $this->output);
+
+ $this->addExtensionToTheIniFile
+ ->expects(self::never())
+ ->method('__invoke');
+
+ self::assertTrue($this->checkAndAddExtensionToIniIfNeeded->__invoke(
+ self::INI_FILE,
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->output,
+ null,
+ ));
+
+ $output = $this->output->fetch();
+ self::assertStringContainsString(
+ 'Extension is already enabled in the INI file',
+ $output,
+ );
+ }
+
+ public function testExtensionIsAlreadyEnabledWithAdditionalStepAndExtensionLoaded(): void
+ {
+ $this->isExtensionAlreadyInTheIniFile
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName)
+ ->willReturn(true);
+
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('assertExtensionIsLoadedInRuntime')
+ ->with($this->downloadedPackage->package->extensionName, $this->output);
+
+ $this->addExtensionToTheIniFile
+ ->expects(self::never())
+ ->method('__invoke');
+
+ $additionalStepInvoked = false;
+ self::assertTrue($this->checkAndAddExtensionToIniIfNeeded->__invoke(
+ self::INI_FILE,
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->output,
+ static function () use (&$additionalStepInvoked): bool {
+ $additionalStepInvoked = true;
+
+ return true;
+ },
+ ));
+
+ self::assertTrue($additionalStepInvoked, 'Failed asserting that the additional step was invoked');
+
+ $output = $this->output->fetch();
+ self::assertStringContainsString(
+ 'Extension is already enabled in the INI file',
+ $output,
+ );
+ }
+
+ public function testExtensionIsNotYetAdded(): void
+ {
+ $this->isExtensionAlreadyInTheIniFile
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName)
+ ->willReturn(false);
+
+ $this->mockPhpBinary
+ ->expects(self::never())
+ ->method('assertExtensionIsLoadedInRuntime');
+
+ $this->addExtensionToTheIniFile
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(
+ self::INI_FILE,
+ $this->downloadedPackage->package,
+ $this->targetPlatform->phpBinaryPath,
+ $this->output,
+ )
+ ->willReturn(true);
+
+ self::assertTrue($this->checkAndAddExtensionToIniIfNeeded->__invoke(
+ self::INI_FILE,
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->output,
+ null,
+ ));
+ }
+
+ public function testExtensionIsNotYetAddedButFailsToBeAdded(): void
+ {
+ $this->isExtensionAlreadyInTheIniFile
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName)
+ ->willReturn(false);
+
+ $this->mockPhpBinary
+ ->expects(self::never())
+ ->method('assertExtensionIsLoadedInRuntime');
+
+ $this->addExtensionToTheIniFile
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(
+ self::INI_FILE,
+ $this->downloadedPackage->package,
+ $this->targetPlatform->phpBinaryPath,
+ $this->output,
+ )
+ ->willReturn(false);
+
+ self::assertFalse($this->checkAndAddExtensionToIniIfNeeded->__invoke(
+ self::INI_FILE,
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->output,
+ null,
+ ));
+ }
+}
diff --git a/test/unit/Installing/Ini/DockerPhpExtEnableTest.php b/test/unit/Installing/Ini/DockerPhpExtEnableTest.php
new file mode 100644
index 00000000..410190a0
--- /dev/null
+++ b/test/unit/Installing/Ini/DockerPhpExtEnableTest.php
@@ -0,0 +1,176 @@
+output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE);
+
+ $this->mockPhpBinary = $this->createMock(PhpBinaryPath::class);
+ /**
+ * @psalm-suppress PossiblyNullFunctionCall
+ * @psalm-suppress UndefinedThisPropertyAssignment
+ */
+ (fn () => $this->phpBinaryPath = '/path/to/php')
+ ->bindTo($this->mockPhpBinary, PhpBinaryPath::class)();
+
+ $this->targetPlatform = new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ $this->mockPhpBinary,
+ Architecture::x86_64,
+ ThreadSafetyMode::ThreadSafe,
+ 1,
+ null,
+ );
+
+ $this->downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath(
+ new Package(
+ $this->createMock(CompletePackageInterface::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foobar'),
+ 'foo/bar',
+ '1.2.3',
+ null,
+ [],
+ true,
+ true,
+ null,
+ null,
+ null,
+ 99,
+ ),
+ '/path/to/extracted/source',
+ );
+
+ $this->binaryFile = new BinaryFile('/path/to/compiled/extension.so', 'fake checksum');
+ }
+
+ public function testCannotBeUsedWhenDockerPhpExtEnableIsNotInPath(): void
+ {
+ self::assertFalse(
+ (new DockerPhpExtEnable(self::NON_EXISTENT_DOCKER_PHP_EXT_ENABLE))
+ ->canBeUsed($this->targetPlatform),
+ );
+ }
+
+ public function testCanBeUsedWhenDockerPhpExtEnableIsInPath(): void
+ {
+ self::assertTrue(
+ (new DockerPhpExtEnable(self::GOOD_DOCKER_PHP_EXT_ENABLE))
+ ->canBeUsed($this->targetPlatform),
+ );
+ }
+
+ public function testSetupReturnsFalseWhenWhenDockerPhpExtEnableIsNotInPath(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::never())
+ ->method('assertExtensionIsLoadedInRuntime');
+
+ self::assertFalse(
+ (new DockerPhpExtEnable(self::NON_EXISTENT_DOCKER_PHP_EXT_ENABLE))
+ ->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ),
+ );
+ }
+
+ public function testReturnsTrueWhenDockerPhpExtEnableSuccessfullyEnablesExtension(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('assertExtensionIsLoadedInRuntime')
+ ->with($this->downloadedPackage->package->extensionName, $this->output);
+
+ self::assertTrue(
+ (new DockerPhpExtEnable(self::GOOD_DOCKER_PHP_EXT_ENABLE))
+ ->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ),
+ );
+ }
+
+ public function testReturnsFalseWhenDockerPhpExtEnableFailsToBeRun(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::never())
+ ->method('assertExtensionIsLoadedInRuntime');
+
+ self::assertFalse(
+ (new DockerPhpExtEnable(self::BAD_DOCKER_PHP_EXT_ENABLE))
+ ->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ),
+ );
+ }
+
+ public function testReturnsFalseWhenDockerPhpExtEnableFailsToAssertExtensionWasEnabled(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('assertExtensionIsLoadedInRuntime')
+ ->with($this->downloadedPackage->package->extensionName, $this->output)
+ ->willThrowException(ExtensionIsNotLoaded::fromExpectedExtension(
+ $this->mockPhpBinary,
+ $this->downloadedPackage->package->extensionName,
+ ));
+
+ self::assertFalse(
+ (new DockerPhpExtEnable(self::GOOD_DOCKER_PHP_EXT_ENABLE))
+ ->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ),
+ );
+ }
+}
diff --git a/test/unit/Installing/Ini/IsExtensionAlreadyInTheIniFileTest.php b/test/unit/Installing/Ini/IsExtensionAlreadyInTheIniFileTest.php
new file mode 100644
index 00000000..18f1a293
--- /dev/null
+++ b/test/unit/Installing/Ini/IsExtensionAlreadyInTheIniFileTest.php
@@ -0,0 +1,49 @@
+output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE);
+
+ $this->mockPhpBinary = $this->createMock(PhpBinaryPath::class);
+ /**
+ * @psalm-suppress PossiblyNullFunctionCall
+ * @psalm-suppress UndefinedThisPropertyAssignment
+ */
+ (fn () => $this->phpBinaryPath = '/path/to/php')
+ ->bindTo($this->mockPhpBinary, PhpBinaryPath::class)();
+
+ $this->checkAndAddExtensionToIniIfNeeded = $this->createMock(CheckAndAddExtensionToIniIfNeeded::class);
+
+ $this->targetPlatform = new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ $this->mockPhpBinary,
+ Architecture::x86_64,
+ ThreadSafetyMode::ThreadSafe,
+ 1,
+ null,
+ );
+
+ $this->downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath(
+ new Package(
+ $this->createMock(CompletePackageInterface::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foobar'),
+ 'foo/bar',
+ '1.2.3',
+ null,
+ [],
+ true,
+ true,
+ null,
+ null,
+ null,
+ 99,
+ ),
+ '/path/to/extracted/source',
+ );
+
+ $this->binaryFile = new BinaryFile('/path/to/compiled/extension.so', 'fake checksum');
+ }
+
+ #[RequiresOperatingSystemFamily('Windows')]
+ public function testCanBeUsedReturnsFalseOnWindows(): void
+ {
+ self::assertFalse(
+ (new OndrejPhpenmod(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ self::GOOD_PHPENMOD,
+ self::NON_EXISTENT_MODS_AVAILABLE_PATH,
+ ))->canBeUsed($this->targetPlatform),
+ );
+ }
+
+ #[RequiresOperatingSystemFamily('Linux')]
+ public function testCanBeUsedReturnsFalseWhenPhpenmodNotInPath(): void
+ {
+ self::assertFalse(
+ (new OndrejPhpenmod(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ self::NON_EXISTENT_PHPENMOD,
+ self::NON_EXISTENT_MODS_AVAILABLE_PATH,
+ ))->canBeUsed($this->targetPlatform),
+ );
+ }
+
+ #[RequiresOperatingSystemFamily('Linux')]
+ public function testCanBeUsedReturnsTrueWhenPhpenmodInPath(): void
+ {
+ self::assertTrue(
+ (new OndrejPhpenmod(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ self::GOOD_PHPENMOD,
+ self::NON_EXISTENT_MODS_AVAILABLE_PATH,
+ ))->canBeUsed($this->targetPlatform),
+ );
+ }
+
+ #[RequiresOperatingSystemFamily('Windows')]
+ public function testSetupReturnsFalseOnWindows(): void
+ {
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::never())
+ ->method('__invoke');
+
+ self::assertFalse(
+ (new OndrejPhpenmod(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ self::GOOD_PHPENMOD,
+ self::NON_EXISTENT_MODS_AVAILABLE_PATH,
+ ))->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ),
+ );
+ }
+
+ #[RequiresOperatingSystemFamily('Linux')]
+ public function testSetupReturnsFalseWhenPhpenmodNotInPath(): void
+ {
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::never())
+ ->method('__invoke');
+
+ self::assertFalse(
+ (new OndrejPhpenmod(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ self::NON_EXISTENT_PHPENMOD,
+ self::NON_EXISTENT_MODS_AVAILABLE_PATH,
+ ))->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ),
+ );
+ }
+
+ #[RequiresOperatingSystemFamily('Linux')]
+ public function testSetupReturnsFalseWhenAdditionalPhpIniPathNotSet(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('additionalIniDirectory')
+ ->willReturn(null);
+
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::never())
+ ->method('__invoke');
+
+ self::assertFalse(
+ (new OndrejPhpenmod(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ self::GOOD_PHPENMOD,
+ self::NON_EXISTENT_MODS_AVAILABLE_PATH,
+ ))->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ),
+ );
+
+ self::assertStringContainsString(
+ 'Additional INI file path was not set - may not be Ondrej PHP repo',
+ $this->output->fetch(),
+ );
+ }
+
+ #[RequiresOperatingSystemFamily('Linux')]
+ public function testSetupReturnsFalseWhenModsAvailablePathDoesNotExist(): void
+ {
+ $this->mockPhpBinary
+ ->method('additionalIniDirectory')
+ ->willReturn('/value/does/not/matter');
+
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::never())
+ ->method('__invoke');
+
+ self::assertFalse(
+ (new OndrejPhpenmod(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ self::GOOD_PHPENMOD,
+ self::NON_EXISTENT_MODS_AVAILABLE_PATH,
+ ))->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ),
+ );
+
+ self::assertStringContainsString(
+ 'Mods available path ' . self::NON_EXISTENT_MODS_AVAILABLE_PATH . ' does not exist',
+ $this->output->fetch(),
+ );
+ }
+
+ #[RequiresOperatingSystemFamily('Linux')]
+ public function testSetupReturnsFalseWhenModsAvailablePathNotADirectory(): void
+ {
+ $this->mockPhpBinary
+ ->method('additionalIniDirectory')
+ ->willReturn('/value/does/not/matter');
+
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::never())
+ ->method('__invoke');
+
+ self::assertFalse(
+ (new OndrejPhpenmod(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ self::GOOD_PHPENMOD,
+ __FILE__,
+ ))->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ),
+ );
+
+ self::assertStringContainsString(
+ 'Mods available path ' . __FILE__ . ' is not a directory',
+ $this->output->fetch(),
+ );
+ }
+
+ #[RequiresOperatingSystemFamily('Linux')]
+ public function testSetupReturnsFalseWhenModsAvailablePathNotWritable(): void
+ {
+ if (TargetPlatform::isRunningAsRoot()) {
+ self::markTestSkipped('Test cannot be run as root, as root can always write files');
+ }
+
+ $modsAvailablePath = tempnam(sys_get_temp_dir(), 'pie_test_mods_available_path');
+ unlink($modsAvailablePath);
+ mkdir($modsAvailablePath, 000, true);
+
+ $this->mockPhpBinary
+ ->method('additionalIniDirectory')
+ ->willReturn('/value/does/not/matter');
+
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::never())
+ ->method('__invoke');
+
+ self::assertFalse(
+ (new OndrejPhpenmod(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ self::GOOD_PHPENMOD,
+ $modsAvailablePath,
+ ))->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ),
+ );
+
+ self::assertStringContainsString(
+ 'Mods available path ' . $modsAvailablePath . ' is not writable',
+ $this->output->fetch(),
+ );
+
+ rmdir($modsAvailablePath);
+ }
+
+ #[RequiresOperatingSystemFamily('Linux')]
+ public function testSetupReturnsFalseAndRemovesPieCreatedIniFileWhenPhpenmodAdditionalStepFails(): void
+ {
+ $modsAvailablePath = tempnam(sys_get_temp_dir(), 'pie_test_mods_available_path');
+ unlink($modsAvailablePath);
+ mkdir($modsAvailablePath, recursive: true);
+
+ $expectedIniFile = $modsAvailablePath . DIRECTORY_SEPARATOR . 'foobar.ini';
+
+ $this->mockPhpBinary
+ ->method('additionalIniDirectory')
+ ->willReturn('/value/does/not/matter');
+
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(
+ $expectedIniFile,
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->output,
+ self::isType(IsType::TYPE_CALLABLE),
+ )
+ ->willReturnCallback(
+ /** @param callable():bool $additionalEnableStep */
+ static function (
+ string $iniFile,
+ TargetPlatform $targetPlatform,
+ DownloadedPackage $downloadedPackage,
+ OutputInterface $output,
+ callable $additionalEnableStep,
+ ): bool {
+ return $additionalEnableStep();
+ },
+ );
+
+ self::assertFalse(
+ (new OndrejPhpenmod(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ self::BAD_PHPENMOD,
+ $modsAvailablePath,
+ ))->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ),
+ );
+
+ self::assertFileDoesNotExist($expectedIniFile);
+
+ self::assertStringContainsString(
+ 'something bad happened',
+ $this->output->fetch(),
+ );
+
+ rmdir($modsAvailablePath);
+ }
+
+ #[RequiresOperatingSystemFamily('Linux')]
+ public function testSetupReturnsFalseAndRemovesPieCreatedIniFileWhenCheckAndAddExtensionFails(): void
+ {
+ $modsAvailablePath = tempnam(sys_get_temp_dir(), 'pie_test_mods_available_path');
+ unlink($modsAvailablePath);
+ mkdir($modsAvailablePath, recursive: true);
+
+ $expectedIniFile = $modsAvailablePath . DIRECTORY_SEPARATOR . 'foobar.ini';
+
+ $this->mockPhpBinary
+ ->method('additionalIniDirectory')
+ ->willReturn('/value/does/not/matter');
+
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(
+ $expectedIniFile,
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->output,
+ self::isType(IsType::TYPE_CALLABLE),
+ )
+ ->willReturn(false);
+
+ self::assertFalse(
+ (new OndrejPhpenmod(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ self::GOOD_PHPENMOD,
+ $modsAvailablePath,
+ ))->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ),
+ );
+
+ self::assertFileDoesNotExist($expectedIniFile);
+
+ rmdir($modsAvailablePath);
+ }
+
+ #[RequiresOperatingSystemFamily('Linux')]
+ public function testSetupReturnsTrueWhenExtensionIsEnabled(): void
+ {
+ $modsAvailablePath = tempnam(sys_get_temp_dir(), 'pie_test_mods_available_path');
+ unlink($modsAvailablePath);
+ mkdir($modsAvailablePath, recursive: true);
+
+ $expectedIniFile = $modsAvailablePath . DIRECTORY_SEPARATOR . 'foobar.ini';
+
+ $this->mockPhpBinary
+ ->method('additionalIniDirectory')
+ ->willReturn('/value/does/not/matter');
+
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(
+ $expectedIniFile,
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->output,
+ self::isType(IsType::TYPE_CALLABLE),
+ )
+ ->willReturnCallback(
+ /** @param callable():bool $additionalEnableStep */
+ static function (
+ string $iniFile,
+ TargetPlatform $targetPlatform,
+ DownloadedPackage $downloadedPackage,
+ OutputInterface $output,
+ callable $additionalEnableStep,
+ ): bool {
+ return $additionalEnableStep();
+ },
+ );
+
+ self::assertTrue(
+ (new OndrejPhpenmod(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ self::GOOD_PHPENMOD,
+ $modsAvailablePath,
+ ))->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ),
+ );
+
+ self::assertFileExists($expectedIniFile);
+
+ unlink($expectedIniFile);
+ rmdir($modsAvailablePath);
+ }
+}
diff --git a/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php b/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php
new file mode 100644
index 00000000..07d4bd68
--- /dev/null
+++ b/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php
@@ -0,0 +1,204 @@
+canBeUsed($this->targetPlatform()));
+ }
+
+ public function testCannotBeUsedWithAnyApproaches(): void
+ {
+ $one = $this->createMock(SetupIniApproach::class);
+ $one->expects(self::once())->method('canBeUsed')->willReturn(false);
+ $two = $this->createMock(SetupIniApproach::class);
+ $two->expects(self::once())->method('canBeUsed')->willReturn(false);
+
+ self::assertFalse((new PickBestSetupIniApproach([$one, $two]))->canBeUsed($this->targetPlatform()));
+ }
+
+ public function testCanBeUsedWithApproachOne(): void
+ {
+ $one = $this->createMock(SetupIniApproach::class);
+ $one->expects(self::once())->method('canBeUsed')->willReturn(false);
+ $two = $this->createMock(SetupIniApproach::class);
+ $two->expects(self::once())->method('canBeUsed')->willReturn(true);
+
+ self::assertTrue((new PickBestSetupIniApproach([$one, $two]))->canBeUsed($this->targetPlatform()));
+ }
+
+ public function testCanBeUsedWithApproachTwo(): void
+ {
+ $one = $this->createMock(SetupIniApproach::class);
+ $one->expects(self::once())->method('canBeUsed')->willReturn(true);
+ $two = $this->createMock(SetupIniApproach::class);
+ $two->expects(self::once())->method('canBeUsed')->willReturn(false);
+
+ self::assertTrue((new PickBestSetupIniApproach([$one, $two]))->canBeUsed($this->targetPlatform()));
+ }
+
+ public function testCanBeUsedWithAllApproaches(): void
+ {
+ $one = $this->createMock(SetupIniApproach::class);
+ $one->expects(self::once())->method('canBeUsed')->willReturn(true);
+ $two = $this->createMock(SetupIniApproach::class);
+ $two->expects(self::once())->method('canBeUsed')->willReturn(true);
+
+ self::assertTrue((new PickBestSetupIniApproach([$one, $two]))->canBeUsed($this->targetPlatform()));
+ }
+
+ public function testVerboseMessageIsEmittedSettingUpWithoutAnyApproaches(): void
+ {
+ $output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE);
+
+ self::assertFalse((new PickBestSetupIniApproach([]))->setup(
+ $this->targetPlatform(),
+ DownloadedPackage::fromPackageAndExtractedPath(
+ new Package(
+ $this->createMock(CompletePackage::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foo'),
+ 'test-vendor/test-package',
+ '1.2.3',
+ 'https://test-uri/',
+ [],
+ true,
+ true,
+ null,
+ null,
+ null,
+ 99,
+ ),
+ '/path/to/extracted/source',
+ ),
+ new BinaryFile('/path/to/extracted/source/module/foo.so', 'some-checksum'),
+ $output,
+ ));
+
+ $outputString = $output->fetch();
+ self::assertStringContainsString(
+ 'No INI setup approaches can be used on this platform.',
+ $outputString,
+ );
+ }
+
+ public function testWorkingApproachIsUsed(): void
+ {
+ $output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE);
+
+ $one = $this->createMock(SetupIniApproach::class);
+ $one->method('canBeUsed')->willReturn(true);
+ $one->expects(self::once())->method('setup')->willReturn(false);
+ $two = $this->createMock(SetupIniApproach::class);
+ $two->method('canBeUsed')->willReturn(true);
+ $two->expects(self::once())->method('setup')->willReturn(true);
+
+ self::assertTrue((new PickBestSetupIniApproach([$one, $two]))->setup(
+ $this->targetPlatform(),
+ DownloadedPackage::fromPackageAndExtractedPath(
+ new Package(
+ $this->createMock(CompletePackage::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foo'),
+ 'test-vendor/test-package',
+ '1.2.3',
+ 'https://test-uri/',
+ [],
+ true,
+ true,
+ null,
+ null,
+ null,
+ 99,
+ ),
+ '/path/to/extracted/source',
+ ),
+ new BinaryFile('/path/to/extracted/source/module/foo.so', 'some-checksum'),
+ $output,
+ ));
+
+ $outputString = $output->fetch();
+ self::assertStringContainsString(
+ 'Trying to enable extension using MockObject_SetupIniApproach',
+ $outputString,
+ );
+ }
+
+ public function testSetupFailsWhenNoApproachesWork(): void
+ {
+ $output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE);
+
+ $one = $this->createMock(SetupIniApproach::class);
+ $one->method('canBeUsed')->willReturn(true);
+ $one->expects(self::once())->method('setup')->willReturn(false);
+ $two = $this->createMock(SetupIniApproach::class);
+ $two->method('canBeUsed')->willReturn(true);
+ $two->expects(self::once())->method('setup')->willReturn(false);
+
+ self::assertFalse((new PickBestSetupIniApproach([$one, $two]))->setup(
+ $this->targetPlatform(),
+ DownloadedPackage::fromPackageAndExtractedPath(
+ new Package(
+ $this->createMock(CompletePackage::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foo'),
+ 'test-vendor/test-package',
+ '1.2.3',
+ 'https://test-uri/',
+ [],
+ true,
+ true,
+ null,
+ null,
+ null,
+ 99,
+ ),
+ '/path/to/extracted/source',
+ ),
+ new BinaryFile('/path/to/extracted/source/module/foo.so', 'some-checksum'),
+ $output,
+ ));
+
+ $outputString = $output->fetch();
+ self::assertStringContainsString(
+ 'None of the INI setup approaches succeeded.',
+ $outputString,
+ );
+ }
+}
diff --git a/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php b/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php
new file mode 100644
index 00000000..de1795f3
--- /dev/null
+++ b/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php
@@ -0,0 +1,124 @@
+output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE);
+
+ $this->mockPhpBinary = $this->createMock(PhpBinaryPath::class);
+ /**
+ * @psalm-suppress PossiblyNullFunctionCall
+ * @psalm-suppress UndefinedThisPropertyAssignment
+ */
+ (fn () => $this->phpBinaryPath = '/path/to/php')
+ ->bindTo($this->mockPhpBinary, PhpBinaryPath::class)();
+
+ $this->targetPlatform = new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ $this->mockPhpBinary,
+ Architecture::x86_64,
+ ThreadSafetyMode::ThreadSafe,
+ 1,
+ null,
+ );
+
+ $this->downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath(
+ new Package(
+ $this->createMock(CompletePackageInterface::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foobar'),
+ 'foo/bar',
+ '1.2.3',
+ null,
+ [],
+ true,
+ true,
+ null,
+ null,
+ null,
+ 99,
+ ),
+ '/path/to/extracted/source',
+ );
+
+ $this->binaryFile = new BinaryFile('/path/to/compiled/extension.so', 'fake checksum');
+
+ $this->preCheckExtensionAlreadyLoaded = new PreCheckExtensionAlreadyLoaded();
+ }
+
+ public function testCanBeUsed(): void
+ {
+ self::assertTrue($this->preCheckExtensionAlreadyLoaded->canBeUsed(
+ $this->targetPlatform,
+ ));
+ }
+
+ public function testSetupReturnsTrueWhenExtAlreadyRuntimeLoaded(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('assertExtensionIsLoadedInRuntime')
+ ->with($this->downloadedPackage->package->extensionName, $this->output);
+
+ self::assertTrue($this->preCheckExtensionAlreadyLoaded->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ));
+ }
+
+ public function testSetupReturnsFalseWhenExtIsNotRuntimeLoaded(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('assertExtensionIsLoadedInRuntime')
+ ->with($this->downloadedPackage->package->extensionName, $this->output)
+ ->willThrowException(ExtensionIsNotLoaded::fromExpectedExtension(
+ $this->mockPhpBinary,
+ $this->downloadedPackage->package->extensionName,
+ ));
+
+ self::assertFalse($this->preCheckExtensionAlreadyLoaded->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ));
+ }
+}
diff --git a/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php b/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php
new file mode 100644
index 00000000..404a1d04
--- /dev/null
+++ b/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php
@@ -0,0 +1,244 @@
+output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE);
+
+ $this->mockPhpBinary = $this->createMock(PhpBinaryPath::class);
+ /**
+ * @psalm-suppress PossiblyNullFunctionCall
+ * @psalm-suppress UndefinedThisPropertyAssignment
+ */
+ (fn () => $this->phpBinaryPath = '/path/to/php')
+ ->bindTo($this->mockPhpBinary, PhpBinaryPath::class)();
+
+ $this->checkAndAddExtensionToIniIfNeeded = $this->createMock(CheckAndAddExtensionToIniIfNeeded::class);
+
+ $this->targetPlatform = new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ $this->mockPhpBinary,
+ Architecture::x86_64,
+ ThreadSafetyMode::ThreadSafe,
+ 1,
+ null,
+ );
+
+ $this->downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath(
+ new Package(
+ $this->createMock(CompletePackageInterface::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foobar'),
+ 'foo/bar',
+ '1.2.3',
+ null,
+ [],
+ true,
+ true,
+ null,
+ null,
+ null,
+ 99,
+ ),
+ '/path/to/extracted/source',
+ );
+
+ $this->binaryFile = new BinaryFile('/path/to/compiled/extension.so', 'fake checksum');
+
+ $this->standardAdditionalPhpIniDirectory = new StandardAdditionalPhpIniDirectory(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ );
+ }
+
+ public function testCannotBeUsedWithNoDefinedAdditionalPhpIniDirectory(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('additionalIniDirectory')
+ ->willReturn(null);
+
+ self::assertFalse($this->standardAdditionalPhpIniDirectory->canBeUsed($this->targetPlatform));
+ }
+
+ public function testCanBeUsedWithDefinedAdditionalPhpIniDirectory(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('additionalIniDirectory')
+ ->willReturn('/path/to/the/php.d');
+
+ self::assertTrue($this->standardAdditionalPhpIniDirectory->canBeUsed($this->targetPlatform));
+ }
+
+ public function testSetupReturnsWhenAdditionalPhpIniDirectoryIsNotSet(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('additionalIniDirectory')
+ ->willReturn(null);
+
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::never())
+ ->method('__invoke');
+
+ self::assertFalse($this->standardAdditionalPhpIniDirectory->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ));
+ }
+
+ public function testReturnsTrueWhenCheckAndAddExtensionIsInvoked(): void
+ {
+ $additionalPhpIniDirectory = tempnam(sys_get_temp_dir(), 'pie_additional_php_ini_path');
+ unlink($additionalPhpIniDirectory);
+ mkdir($additionalPhpIniDirectory, recursive: true);
+
+ $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '99-foobar.ini';
+
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('additionalIniDirectory')
+ ->willReturn($additionalPhpIniDirectory);
+
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(
+ $expectedIniFile,
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->output,
+ )
+ ->willReturn(true);
+
+ self::assertTrue($this->standardAdditionalPhpIniDirectory->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ));
+ self::assertFileExists($expectedIniFile);
+
+ unlink($expectedIniFile);
+ rmdir($additionalPhpIniDirectory);
+ }
+
+ public function testReturnsFalseAndRemovesPieCreatedIniFileWhenCheckAndAddExtensionIsInvoked(): void
+ {
+ $additionalPhpIniDirectory = tempnam(sys_get_temp_dir(), 'pie_additional_php_ini_path');
+ unlink($additionalPhpIniDirectory);
+ mkdir($additionalPhpIniDirectory, recursive: true);
+
+ $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '99-foobar.ini';
+
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('additionalIniDirectory')
+ ->willReturn($additionalPhpIniDirectory);
+
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(
+ $expectedIniFile,
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->output,
+ )
+ ->willReturn(false);
+
+ self::assertFalse($this->standardAdditionalPhpIniDirectory->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ));
+ self::assertFileDoesNotExist($expectedIniFile);
+
+ rmdir($additionalPhpIniDirectory);
+ }
+
+ public function testReturnsFalseAndLeavesNonPieCreatedIniFileWhenCheckAndAddExtensionIsInvoked(): void
+ {
+ $additionalPhpIniDirectory = tempnam(sys_get_temp_dir(), 'pie_additional_php_ini_path');
+ unlink($additionalPhpIniDirectory);
+ mkdir($additionalPhpIniDirectory, recursive: true);
+
+ $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '99-foobar.ini';
+ touch($expectedIniFile);
+
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('additionalIniDirectory')
+ ->willReturn($additionalPhpIniDirectory);
+
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(
+ $expectedIniFile,
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->output,
+ )
+ ->willReturn(false);
+
+ self::assertFalse($this->standardAdditionalPhpIniDirectory->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ));
+ self::assertFileExists($expectedIniFile);
+
+ unlink($expectedIniFile);
+ rmdir($additionalPhpIniDirectory);
+ }
+}
diff --git a/test/unit/Installing/Ini/StandardSinglePhpIniTest.php b/test/unit/Installing/Ini/StandardSinglePhpIniTest.php
new file mode 100644
index 00000000..34b19daa
--- /dev/null
+++ b/test/unit/Installing/Ini/StandardSinglePhpIniTest.php
@@ -0,0 +1,181 @@
+output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE);
+
+ $this->mockPhpBinary = $this->createMock(PhpBinaryPath::class);
+ /**
+ * @psalm-suppress PossiblyNullFunctionCall
+ * @psalm-suppress UndefinedThisPropertyAssignment
+ */
+ (fn () => $this->phpBinaryPath = '/path/to/php')
+ ->bindTo($this->mockPhpBinary, PhpBinaryPath::class)();
+
+ $this->checkAndAddExtensionToIniIfNeeded = $this->createMock(CheckAndAddExtensionToIniIfNeeded::class);
+
+ $this->targetPlatform = new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ $this->mockPhpBinary,
+ Architecture::x86_64,
+ ThreadSafetyMode::ThreadSafe,
+ 1,
+ null,
+ );
+
+ $this->downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath(
+ new Package(
+ $this->createMock(CompletePackageInterface::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foobar'),
+ 'foo/bar',
+ '1.2.3',
+ null,
+ [],
+ true,
+ true,
+ null,
+ null,
+ null,
+ 99,
+ ),
+ '/path/to/extracted/source',
+ );
+
+ $this->binaryFile = new BinaryFile('/path/to/compiled/extension.so', 'fake checksum');
+
+ $this->standardSinglePhpIni = new StandardSinglePhpIni(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ );
+ }
+
+ public function testCannotBeUsedWithNoDefinedPhpIni(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('loadedIniConfigurationFile')
+ ->willReturn(null);
+
+ self::assertFalse($this->standardSinglePhpIni->canBeUsed($this->targetPlatform));
+ }
+
+ public function testCanBeUsedWithDefinedPhpIni(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('loadedIniConfigurationFile')
+ ->willReturn('/path/to/php.ini');
+
+ self::assertTrue($this->standardSinglePhpIni->canBeUsed($this->targetPlatform));
+ }
+
+ public function testSetupReturnsWhenIniFileIsNotSet(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('loadedIniConfigurationFile')
+ ->willReturn(null);
+
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::never())
+ ->method('__invoke');
+
+ self::assertFalse($this->standardSinglePhpIni->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ));
+ }
+
+ public function testReturnsTrueWhenCheckAndAddExtensionIsInvoked(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('loadedIniConfigurationFile')
+ ->willReturn(self::INI_FILE);
+
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(
+ self::INI_FILE,
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->output,
+ )
+ ->willReturn(true);
+
+ self::assertTrue($this->standardSinglePhpIni->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ));
+ }
+
+ public function testReturnsFalseWhenCheckAndAddExtensionIsInvoked(): void
+ {
+ $this->mockPhpBinary
+ ->expects(self::once())
+ ->method('loadedIniConfigurationFile')
+ ->willReturn(self::INI_FILE);
+
+ $this->checkAndAddExtensionToIniIfNeeded
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(
+ self::INI_FILE,
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->output,
+ )
+ ->willReturn(false);
+
+ self::assertFalse($this->standardSinglePhpIni->setup(
+ $this->targetPlatform,
+ $this->downloadedPackage,
+ $this->binaryFile,
+ $this->output,
+ ));
+ }
+}
diff --git a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php
index 39ffdaa3..5e3bd10f 100644
--- a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php
+++ b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php
@@ -5,22 +5,27 @@
namespace Php\PieUnitTest\Platform\TargetPhp;
use Composer\Util\Platform;
+use Php\Pie\ExtensionName;
use Php\Pie\Platform\Architecture;
use Php\Pie\Platform\OperatingSystem;
use Php\Pie\Platform\OperatingSystemFamily;
+use Php\Pie\Platform\TargetPhp\Exception\ExtensionIsNotLoaded;
use Php\Pie\Platform\TargetPhp\Exception\InvalidPhpBinaryPath;
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Process\PhpExecutableFinder;
use function array_column;
use function array_combine;
use function array_filter;
+use function array_key_exists;
use function array_map;
use function array_unique;
use function assert;
+use function count;
use function defined;
use function dirname;
use function file_exists;
@@ -271,4 +276,32 @@ public function testDifferentVersionsOfPhp(string $phpPath): void
self::assertGreaterThan(0, $php->phpIntSize());
self::assertNotEmpty($php->phpinfo());
}
+
+ public function testAssertExtensionIsLoaded(): void
+ {
+ $php = PhpBinaryPath::fromCurrentProcess();
+ $loadedExtensions = $php->extensions();
+
+ if (! count($loadedExtensions) || ! array_key_exists('Core', $loadedExtensions)) {
+ self::fail('Core extension is not loaded, this is quite unexpected...');
+ }
+
+ $output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE);
+ $php->assertExtensionIsLoadedInRuntime(ExtensionName::normaliseFromString('Core'), $output);
+
+ self::assertStringContainsString(
+ 'Successfully asserted that extension Core is loaded in runtime.',
+ $output->fetch(),
+ );
+ }
+
+ public function testAssertExtensionFailsWhenNotLoaded(): void
+ {
+ $php = PhpBinaryPath::fromCurrentProcess();
+
+ $this->expectException(ExtensionIsNotLoaded::class);
+ $php->assertExtensionIsLoadedInRuntime(ExtensionName::normaliseFromString(
+ 'hopefully_this_extension_name_is_not_real_otherwise_this_test_will_fail',
+ ));
+ }
}
diff --git a/test/unit/Platform/WindowsExtensionAssetNameTest.php b/test/unit/Platform/WindowsExtensionAssetNameTest.php
index f152af9b..75e72a6b 100644
--- a/test/unit/Platform/WindowsExtensionAssetNameTest.php
+++ b/test/unit/Platform/WindowsExtensionAssetNameTest.php
@@ -55,6 +55,7 @@ public function setUp(): void
null,
null,
null,
+ 99,
);
}