← Back
Editing: CommandHelper.php
<?php declare(strict_types=1); namespace Php\Pie\Command; use Composer\Composer; use Composer\IO\IOInterface; use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionParser; use Composer\Repository\ComposerRepository; use Composer\Repository\PathRepository; use Composer\Repository\VcsRepository; use Composer\Util\Platform; use InvalidArgumentException; use OutOfRangeException; use Php\Pie\ComposerIntegration\PieComposerFactory; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\DependencyResolver\InvalidPackageName; use Php\Pie\DependencyResolver\Package; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\DependencyResolver\UnableToResolveRequirement; use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages; use Php\Pie\Platform as PiePlatform; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; use Php\Pie\Platform\TargetPhp\PhpizePath; use Php\Pie\Platform\TargetPlatform; use Psr\Container\ContainerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Throwable; use Webmozart\Assert\Assert; use function array_key_exists; use function array_map; use function count; use function is_array; use function is_string; use function reset; use function sprintf; use function str_starts_with; use function strtolower; use function substr; use function trim; use const PHP_VERSION; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class CommandHelper { public const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version'; public const OPTION_WITH_PHP_CONFIG = 'with-php-config'; public const OPTION_WITH_PHP_PATH = 'with-php-path'; public const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path'; public const OPTION_WORKING_DIRECTORY = 'working-dir'; public const OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL = 'allow-non-interactive-project-install'; private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs'; private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension'; private const OPTION_FORCE = 'force'; private const OPTION_NO_CACHE = 'no-cache'; private const OPTION_AUTO_INSTALL_BUILD_TOOLS = 'auto-install-build-tools'; private const OPTION_SUPPRESS_BUILD_TOOLS_CHECK = 'no-build-tools-check'; private const OPTION_AUTO_INSTALL_SYSTEM_DEPENDENCIES = 'auto-install-system-dependencies'; private const OPTION_SUPPRESS_SYSTEM_DEPENDENCIES_CHECK = 'no-system-dependencies-check'; private function __construct() { } public static function configurePhpConfigOptions(Command $command): void { $command->addOption( self::OPTION_WITH_PHP_CONFIG, null, InputOption::VALUE_REQUIRED, 'The path to the `php-config` binary to find the target PHP platform on ' . OperatingSystem::NonWindows->asFriendlyName() . ', e.g. --' . self::OPTION_WITH_PHP_CONFIG . '=/usr/bin/php-config7.4', ); $command->addOption( self::OPTION_WITH_PHP_PATH, null, InputOption::VALUE_REQUIRED, 'The path to the `php` binary to use as the target PHP platform on ' . OperatingSystem::Windows->asFriendlyName() . ', e.g. --' . self::OPTION_WITH_PHP_PATH . '=C:\usr\php7.4.33\php.exe', ); $command->addOption( self::OPTION_WITH_PHPIZE_PATH, null, InputOption::VALUE_REQUIRED, 'The path to the `phpize` binary to use as the target PHP platform, e.g. --' . self::OPTION_WITH_PHPIZE_PATH . '=/usr/bin/phpize7.4', ); $command->addOption( self::OPTION_NO_CACHE, null, InputOption::VALUE_NONE, 'Prevent the use of the Composer cache.', ); } public static function configureDownloadBuildInstallOptions(Command $command, bool $withRequestedPackageAndVersion = true): void { if ($withRequestedPackageAndVersion) { $command->addArgument( self::ARG_REQUESTED_PACKAGE_AND_VERSION, InputArgument::OPTIONAL, 'The PIE package name and version constraint to use, in the format {vendor/package}{?:{?version-constraint}{?@stability}}, for example `xdebug/xdebug:^3.4@alpha`, `xdebug/xdebug:@alpha`, `xdebug/xdebug:^3.4`, etc.', ); } $command->addOption( self::OPTION_MAKE_PARALLEL_JOBS, 'j', InputOption::VALUE_REQUIRED, 'Override many jobs to run in parallel when running compiling (this is passed to "make -jN" during build). PIE will try to detect this by default.', ); $command->addOption( self::OPTION_SKIP_ENABLE_EXTENSION, null, InputOption::VALUE_NONE, 'Specify this to skip attempting to enable the extension in php.ini', ); $command->addOption( self::OPTION_FORCE, null, InputOption::VALUE_NONE, 'To attempt to install a version that doesn\'t match the version constraints from the meta-data, for instance to install an older version than recommended, or when the signature is not available.', ); $command->addOption( self::OPTION_WORKING_DIRECTORY, 'd', InputOption::VALUE_REQUIRED, 'The working directory to use, where applicable. If not specified, the current working directory is used. Only used in certain contexts.', ); self::configurePhpConfigOptions($command); $command->addOption( self::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL, null, InputOption::VALUE_NONE, 'When installing a PHP project, allow non-interactive project installations. Only used in certain contexts.', ); $command->addOption( self::OPTION_AUTO_INSTALL_BUILD_TOOLS, null, InputOption::VALUE_NONE, 'If build tools are missing, automatically install them, instead of prompting.', ); $command->addOption( self::OPTION_SUPPRESS_BUILD_TOOLS_CHECK, null, InputOption::VALUE_NONE, 'Do not perform the check to see if build tools are present on the system.', ); $command->addOption( self::OPTION_AUTO_INSTALL_SYSTEM_DEPENDENCIES, null, InputOption::VALUE_NONE, 'If system dependencies missing, automatically install them, instead of prompting.', ); $command->addOption( self::OPTION_SUPPRESS_SYSTEM_DEPENDENCIES_CHECK, null, InputOption::VALUE_NONE, 'Do not perform the check to see if system dependencies are present on the system.', ); /** * Allows additional options for the `./configure` command to be passed here. * Note, this means you probably need to call {@see self::validateInput()} to validate the input manually... */ $command->ignoreValidationErrors(); } public static function validateInput(InputInterface $input, Command $command): void { $input->bind($command->getDefinition()); } public static function determineTargetPlatformFromInputs(InputInterface $input, IOInterface $io): TargetPlatform { $phpBinaryPath = PhpBinaryPath::fromCurrentProcess(); /** @var mixed $withPhpConfig */ $withPhpConfig = $input->getOption(self::OPTION_WITH_PHP_CONFIG); $specifiedWithPhpConfig = is_string($withPhpConfig) && $withPhpConfig !== ''; /** @var mixed $withPhpPath */ $withPhpPath = $input->getOption(self::OPTION_WITH_PHP_PATH); $specifiedWithPhpPath = is_string($withPhpPath) && $withPhpPath !== ''; if (Platform::isWindows() && $specifiedWithPhpConfig) { throw new InvalidArgumentException('The --with-php-config=/path/to/php-config cannot be used on Windows, use --with-php-path=/path/to/php instead.'); } if (! Platform::isWindows() && $specifiedWithPhpPath && ! $specifiedWithPhpConfig) { throw new InvalidArgumentException('The --with-php-path=/path/to/php cannot be used on non-Windows, use --with-php-config=/path/to/php-config instead.'); } if ($specifiedWithPhpConfig) { $phpBinaryPath = PhpBinaryPath::fromPhpConfigExecutable($withPhpConfig); } if ($specifiedWithPhpPath) { $phpBinaryPath = PhpBinaryPath::fromPhpBinaryPath($withPhpPath); } $makeParallelJobs = null; /** `null` means {@see TargetPlatform} will try to auto-detect */ if ($input->hasOption(self::OPTION_MAKE_PARALLEL_JOBS)) { $makeParallelJobsOptions = (int) $input->getOption(self::OPTION_MAKE_PARALLEL_JOBS); if ($makeParallelJobsOptions > 0) { $makeParallelJobs = $makeParallelJobsOptions; } } $phpizePath = null; if ($input->hasOption(self::OPTION_WITH_PHPIZE_PATH)) { $phpizePathOption = $input->getOption(self::OPTION_WITH_PHPIZE_PATH); if (is_string($phpizePathOption) && trim($phpizePathOption) !== '') { if (Platform::isWindows()) { throw new InvalidArgumentException('The --with-phpize-path=/path/to/phpize cannot be used on Windows.'); } $phpizePath = new PhpizePath($phpizePathOption); } } $targetPlatform = TargetPlatform::fromPhpBinaryPath($phpBinaryPath, $makeParallelJobs, $phpizePath); if (PiePlatform::isRunningStaticPhp()) { $io->write(sprintf('<info>You are running a PIE Static PHP %s build</info>', PHP_VERSION)); } else { $io->write(sprintf('<info>You are running PHP %s</info>', PHP_VERSION)); } $io->write(sprintf( '<info>Target PHP installation:</info> %s %s%s, on %s %s (from %s)', $phpBinaryPath->version(), $targetPlatform->threadSafety->asShort(), strtolower($targetPlatform->windowsCompiler !== null ? ', ' . $targetPlatform->windowsCompiler->name : ''), $targetPlatform->operatingSystem->asFriendlyName(), $targetPlatform->architecture->name, $phpBinaryPath->phpBinaryPath, )); $io->write( sprintf( '<info>Using pie.json:</info> %s', PiePlatform::getPieJsonFilename($targetPlatform), ), verbosity: IOInterface::VERBOSE, ); 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 determineForceInstallingPackageVersion(InputInterface $input): bool { return $input->hasOption(self::OPTION_FORCE) && $input->getOption(self::OPTION_FORCE); } public static function autoInstallBuildTools(InputInterface $input): bool { return $input->hasOption(self::OPTION_AUTO_INSTALL_BUILD_TOOLS) && $input->getOption(self::OPTION_AUTO_INSTALL_BUILD_TOOLS); } public static function shouldCheckForBuildTools(InputInterface $input): bool { if (Platform::isWindows()) { return false; } return ! $input->hasOption(self::OPTION_SUPPRESS_BUILD_TOOLS_CHECK) || ! $input->getOption(self::OPTION_SUPPRESS_BUILD_TOOLS_CHECK); } public static function autoInstallSystemDependencies(InputInterface $input): bool { return $input->hasOption(self::OPTION_AUTO_INSTALL_SYSTEM_DEPENDENCIES) && $input->getOption(self::OPTION_AUTO_INSTALL_SYSTEM_DEPENDENCIES); } public static function shouldCheckSystemDependencies(InputInterface $input): bool { if (Platform::isWindows()) { return false; } return ! $input->hasOption(self::OPTION_SUPPRESS_SYSTEM_DEPENDENCIES_CHECK) || ! $input->getOption(self::OPTION_SUPPRESS_SYSTEM_DEPENDENCIES_CHECK); } public static function requestedNameAndVersionPair(InputInterface $input): RequestedPackageAndVersion { $requestedPackageString = $input->getArgument(self::ARG_REQUESTED_PACKAGE_AND_VERSION); if (! is_string($requestedPackageString) || $requestedPackageString === '') { throw new InvalidArgumentException('No package was requested for installation'); } $nameAndVersionPairs = (new VersionParser()) ->parseNameVersionPairs([$requestedPackageString]); $requestedNameAndVersionPair = reset($nameAndVersionPairs); if (! is_array($requestedNameAndVersionPair)) { throw new InvalidArgumentException('Failed to parse the name/version pair'); } if (! array_key_exists('version', $requestedNameAndVersionPair)) { $requestedNameAndVersionPair['version'] = null; } Assert::stringNotEmpty($requestedNameAndVersionPair['name']); Assert::nullOrStringNotEmpty($requestedNameAndVersionPair['version']); return new RequestedPackageAndVersion( $requestedNameAndVersionPair['name'], $requestedNameAndVersionPair['version'], ); } public static function bindConfigureOptionsFromPackage(Command $command, Package $package, InputInterface $input): void { foreach ($package->configureOptions() as $configureOption) { $command->addOption( $configureOption->name, null, $configureOption->needsValue ? InputOption::VALUE_REQUIRED : InputOption::VALUE_NONE, $configureOption->description, ); } self::validateInput($input, $command); } /** @return list<non-empty-string> */ public static function processConfigureOptionsFromInput(Package $package, InputInterface $input): array { $configureOptionsValues = []; foreach ($package->configureOptions() as $configureOption) { if (! $input->hasOption($configureOption->name)) { continue; } $value = $input->getOption($configureOption->name); if ($configureOption->needsValue) { if (is_string($value) && $value !== '') { $configureOptionsValues[] = '--' . $configureOption->name . '=' . $value; } continue; } Assert::boolean($value); if ($value !== true) { continue; } $configureOptionsValues[] = '--' . $configureOption->name; } return $configureOptionsValues; } public static function listRepositories(Composer $composer, IOInterface $io): void { $io->write('The following repositories are in use for this Target PHP:'); foreach ($composer->getRepositoryManager()->getRepositories() as $repo) { if ($repo instanceof ComposerRepository) { $repoConfig = $repo->getRepoConfig(); $repoUrl = array_key_exists('url', $repoConfig) && is_string($repoConfig['url']) && $repoConfig['url'] !== '' ? $repoConfig['url'] : null; if ($repoUrl === 'https://repo.packagist.org') { $io->write(' - Packagist'); continue; } $io->write(sprintf(' - Composer (%s)', $repoUrl ?? 'no url?')); continue; } if ($repo instanceof VcsRepository) { $io->write(sprintf( ' - VCS Repository (%s)', $repo->getDriver()?->getUrl() ?? 'no url?', )); continue; } if (! $repo instanceof PathRepository) { continue; } $repoConfig = $repo->getRepoConfig(); $io->write(sprintf( ' - Path Repository (%s)', array_key_exists('url', $repoConfig) && is_string($repoConfig['url']) && $repoConfig['url'] !== '' ? $repoConfig['url'] : 'no path?', )); } } public static function handlePackageNotFound( InvalidPackageName|UnableToResolveRequirement $exception, FindMatchingPackages $findMatchingPackages, IOInterface $io, TargetPlatform $targetPlatform, ContainerInterface $container, ): int { $pieComposer = PieComposerFactory::createPieComposer( $container, PieComposerRequest::noOperation( $io, $targetPlatform, ), ); $requestedPackageName = $exception->requestedPackageAndVersion->package; if (str_starts_with($requestedPackageName, 'ext-')) { $requestedPackageName = substr($requestedPackageName, 4); } $io->writeError(''); $io->writeError(sprintf('<error>Could not install package: %s</error>', $requestedPackageName)); $io->writeError($exception->getMessage()); try { $matches = array_map( static function (array $match) use ($io, $pieComposer): array { $composerMatchingPackage = $pieComposer->getRepositoryManager()->findPackage($match['name'], '*'); // Attempts to augment the Composer packages found with the PIE extension name if ($composerMatchingPackage instanceof CompletePackageInterface) { try { $match['extension-name'] = Package ::fromComposerCompletePackage($composerMatchingPackage) ->extensionName() ->name(); } catch (Throwable $t) { $io->writeError( sprintf( 'Tried looking up extension name for %s, but failed: %s', $match['name'], $t->getMessage(), ), verbosity: IOInterface::VERY_VERBOSE, ); } } return $match; }, $findMatchingPackages->for($pieComposer, $requestedPackageName), ); if (count($matches)) { $io->write(''); if (count($matches) === 1) { $io->write('<info>Did you mean this?</info>'); } else { $io->write('<info>Did you mean one of these?</info>'); } array_map( static function (array $match) use ($io): void { $io->write(sprintf( ' - %s%s: %s', $match['name'], array_key_exists('extension-name', $match) && is_string($match['extension-name']) ? ' (provides extension: ' . $match['extension-name'] . ')' : '', $match['description'] ?? 'no description available', )); }, $matches, ); } } catch (OutOfRangeException) { $io->writeError( sprintf( 'Tried searching for "%s", but nothing was found.', $requestedPackageName, ), verbosity: IOInterface::VERBOSE, ); } return 1; } public static function applyNoCacheOptionIfSet(InputInterface $input, IOInterface $io): void { if (! $input->hasOption(self::OPTION_NO_CACHE) || ! $input->getOption(self::OPTION_NO_CACHE)) { return; } $io->writeError('Disabling cache usage', verbosity: IOInterface::DEBUG); Platform::putEnv('COMPOSER_CACHE_DIR', Platform::isWindows() ? 'nul' : '/dev/null'); } }
Save File
Cancel