diff --git a/bin/pie b/bin/pie index 6d548bb..7a62ca5 100755 --- a/bin/pie +++ b/bin/pie @@ -7,6 +7,7 @@ namespace Php\Pie; use Php\Pie\Command\BuildCommand; use Php\Pie\Command\DownloadCommand; +use Php\Pie\Command\InstallCommand; use Symfony\Component\Console\Application; use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; use Symfony\Component\Console\Input\InputInterface; @@ -23,6 +24,7 @@ $application->setCommandLoader(new ContainerCommandLoader( [ 'download' => DownloadCommand::class, 'build' => BuildCommand::class, + 'install' => InstallCommand::class, ] )); $application->run($container->get(InputInterface::class), $container->get(OutputInterface::class)); diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php new file mode 100644 index 0000000..3a8e049 --- /dev/null +++ b/src/Command/InstallCommand.php @@ -0,0 +1,67 @@ +writeln('This command needs elevated privileges, and may prompt you for your password.'); + } + + $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); + + $requestedNameAndVersionPair = CommandHelper::requestedNameAndVersionPair($input); + + $downloadedPackage = CommandHelper::downloadPackage( + $this->dependencyResolver, + $targetPlatform, + $requestedNameAndVersionPair, + $this->downloadAndExtract, + $output, + ); + + CommandHelper::bindConfigureOptionsFromPackage($this, $downloadedPackage->package, $input); + + $configureOptionsValues = CommandHelper::processConfigureOptionsFromInput($downloadedPackage->package, $input); + + ($this->build)($downloadedPackage, $targetPlatform, $configureOptionsValues, $output); + + ($this->install)($downloadedPackage, $targetPlatform, $output); + + return Command::SUCCESS; + } +} diff --git a/src/Container.php b/src/Container.php index bf50bc4..eb4db73 100644 --- a/src/Container.php +++ b/src/Container.php @@ -21,6 +21,7 @@ use Php\Pie\Building\WindowsBuild; use Php\Pie\Command\BuildCommand; use Php\Pie\Command\DownloadCommand; +use Php\Pie\Command\InstallCommand; use Php\Pie\DependencyResolver\DependencyResolver; use Php\Pie\DependencyResolver\ResolveDependencyWithComposer; use Php\Pie\Downloading\DownloadAndExtract; @@ -30,6 +31,9 @@ use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Downloading\UnixDownloadAndExtract; use Php\Pie\Downloading\WindowsDownloadAndExtract; +use Php\Pie\Installing\Install; +use Php\Pie\Installing\UnixInstall; +use Php\Pie\Installing\WindowsInstall; use Php\Pie\Platform\TargetPhp\ResolveTargetPhpToPlatformRepository; use Psr\Container\ContainerInterface; use Symfony\Component\Console\Helper\HelperSet; @@ -49,6 +53,7 @@ public static function factory(): ContainerInterface $container->singleton(DownloadCommand::class); $container->singleton(BuildCommand::class); + $container->singleton(InstallCommand::class); $container->singleton(IOInterface::class, static function (ContainerInterface $container): IOInterface { return new ConsoleIO( @@ -126,6 +131,17 @@ static function (ContainerInterface $container): Build { }, ); + $container->singleton( + Install::class, + static function (ContainerInterface $container): Install { + if (Platform::isWindows()) { + return $container->get(WindowsInstall::class); + } + + return $container->get(UnixInstall::class); + }, + ); + return $container; } } diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index 41446fb..60298be 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -7,6 +7,7 @@ use Composer\Package\CompletePackageInterface; use Php\Pie\ConfigureOption; use Php\Pie\ExtensionName; +use Php\Pie\ExtensionType; use function array_key_exists; use function array_map; @@ -23,6 +24,7 @@ final class Package /** @param list $configureOptions */ public function __construct( + public readonly ExtensionType $extensionType, public readonly ExtensionName $extensionName, public readonly string $name, public readonly string $version, @@ -43,6 +45,7 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com : []; return new self( + ExtensionType::tryFrom($completePackage->getType()) ?? ExtensionType::PhpModule, ExtensionName::determineFromComposerPackage($completePackage), $completePackage->getPrettyName(), $completePackage->getPrettyVersion(), diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index 23c199a..f7af0c1 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -7,10 +7,10 @@ use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionSelector; use Composer\Repository\RepositorySet; +use Php\Pie\ExtensionType; use Php\Pie\Platform\TargetPhp\ResolveTargetPhpToPlatformRepository; use Php\Pie\Platform\TargetPlatform; -use function in_array; use function preg_match; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ @@ -43,8 +43,7 @@ public function __invoke(TargetPlatform $targetPlatform, string $packageName, st throw UnableToResolveRequirement::fromRequirement($packageName, $requestedVersion); } - $type = $package->getType(); - if (! in_array($type, [Package::TYPE_PHP_MODULE, Package::TYPE_ZEND_EXTENSION])) { + if (! ExtensionType::isValid($package->getType())) { throw UnableToResolveRequirement::toPhpOrZendExtension($package, $packageName, $requestedVersion); } diff --git a/src/Downloading/DownloadZip.php b/src/Downloading/DownloadZip.php index 0f9b0b5..94811d0 100644 --- a/src/Downloading/DownloadZip.php +++ b/src/Downloading/DownloadZip.php @@ -9,6 +9,8 @@ /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ interface DownloadZip { + public const DOWNLOADED_ZIP_FILENAME = 'downloaded.zip'; + /** * @param non-empty-string $localPath * diff --git a/src/Downloading/DownloadZipWithGuzzle.php b/src/Downloading/DownloadZipWithGuzzle.php index 142cf0e..4515ec1 100644 --- a/src/Downloading/DownloadZipWithGuzzle.php +++ b/src/Downloading/DownloadZipWithGuzzle.php @@ -12,6 +12,8 @@ use function assert; use function file_put_contents; +use const DIRECTORY_SEPARATOR; + /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class DownloadZipWithGuzzle implements DownloadZip { @@ -36,7 +38,7 @@ public function downloadZipAndReturnLocalPath(RequestInterface $request, string AssertHttp::responseStatusCode(200, $response); - $tmpZipFile = $localPath . '/downloaded.zip'; + $tmpZipFile = $localPath . DIRECTORY_SEPARATOR . DownloadZip::DOWNLOADED_ZIP_FILENAME; file_put_contents($tmpZipFile, $response->getBody()->__toString()); return $tmpZipFile; diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php index 0d9bd70..64daadd 100644 --- a/src/Downloading/GithubPackageReleaseAssets.php +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -9,16 +9,14 @@ use GuzzleHttp\Psr7\Request; use GuzzleHttp\RequestOptions; use Php\Pie\DependencyResolver\Package; -use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; -use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\TargetPlatform; +use Php\Pie\Platform\WindowsExtensionAssetName; use Psl\Json; use Psl\Type; use Psr\Http\Message\ResponseInterface; use function assert; use function in_array; -use function sprintf; use function strtolower; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ @@ -47,34 +45,7 @@ public function findWindowsDownloadUrlForPackage(TargetPlatform $targetPlatform, /** @return non-empty-list */ private function expectedWindowsAssetNames(TargetPlatform $targetPlatform, Package $package): array { - if ($targetPlatform->operatingSystem !== OperatingSystem::Windows || $targetPlatform->windowsCompiler === null) { - throw CouldNotFindReleaseAsset::forMissingWindowsCompiler($targetPlatform); - } - - /** - * During development, we swapped compiler/ts around. It is fairly trivial to support both, so we can check - * both formats pretty easily, just to avoid confusion for package maintainers... - */ - return [ - strtolower(sprintf( - 'php_%s-%s-%s-%s-%s-%s.zip', - $package->extensionName->name(), - $package->version, - $targetPlatform->phpBinaryPath->majorMinorVersion(), - $targetPlatform->threadSafety->asShort(), - strtolower($targetPlatform->windowsCompiler->name), - $targetPlatform->architecture->name, - )), - strtolower(sprintf( - 'php_%s-%s-%s-%s-%s-%s.zip', - $package->extensionName->name(), - $package->version, - $targetPlatform->phpBinaryPath->majorMinorVersion(), - strtolower($targetPlatform->windowsCompiler->name), - $targetPlatform->threadSafety->asShort(), - $targetPlatform->architecture->name, - )), - ]; + return WindowsExtensionAssetName::zipNames($targetPlatform, $package); } /** @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3734 */ diff --git a/src/ExtensionType.php b/src/ExtensionType.php new file mode 100644 index 0000000..f783de9 --- /dev/null +++ b/src/ExtensionType.php @@ -0,0 +1,17 @@ +extractedSourcePath)) + ->mustRun() + ->getOutput(); + + $sharedObjectName = $downloadedPackage->package->extensionName->name() . '.so'; + $expectedSharedObjectLocation = sprintf( + '%s/%s', + $targetPlatform->phpBinaryPath->extensionPath(), + $sharedObjectName, + ); + + if (! file_exists($expectedSharedObjectLocation)) { + throw new RuntimeException('Install failed, ' . $expectedSharedObjectLocation . ' was not installed.'); + } + + $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 $expectedSharedObjectLocation; + } +} diff --git a/src/Installing/WindowsInstall.php b/src/Installing/WindowsInstall.php new file mode 100644 index 0000000..97f87db --- /dev/null +++ b/src/Installing/WindowsInstall.php @@ -0,0 +1,206 @@ +extractedSourcePath; + $sourceDllName = $this->determineDllName($targetPlatform, $downloadedPackage); + $sourcePdbName = str_replace('.dll', '.pdb', $sourceDllName); + assert($sourcePdbName !== ''); + + $destinationDllName = $this->copyExtensionDll($targetPlatform, $downloadedPackage, $sourceDllName); + $output->writeln('Copied DLL to: ' . $destinationDllName); + + $destinationPdbName = $this->copyExtensionPdb($targetPlatform, $downloadedPackage, $sourcePdbName, $destinationDllName); + if ($destinationPdbName !== null) { + $output->writeln('Copied PDB to: ' . $destinationPdbName); + } + + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($extractedSourcePath)) as $file) { + assert($file instanceof SplFileInfo); + + /** + * Skip directories, the main DLL, PDB, and the downloaded.zip + */ + if ( + $file->isDir() + || $this->normalisedPathsMatch($file->getPathname(), $sourceDllName) + || $this->normalisedPathsMatch($file->getPathname(), $sourcePdbName) + || $file->getFilename() === DownloadZip::DOWNLOADED_ZIP_FILENAME + ) { + continue; + } + + $destinationExtraDll = $this->copyDependencyDll($targetPlatform, $file); + if ($destinationExtraDll !== null) { + $output->writeln('Copied extra DLL: ' . $destinationExtraDll); + + continue; + } + + $destinationPathname = $this->copyExtraFile($targetPlatform, $downloadedPackage, $file); + $output->writeln('Copied extras: ' . $destinationPathname); + } + + /** + * @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 $destinationDllName; + } + + /** @return non-empty-string */ + private function determineDllName(TargetPlatform $targetPlatform, DownloadedPackage $package): string + { + $possibleDllNames = WindowsExtensionAssetName::dllNames($targetPlatform, $package->package); + foreach ($possibleDllNames as $dllName) { + $fullDllName = $package->extractedSourcePath . '/' . $dllName; + if (file_exists($fullDllName)) { + return $fullDllName; + } + } + + throw new RuntimeException('Unable to find DLL for package, checked: ' . implode(', ', $possibleDllNames)); + } + + /** + * Normalise both path parameters (i.e. replace `\` with `/`) and compare them. Useful if the two paths are built + * differently with different/incorrect directory separators, e.g. "C:\path\to/thing" vs "C:\path\to\thing" + */ + private function normalisedPathsMatch(string $first, string $second): bool + { + return str_replace('\\', '/', $first) === str_replace('\\', '/', $second); + } + + /** + * Copy the main PHP extension DLL into the extension path. + * + * @param non-empty-string $sourceDllName + * + * @return non-empty-string + */ + private function copyExtensionDll(TargetPlatform $targetPlatform, DownloadedPackage $downloadedPackage, string $sourceDllName): string + { + $destinationDllName = $targetPlatform->phpBinaryPath->extensionPath() . DIRECTORY_SEPARATOR + . '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); + } + + return $destinationDllName; + } + + /** + * Copy the PDB (Program Database, which is debugging information basically), into the same directory as the DLL, + * if it exists. + * + * Returns `null` if the source PDB does not exist (and thus, does not need to be copied). + * + * @param non-empty-string $sourcePdbName + * @param non-empty-string $destinationDllName + * + * @return non-empty-string|null + */ + private function copyExtensionPdb(TargetPlatform $targetPlatform, DownloadedPackage $downloadedPackage, string $sourcePdbName, string $destinationDllName): string|null + { + if (! file_exists($sourcePdbName)) { + return null; + } + + $destinationPdbName = str_replace('.dll', '.pdb', $destinationDllName); + assert($destinationPdbName !== ''); + + if (! copy($sourcePdbName, $destinationPdbName) || ! file_exists($destinationPdbName) && ! is_file($destinationPdbName)) { + throw new RuntimeException('Failed to install PDB to ' . $destinationPdbName); + } + + return $destinationPdbName; + } + + /** + * Any other DLL file included in the source package should be copied into the same path where `php.exe` is - these + * would commonly be dependencies/libraries that the extension depends on, and is bundled with. + * + * If the file is NOT a DLL, this method will return `null` + * + * @return non-empty-string|null + */ + private function copyDependencyDll(TargetPlatform $targetPlatform, SplFileInfo $file): string|null + { + if ($file->getExtension() !== 'dll') { + return null; + } + + $destinationExtraDll = dirname($targetPlatform->phpBinaryPath->phpBinaryPath) . DIRECTORY_SEPARATOR . $file->getFilename(); + + if (! copy($file->getPathname(), $destinationExtraDll) || ! file_exists($destinationExtraDll) && ! is_file($destinationExtraDll)) { + throw new RuntimeException('Failed to copy to ' . $destinationExtraDll); + } + + return $destinationExtraDll; + } + + /** + * Any other remaining file should be copied into the "extras" path, e.g. `C:\php\extras\my-php-ext\.` + * + * @return non-empty-string + */ + private function copyExtraFile(TargetPlatform $targetPlatform, DownloadedPackage $downloadedPackage, SplFileInfo $file): string + { + $destinationFullFilename = dirname($targetPlatform->phpBinaryPath->phpBinaryPath) . DIRECTORY_SEPARATOR + . 'extras' . DIRECTORY_SEPARATOR + . $downloadedPackage->package->extensionName->name() . DIRECTORY_SEPARATOR + . substr($file->getPathname(), strlen($downloadedPackage->extractedSourcePath) + 1); + + $destinationPath = dirname($destinationFullFilename); + + if (! file_exists($destinationPath)) { + mkdir($destinationPath, 0777, true); + } + + if (! copy($file->getPathname(), $destinationFullFilename) || ! file_exists($destinationFullFilename) && ! is_file($destinationFullFilename)) { + throw new RuntimeException('Failed to copy to ' . $destinationFullFilename); + } + + return $destinationFullFilename; + } +} diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index 025cc8d..300c47a 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -9,13 +9,22 @@ use Php\Pie\Platform\OperatingSystem; use Psl\Json; use Psl\Type; +use RuntimeException; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; use Webmozart\Assert\Assert; +use function array_key_exists; +use function assert; +use function dirname; +use function file_exists; +use function is_dir; +use function preg_match; use function sprintf; use function trim; +use const DIRECTORY_SEPARATOR; + /** * @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks * @@ -34,6 +43,36 @@ private function __construct( // @todo https://github.com/php/pie/issues/12 - we could verify that the given $phpBinaryPath really is a PHP install } + /** @return non-empty-string */ + public function extensionPath(): string + { + $phpinfo = $this->phpinfo(); + + if ( + preg_match('#^extension_dir\s+=>\s+([^=]+)\s+=>\s+([^=]+)$#m', $phpinfo, $matches) + && array_key_exists(1, $matches) + && trim($matches[1]) !== '' + && trim($matches[1]) !== 'no value' + ) { + $extensionPath = trim($matches[1]); + assert($extensionPath !== ''); + + if (file_exists($extensionPath) && is_dir($extensionPath)) { + return $extensionPath; + } + + // `extension_dir` may be a relative URL on Windows, so resolve it according to the location of PHP + $phpPath = dirname($this->phpBinaryPath); + $attemptExtensionPath = $phpPath . DIRECTORY_SEPARATOR . $extensionPath; + + if (file_exists($attemptExtensionPath) && is_dir($attemptExtensionPath)) { + return $attemptExtensionPath; + } + } + + throw new RuntimeException('Could not determine extension path for ' . $this->phpBinaryPath); + } + /** * 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/src/Platform/TargetPlatform.php b/src/Platform/TargetPlatform.php index 53b3a07..548aa08 100644 --- a/src/Platform/TargetPlatform.php +++ b/src/Platform/TargetPlatform.php @@ -8,6 +8,8 @@ use function array_key_exists; use function explode; +use function function_exists; +use function posix_getuid; use function preg_match; use function trim; @@ -27,6 +29,11 @@ public function __construct( ) { } + public static function isRunningAsRoot(): bool + { + return function_exists('posix_getuid') && posix_getuid() === 0; + } + public static function fromPhpBinaryPath(PhpBinaryPath $phpBinaryPath): self { $os = $phpBinaryPath->operatingSystem(); diff --git a/src/Platform/WindowsExtensionAssetName.php b/src/Platform/WindowsExtensionAssetName.php new file mode 100644 index 0000000..a00a832 --- /dev/null +++ b/src/Platform/WindowsExtensionAssetName.php @@ -0,0 +1,67 @@ + */ + private static function assetNames(TargetPlatform $targetPlatform, Package $package, string $fileExtension): array + { + if ($targetPlatform->operatingSystem !== OperatingSystem::Windows || $targetPlatform->windowsCompiler === null) { + throw CouldNotFindReleaseAsset::forMissingWindowsCompiler($targetPlatform); + } + + /** + * During development, we swapped compiler/ts around. It is fairly trivial to support both, so we can check + * both formats pretty easily, just to avoid confusion for package maintainers... + */ + return [ + strtolower(sprintf( + 'php_%s-%s-%s-%s-%s-%s.%s', + $package->extensionName->name(), + $package->version, + $targetPlatform->phpBinaryPath->majorMinorVersion(), + $targetPlatform->threadSafety->asShort(), + strtolower($targetPlatform->windowsCompiler->name), + $targetPlatform->architecture->name, + $fileExtension, + )), + strtolower(sprintf( + 'php_%s-%s-%s-%s-%s-%s.%s', + $package->extensionName->name(), + $package->version, + $targetPlatform->phpBinaryPath->majorMinorVersion(), + strtolower($targetPlatform->windowsCompiler->name), + $targetPlatform->threadSafety->asShort(), + $targetPlatform->architecture->name, + $fileExtension, + )), + ]; + } + + /** @return non-empty-list */ + public static function zipNames(TargetPlatform $targetPlatform, Package $package): array + { + return self::assetNames($targetPlatform, $package, 'zip'); + } + + /** @return non-empty-list */ + public static function dllNames(TargetPlatform $targetPlatform, Package $package): array + { + return self::assetNames($targetPlatform, $package, 'dll'); + } +} diff --git a/test/assets/pie_test_ext_win/README.md b/test/assets/pie_test_ext_win/README.md new file mode 100644 index 0000000..43e2c49 --- /dev/null +++ b/test/assets/pie_test_ext_win/README.md @@ -0,0 +1,10 @@ +This is just an example file structure for a Windows PHP module. + +Note that NONE of the .dll and .pdb files here are actually binaries! + +Given the PHP path of `C:\php\php.exe`: + + - `php_pie_test_ext-1.2.3-8.3-ts-vs16-x86_64.dll` should be installed to `C:\php\ext\php_pie_test_ext.dll` + - `php_pie_test_ext-1.2.3-8.3-ts-vs16-x86_64.pdb` should be installed to `C:\php\ext\php_pie_test_ext.pdb` + - `supporting-library.dll` should be installed to `C:\php\supporting-library.dll` + - `README.md` should be installed to `C:\php\extras\pie_test_ext\README.md` diff --git a/test/assets/pie_test_ext_win/more/more-information.txt b/test/assets/pie_test_ext_win/more/more-information.txt new file mode 100644 index 0000000..c78b62b --- /dev/null +++ b/test/assets/pie_test_ext_win/more/more-information.txt @@ -0,0 +1 @@ +only a test file diff --git a/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.1-ts-vs16-x86_64.dll b/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.1-ts-vs16-x86_64.dll new file mode 100644 index 0000000..c78b62b --- /dev/null +++ b/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.1-ts-vs16-x86_64.dll @@ -0,0 +1 @@ +only a test file diff --git a/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.1-ts-vs16-x86_64.pdb b/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.1-ts-vs16-x86_64.pdb new file mode 100644 index 0000000..c78b62b --- /dev/null +++ b/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.1-ts-vs16-x86_64.pdb @@ -0,0 +1 @@ +only a test file diff --git a/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.2-ts-vs16-x86_64.dll b/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.2-ts-vs16-x86_64.dll new file mode 100644 index 0000000..c78b62b --- /dev/null +++ b/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.2-ts-vs16-x86_64.dll @@ -0,0 +1 @@ +only a test file diff --git a/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.2-ts-vs16-x86_64.pdb b/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.2-ts-vs16-x86_64.pdb new file mode 100644 index 0000000..c78b62b --- /dev/null +++ b/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.2-ts-vs16-x86_64.pdb @@ -0,0 +1 @@ +only a test file diff --git a/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.3-ts-vs16-x86_64.dll b/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.3-ts-vs16-x86_64.dll new file mode 100644 index 0000000..c78b62b --- /dev/null +++ b/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.3-ts-vs16-x86_64.dll @@ -0,0 +1 @@ +only a test file diff --git a/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.3-ts-vs16-x86_64.pdb b/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.3-ts-vs16-x86_64.pdb new file mode 100644 index 0000000..c78b62b --- /dev/null +++ b/test/assets/pie_test_ext_win/php_pie_test_ext-1.2.3-8.3-ts-vs16-x86_64.pdb @@ -0,0 +1 @@ +only a test file diff --git a/test/assets/pie_test_ext_win/supporting-library.dll b/test/assets/pie_test_ext_win/supporting-library.dll new file mode 100644 index 0000000..c78b62b --- /dev/null +++ b/test/assets/pie_test_ext_win/supporting-library.dll @@ -0,0 +1 @@ +only a test file diff --git a/test/integration/Building/UnixBuildTest.php b/test/integration/Building/UnixBuildTest.php index 08fe182..2605e9f 100644 --- a/test/integration/Building/UnixBuildTest.php +++ b/test/integration/Building/UnixBuildTest.php @@ -10,6 +10,7 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; +use Php\Pie\ExtensionType; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; use Php\Pie\Platform\TargetPlatform; use PHPUnit\Framework\Attributes\CoversClass; @@ -32,6 +33,7 @@ public function testUnixBuildCanBuildExtension(): void $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( new Package( + ExtensionType::PhpModule, ExtensionName::normaliseFromString('pie_test_ext'), 'pie_test_ext', '0.1.0', diff --git a/test/integration/Command/BuildCommandTest.php b/test/integration/Command/BuildCommandTest.php index 98a80d5..9abb9ba 100644 --- a/test/integration/Command/BuildCommandTest.php +++ b/test/integration/Command/BuildCommandTest.php @@ -38,8 +38,6 @@ public function testBuildCommandWillBuildTheExtension(): void $outputString = $this->commandTester->getDisplay(); - self::assertStringContainsString('Found package: asgrim/example-pie-extension:1.0.1 which provides ext-example_pie_extension', $outputString); - if (Platform::isWindows()) { self::assertStringContainsString('Nothing to do on Windows.', $outputString); diff --git a/test/integration/Command/InstallCommandTest.php b/test/integration/Command/InstallCommandTest.php new file mode 100644 index 0000000..6274252 --- /dev/null +++ b/test/integration/Command/InstallCommandTest.php @@ -0,0 +1,67 @@ +commandTester = new CommandTester(Container::factory()->get(InstallCommand::class)); + } + + public function testInstallCommandWillInstallCompatibleExtension(): void + { + if (PHP_VERSION_ID < 80300 || PHP_VERSION_ID >= 80400) { + self::markTestSkipped('This test can only run on PHP 8.3 - you are running ' . PHP_VERSION); + } + + try { + (new Process(['sudo', 'ls']))->mustRun(); + } catch (ProcessFailedException) { + self::markTestSkipped('Skipping as cannot run with sudo enabled'); + } + + $this->commandTester->execute(['requested-package-and-version' => self::TEST_PACKAGE]); + + $this->commandTester->assertCommandIsSuccessful(); + + $outputString = $this->commandTester->getDisplay(); + self::assertStringContainsString('Install complete: ', $outputString); + self::assertStringContainsString('You must now add "extension=example_pie_extension" to your php.ini', $outputString); + + if ( + ! preg_match('#^Install complete: (.*)$#m', $outputString, $matches) + || ! array_key_exists(1, $matches) + || $matches[1] === '' + || ! file_exists($matches[1]) + || ! is_file($matches[1]) + ) { + return; + } + + (new Process(['sudo', 'rm', $matches[1]]))->mustRun(); + } +} diff --git a/test/integration/Installing/UnixInstallTest.php b/test/integration/Installing/UnixInstallTest.php new file mode 100644 index 0000000..8faac9f --- /dev/null +++ b/test/integration/Installing/UnixInstallTest.php @@ -0,0 +1,82 @@ +mustRun(); + } catch (ProcessFailedException) { + self::markTestSkipped('Skipping as cannot run with sudo enabled'); + } + + $output = new BufferedOutput(); + $targetPlatform = TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess()); + $extensionPath = $targetPlatform->phpBinaryPath->extensionPath(); + + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + new Package( + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('pie_test_ext'), + 'pie_test_ext', + '0.1.0', + null, + [ConfigureOption::fromComposerJsonDefinition(['name' => 'enable-pie_test_ext'])], + ), + self::TEST_EXTENSION_PATH, + ); + + (new UnixBuild())->__invoke( + $downloadedPackage, + $targetPlatform, + ['--enable-pie_test_ext'], + new NullOutput(), + ); + + $installedSharedObject = (new UnixInstall())->__invoke( + $downloadedPackage, + $targetPlatform, + $output, + ); + + $outputString = $output->fetch(); + + self::assertStringContainsString('Install complete: ' . $extensionPath . '/pie_test_ext.so', $outputString); + self::assertStringContainsString('You must now add "extension=pie_test_ext" to your php.ini', $outputString); + + self::assertSame($extensionPath . '/pie_test_ext.so', $installedSharedObject); + self::assertFileExists($installedSharedObject); + + (new Process(['sudo', 'rm', $installedSharedObject]))->mustRun(); + (new Process(['make', 'clean'], $downloadedPackage->extractedSourcePath))->mustRun(); + (new Process(['phpize', '--clean'], $downloadedPackage->extractedSourcePath))->mustRun(); + } +} diff --git a/test/integration/Installing/WindowsInstallTest.php b/test/integration/Installing/WindowsInstallTest.php new file mode 100644 index 0000000..011394a --- /dev/null +++ b/test/integration/Installing/WindowsInstallTest.php @@ -0,0 +1,133 @@ +phpBinaryPath->phpBinaryPath); + $extensionPath = $targetPlatform->phpBinaryPath->extensionPath(); + + $installer = new WindowsInstall(); + + $installedDll = $installer->__invoke($downloadedPackage, $targetPlatform, $output); + self::assertSame($extensionPath . '\php_pie_test_ext.dll', $installedDll); + + $outputString = $output->fetch(); + + self::assertStringContainsString('Copied DLL to: ' . $extensionPath . '\php_pie_test_ext.dll', $outputString); + self::assertStringContainsString('You must now add "extension=pie_test_ext" to your php.ini', $outputString); + + $extrasDirectory = $phpPath . DIRECTORY_SEPARATOR . 'extras' . DIRECTORY_SEPARATOR . 'pie_test_ext'; + + $expectedPdb = str_replace('.dll', '.pdb', $installedDll); + $expectedSupportingDll = $phpPath . DIRECTORY_SEPARATOR . 'supporting-library.dll'; + $expectedSupportingOtherFile = $extrasDirectory . DIRECTORY_SEPARATOR . 'README.md'; + $expectedSubdirectoryFile = $extrasDirectory . DIRECTORY_SEPARATOR . 'more' . DIRECTORY_SEPARATOR . 'more-information.txt'; + assert($expectedPdb !== ''); + + self::assertFileExists($installedDll); + self::assertFileExists($expectedPdb); + self::assertFileExists($expectedSupportingDll); + self::assertFileExists($expectedSupportingOtherFile); + self::assertFileExists($expectedSubdirectoryFile); + + $this->delete($installedDll); + $this->delete($expectedPdb); + $this->delete($expectedSupportingDll); + $this->delete($extrasDirectory); + } + + /** + * Recursively remove a file/path to clean up after testing + * + * @param non-empty-string $path + */ + private function delete(string $path): void + { + if (! file_exists($path)) { + return; + } + + if (! is_dir($path)) { + unlink($path); + + return; + } + + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST, + ); + + foreach ($files as $fileinfo) { + assert($fileinfo instanceof SplFileInfo); + if ($fileinfo->isDir()) { + rmdir($fileinfo->getRealPath()); + continue; + } + + unlink($fileinfo->getRealPath()); + } + + rmdir($path); + } +} diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index 3bf0a2b..bbdd5c0 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -13,6 +13,7 @@ use Php\Pie\Downloading\DownloadAndExtract; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; +use Php\Pie\ExtensionType; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; use Php\Pie\Platform\TargetPlatform; use PHPUnit\Framework\Attributes\CoversClass; @@ -102,6 +103,7 @@ public function testDownloadPackage(): void $targetPlatform, ) ->willReturn($package = new Package( + ExtensionType::PhpModule, ExtensionName::normaliseFromString('test_pie_ext'), 'php/test-pie-ext', '1.2.3', @@ -141,6 +143,7 @@ public function testBindingConfigurationOptionsFromPackage(): void public function testProcessingConfigureOptionsFromInput(): void { $package = new Package( + ExtensionType::PhpModule, ExtensionName::normaliseFromString('lolz'), 'foo/bar', '1.0.0', diff --git a/test/unit/Downloading/AddAuthenticationHeaderTest.php b/test/unit/Downloading/AddAuthenticationHeaderTest.php index 6465c31..03f3a5c 100644 --- a/test/unit/Downloading/AddAuthenticationHeaderTest.php +++ b/test/unit/Downloading/AddAuthenticationHeaderTest.php @@ -9,6 +9,7 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\AddAuthenticationHeader; use Php\Pie\ExtensionName; +use Php\Pie\ExtensionType; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -33,6 +34,7 @@ public function testAuthorizationHeaderIsAdded(): void $requestWithAuthHeader = (new AddAuthenticationHeader())->withAuthHeaderFromComposer( $request, new Package( + ExtensionType::PhpModule, ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', @@ -55,6 +57,7 @@ public function testExceptionIsThrownWhenPackageDoesNotHaveDownloadUrl(): void $addAuthenticationHeader = new AddAuthenticationHeader(); $package = new Package( + ExtensionType::PhpModule, ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', diff --git a/test/unit/Downloading/DownloadedPackageTest.php b/test/unit/Downloading/DownloadedPackageTest.php index 078dd17..3419f4e 100644 --- a/test/unit/Downloading/DownloadedPackageTest.php +++ b/test/unit/Downloading/DownloadedPackageTest.php @@ -7,6 +7,7 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; +use Php\Pie\ExtensionType; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -18,6 +19,7 @@ final class DownloadedPackageTest extends TestCase public function testFromPackageAndExtractedPath(): void { $package = new Package( + ExtensionType::PhpModule, ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', diff --git a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php index 5e5e494..7b1311b 100644 --- a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php +++ b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php @@ -7,6 +7,7 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; use Php\Pie\ExtensionName; +use Php\Pie\ExtensionType; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; @@ -21,6 +22,7 @@ final class CouldNotFindReleaseAssetTest extends TestCase public function testForPackage(): void { $package = new Package( + ExtensionType::PhpModule, ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', @@ -36,6 +38,7 @@ public function testForPackage(): void public function testForPackageWithMissingTag(): void { $package = new Package( + ExtensionType::PhpModule, ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index b37d1e5..21e3dc1 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -13,6 +13,7 @@ use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; use Php\Pie\Downloading\GithubPackageReleaseAssets; use Php\Pie\ExtensionName; +use Php\Pie\ExtensionType; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; @@ -67,6 +68,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void $guzzleMockClient = new Client(['handler' => HandlerStack::create($mockHandler)]); $package = new Package( + ExtensionType::PhpModule, ExtensionName::normaliseFromString('foo'), 'asgrim/example-pie-extension', '1.2.3', @@ -118,6 +120,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThr $guzzleMockClient = new Client(['handler' => HandlerStack::create($mockHandler)]); $package = new Package( + ExtensionType::PhpModule, ExtensionName::normaliseFromString('foo'), 'asgrim/example-pie-extension', '1.2.3', @@ -155,6 +158,7 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF $guzzleMockClient = new Client(['handler' => HandlerStack::create($mockHandler)]); $package = new Package( + ExtensionType::PhpModule, ExtensionName::normaliseFromString('foo'), 'asgrim/example-pie-extension', '1.2.3', diff --git a/test/unit/Downloading/UnixDownloadAndExtractTest.php b/test/unit/Downloading/UnixDownloadAndExtractTest.php index c75ab9c..e460e9c 100644 --- a/test/unit/Downloading/UnixDownloadAndExtractTest.php +++ b/test/unit/Downloading/UnixDownloadAndExtractTest.php @@ -10,6 +10,7 @@ use Php\Pie\Downloading\ExtractZip; use Php\Pie\Downloading\UnixDownloadAndExtract; use Php\Pie\ExtensionName; +use Php\Pie\ExtensionType; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; @@ -60,6 +61,7 @@ public function testInvoke(): void $downloadUrl = 'https://test-uri/' . uniqid('downloadUrl', true); $requestedPackage = new Package( + ExtensionType::PhpModule, ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', diff --git a/test/unit/Downloading/WindowsDownloadAndExtractTest.php b/test/unit/Downloading/WindowsDownloadAndExtractTest.php index e413204..36e0cc5 100644 --- a/test/unit/Downloading/WindowsDownloadAndExtractTest.php +++ b/test/unit/Downloading/WindowsDownloadAndExtractTest.php @@ -11,6 +11,7 @@ use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Downloading\WindowsDownloadAndExtract; use Php\Pie\ExtensionName; +use Php\Pie\ExtensionType; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; @@ -75,6 +76,7 @@ public function testInvoke(): void ->willReturn($extractedPath); $requestedPackage = new Package( + ExtensionType::PhpModule, ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', diff --git a/test/unit/ExtensionTypeTest.php b/test/unit/ExtensionTypeTest.php new file mode 100644 index 0000000..9a27f97 --- /dev/null +++ b/test/unit/ExtensionTypeTest.php @@ -0,0 +1,23 @@ +phpIntSize(), ); } + + public function testExtensionPath(): void + { + $phpBinary = PhpBinaryPath::fromCurrentProcess(); + + $expectedExtensionDir = ini_get('extension_dir'); + + // `extension_dir` may be a relative URL on Windows (e.g. "ext"), so resolve it according to the location of PHP + if (! file_exists($expectedExtensionDir) || ! is_dir($expectedExtensionDir)) { + $absoluteExtensionDir = dirname($phpBinary->phpBinaryPath) . DIRECTORY_SEPARATOR . $expectedExtensionDir; + if (file_exists($absoluteExtensionDir) && is_dir($absoluteExtensionDir)) { + $expectedExtensionDir = $absoluteExtensionDir; + } + } + + self::assertSame( + $expectedExtensionDir, + $phpBinary->extensionPath(), + ); + } } diff --git a/test/unit/Platform/WindowsExtensionAssetNameTest.php b/test/unit/Platform/WindowsExtensionAssetNameTest.php new file mode 100644 index 0000000..29025df --- /dev/null +++ b/test/unit/Platform/WindowsExtensionAssetNameTest.php @@ -0,0 +1,72 @@ +platform = new TargetPlatform( + OperatingSystem::Windows, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + WindowsCompiler::VC14, + ); + + $this->phpVersion = $this->platform->phpBinaryPath->majorMinorVersion(); + + $this->package = new Package( + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'phpf/foo', + '1.2.3', + null, + [], + ); + } + + public function testZipNames(): void + { + self::assertSame( + [ + 'php_foo-1.2.3-' . $this->phpVersion . '-ts-vc14-x86_64.zip', + 'php_foo-1.2.3-' . $this->phpVersion . '-vc14-ts-x86_64.zip', + ], + WindowsExtensionAssetName::zipNames($this->platform, $this->package), + ); + } + + public function testDllNames(): void + { + self::assertSame( + [ + 'php_foo-1.2.3-' . $this->phpVersion . '-ts-vc14-x86_64.dll', + 'php_foo-1.2.3-' . $this->phpVersion . '-vc14-ts-x86_64.dll', + ], + WindowsExtensionAssetName::dllNames($this->platform, $this->package), + ); + } +}