diff --git a/.makefile/e2e.file b/.makefile/e2e.file index fe417366..b8f5c615 100644 --- a/.makefile/e2e.file +++ b/.makefile/e2e.file @@ -314,7 +314,10 @@ e2e_034: $(PHP_SCOPER_PHAR_BIN) fixtures/set034-installed-versions/vendor .PHONY: e2e_035 e2e_035: # Runs end-to-end tests for the fixture set 035 — Tests tha composer autoloaded files are working fine -e2e_035: $(PHP_SCOPER_PHAR_BIN) fixtures/set035-composer-files-autoload/vendor fixtures/set035-composer-files-autoload/guzzle5-include/vendor +e2e_035: $(PHP_SCOPER_PHAR_BIN) \ + fixtures/set035-composer-files-autoload/vendor \ + fixtures/set035-composer-files-autoload/guzzle5-include/vendor \ + fixtures/set035-composer-files-autoload/composer-variable-access/vendor rm -rf build/set035-composer-files-autoload || true cp -R fixtures/set035-composer-files-autoload build/set035-composer-files-autoload @@ -328,6 +331,16 @@ e2e_035: $(PHP_SCOPER_PHAR_BIN) fixtures/set035-composer-files-autoload/vendor f composer --working-dir=build/set035-composer-files-autoload/scoped-guzzle5-include dump-autoload rm -rf build/set035-composer-files-autoload/guzzle5-include || true + $(PHP_SCOPER_PHAR) add-prefix \ + --working-dir=fixtures/set035-composer-files-autoload/composer-variable-access \ + --output-dir=../../../build/set035-composer-files-autoload/scoped-composer-variable-access \ + --force \ + --config=scoper.inc.php \ + --no-interaction \ + --stop-on-failure + composer --working-dir=build/set035-composer-files-autoload/scoped-composer-variable-access dump-autoload + rm -rf build/set035-composer-files-autoload/composer-variable-access || true + php build/set035-composer-files-autoload/index.php 2>&1 > build/set035-composer-files-autoload/output php build/set035-composer-files-autoload/test.php @@ -530,6 +543,13 @@ fixtures/set035-composer-files-autoload/guzzle5-include/composer.lock: fixtures/ @echo "$(@) is not up to date. You may want to run the following command:" @echo "$$ composer --working-dir=fixtures/set035-composer-files-autoload/guzzle5-include update --lock && touch -c $(@)" +fixtures/set035-composer-files-autoload/composer-variable-access/vendor: fixtures/set035-composer-files-autoload/composer-variable-access/composer.lock + composer --working-dir=fixtures/set035-composer-files-autoload/composer-variable-access install --no-dev --no-scripts + touch -c $@ +fixtures/set035-composer-files-autoload/composer-variable-access/composer.lock: fixtures/set035-composer-files-autoload/composer-variable-access/composer.json + @echo "$(@) is not up to date. You may want to run the following command:" + @echo "$$ composer --working-dir=fixtures/set035-composer-files-autoload/composer-variable-access update --lock && touch -c $(@)" + build/set038/phpunit: rm -rf $(E2E_PHPUNIT_DIR) || true git clone --depth=1 --single-branch git@github.com:sebastianbergmann/phpunit.git $@ diff --git a/fixtures/set035-composer-files-autoload/check-non-prefixed-files.php b/fixtures/set035-composer-files-autoload/check-non-prefixed-files.php new file mode 100644 index 00000000..53ba7534 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/check-non-prefixed-files.php @@ -0,0 +1,39 @@ + true, // vendor/symfony/polyfill-php80/bootstrap.php + '60884d26763a20c18bdf80c8935efaac' => true, // included-file.php +]; +$expectedMissingComposerAutoloadFiles = [ + '430aabe1de335715bfb79e58e8c22198' => true, // excluded-file.php +]; + +$actualExpectedPresent = array_diff_key( + $expectedPresentComposerAutoloadFiles, + $composerAutoloadFiles, +); +$actualExpectedMissing = array_diff_key( + $expectedMissingComposerAutoloadFiles, + $composerAutoloadFiles, +); + +if (count($actualExpectedPresent) !== 0) { + echo 'Expected the following hashes to be present:'.PHP_EOL; + echo var_export($actualExpectedPresent, true).PHP_EOL; + exit(1); +} + +if (count($actualExpectedMissing) !== count($expectedMissingComposerAutoloadFiles)) { + echo 'Expected the following hashes to be missing:'.PHP_EOL; + echo var_export($actualExpectedMissing, true).PHP_EOL; + exit(1); +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/composer.json b/fixtures/set035-composer-files-autoload/composer-variable-access/composer.json new file mode 100644 index 00000000..24c195a1 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/composer.json @@ -0,0 +1,12 @@ +{ + "bin": "index.php", + "autoload": { + "files": [ + "included-file.php", + "excluded-file.php" + ] + }, + "require": { + "symfony/polyfill-php80": "^1.28" + } +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/composer.lock b/fixtures/set035-composer-files-autoload/composer-variable-access/composer.lock new file mode 100644 index 00000000..11f21102 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/composer.lock @@ -0,0 +1,102 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "f2b6445a98e27f6cbff958e8d4a558e5", + "packages": [ + { + "name": "symfony/polyfill-php80", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/excluded-file.php b/fixtures/set035-composer-files-autoload/composer-variable-access/excluded-file.php new file mode 100644 index 00000000..7bb5ecdd --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/excluded-file.php @@ -0,0 +1,5 @@ + [ + 'included-file.php', + 'vendor/symfony/polyfill-php80/bootstrap.php', + ], +]; diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/autoload.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/autoload.php new file mode 100644 index 00000000..458757ba --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/autoload.php @@ -0,0 +1,25 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> + */ + private $prefixesPsr0 = array(); + /** + * @var list + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/InstalledVersions.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/InstalledVersions.php new file mode 100644 index 00000000..51e734a7 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/InstalledVersions.php @@ -0,0 +1,359 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/LICENSE b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/LICENSE new file mode 100644 index 00000000..f27399a0 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_classmap.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_classmap.php new file mode 100644 index 00000000..5490b88d --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_classmap.php @@ -0,0 +1,15 @@ + $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', + 'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', + 'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', + 'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', + 'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', +); diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_files.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_files.php new file mode 100644 index 00000000..ff95d3a4 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_files.php @@ -0,0 +1,12 @@ + $vendorDir . '/symfony/polyfill-php80/bootstrap.php', + '60884d26763a20c18bdf80c8935efaac' => $baseDir . '/included-file.php', + '430aabe1de335715bfb79e58e8c22198' => $baseDir . '/excluded-file.php', +); diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_namespaces.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_namespaces.php new file mode 100644 index 00000000..15a2ff3a --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/symfony/polyfill-php80'), +); diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_real.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_real.php new file mode 100644 index 00000000..f5dede1c --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_real.php @@ -0,0 +1,50 @@ +register(true); + + $filesToLoad = \Composer\Autoload\ComposerStaticInit945f0706470c9d6642a4450ed45b7c04::$files; + $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + + require $file; + } + }, null, null); + foreach ($filesToLoad as $fileIdentifier => $file) { + $requireFile($fileIdentifier, $file); + } + + return $loader; + } +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_static.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_static.php new file mode 100644 index 00000000..8c73d580 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/autoload_static.php @@ -0,0 +1,47 @@ + __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', + '60884d26763a20c18bdf80c8935efaac' => __DIR__ . '/../..' . '/included-file.php', + '430aabe1de335715bfb79e58e8c22198' => __DIR__ . '/../..' . '/excluded-file.php', + ); + + public static $prefixLengthsPsr4 = array ( + 'S' => + array ( + 'Symfony\\Polyfill\\Php80\\' => 23, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Symfony\\Polyfill\\Php80\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php80', + ), + ); + + public static $classMap = array ( + 'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', + 'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', + 'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', + 'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit945f0706470c9d6642a4450ed45b7c04::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit945f0706470c9d6642a4450ed45b7c04::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit945f0706470c9d6642a4450ed45b7c04::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/installed.json b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/installed.json new file mode 100644 index 00000000..ae254a7e --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/installed.json @@ -0,0 +1,92 @@ +{ + "packages": [ + { + "name": "symfony/polyfill-php80", + "version": "v1.28.0", + "version_normalized": "1.28.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "time": "2023-01-26T09:26:14+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php80" + } + ], + "dev": false, + "dev-package-names": [] +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/installed.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/installed.php new file mode 100644 index 00000000..8aa4f998 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/installed.php @@ -0,0 +1,32 @@ + array( + 'name' => '__root__', + 'pretty_version' => '0.18.99', + 'version' => '0.18.99.0', + 'reference' => NULL, + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => false, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => '0.18.99', + 'version' => '0.18.99.0', + 'reference' => NULL, + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-php80' => array( + 'pretty_version' => 'v1.28.0', + 'version' => '1.28.0.0', + 'reference' => '6caa57379c4aec19c0a12a38b59b26487dcfe4b5', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-php80', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/platform_check.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/platform_check.php new file mode 100644 index 00000000..6d3407db --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 70100)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.1.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/LICENSE b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/LICENSE new file mode 100644 index 00000000..0ed3a246 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Php80.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Php80.php new file mode 100644 index 00000000..362dd1a9 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Php80.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php80; + +/** + * @author Ion Bazan + * @author Nico Oelgart + * @author Nicolas Grekas + * + * @internal + */ +final class Php80 +{ + public static function fdiv(float $dividend, float $divisor): float + { + return @($dividend / $divisor); + } + + public static function get_debug_type($value): string + { + switch (true) { + case null === $value: return 'null'; + case \is_bool($value): return 'bool'; + case \is_string($value): return 'string'; + case \is_array($value): return 'array'; + case \is_int($value): return 'int'; + case \is_float($value): return 'float'; + case \is_object($value): break; + case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class'; + default: + if (null === $type = @get_resource_type($value)) { + return 'unknown'; + } + + if ('Unknown' === $type) { + $type = 'closed'; + } + + return "resource ($type)"; + } + + $class = \get_class($value); + + if (false === strpos($class, '@')) { + return $class; + } + + return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous'; + } + + public static function get_resource_id($res): int + { + if (!\is_resource($res) && null === @get_resource_type($res)) { + throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res))); + } + + return (int) $res; + } + + public static function preg_last_error_msg(): string + { + switch (preg_last_error()) { + case \PREG_INTERNAL_ERROR: + return 'Internal error'; + case \PREG_BAD_UTF8_ERROR: + return 'Malformed UTF-8 characters, possibly incorrectly encoded'; + case \PREG_BAD_UTF8_OFFSET_ERROR: + return 'The offset did not correspond to the beginning of a valid UTF-8 code point'; + case \PREG_BACKTRACK_LIMIT_ERROR: + return 'Backtrack limit exhausted'; + case \PREG_RECURSION_LIMIT_ERROR: + return 'Recursion limit exhausted'; + case \PREG_JIT_STACKLIMIT_ERROR: + return 'JIT stack limit exhausted'; + case \PREG_NO_ERROR: + return 'No error'; + default: + return 'Unknown error'; + } + } + + public static function str_contains(string $haystack, string $needle): bool + { + return '' === $needle || false !== strpos($haystack, $needle); + } + + public static function str_starts_with(string $haystack, string $needle): bool + { + return 0 === strncmp($haystack, $needle, \strlen($needle)); + } + + public static function str_ends_with(string $haystack, string $needle): bool + { + if ('' === $needle || $needle === $haystack) { + return true; + } + + if ('' === $haystack) { + return false; + } + + $needleLength = \strlen($needle); + + return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength); + } +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/PhpToken.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/PhpToken.php new file mode 100644 index 00000000..fe6e6910 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/PhpToken.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php80; + +/** + * @author Fedonyuk Anton + * + * @internal + */ +class PhpToken implements \Stringable +{ + /** + * @var int + */ + public $id; + + /** + * @var string + */ + public $text; + + /** + * @var int + */ + public $line; + + /** + * @var int + */ + public $pos; + + public function __construct(int $id, string $text, int $line = -1, int $position = -1) + { + $this->id = $id; + $this->text = $text; + $this->line = $line; + $this->pos = $position; + } + + public function getTokenName(): ?string + { + if ('UNKNOWN' === $name = token_name($this->id)) { + $name = \strlen($this->text) > 1 || \ord($this->text) < 32 ? null : $this->text; + } + + return $name; + } + + /** + * @param int|string|array $kind + */ + public function is($kind): bool + { + foreach ((array) $kind as $value) { + if (\in_array($value, [$this->id, $this->text], true)) { + return true; + } + } + + return false; + } + + public function isIgnorable(): bool + { + return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true); + } + + public function __toString(): string + { + return (string) $this->text; + } + + /** + * @return static[] + */ + public static function tokenize(string $code, int $flags = 0): array + { + $line = 1; + $position = 0; + $tokens = token_get_all($code, $flags); + foreach ($tokens as $index => $token) { + if (\is_string($token)) { + $id = \ord($token); + $text = $token; + } else { + [$id, $text, $line] = $token; + } + $tokens[$index] = new static($id, $text, $line, $position); + $position += \strlen($text); + } + + return $tokens; + } +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/README.md b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/README.md new file mode 100644 index 00000000..3816c559 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/README.md @@ -0,0 +1,25 @@ +Symfony Polyfill / Php80 +======================== + +This component provides features added to PHP 8.0 core: + +- [`Stringable`](https://php.net/stringable) interface +- [`fdiv`](https://php.net/fdiv) +- [`ValueError`](https://php.net/valueerror) class +- [`UnhandledMatchError`](https://php.net/unhandledmatcherror) class +- `FILTER_VALIDATE_BOOL` constant +- [`get_debug_type`](https://php.net/get_debug_type) +- [`PhpToken`](https://php.net/phptoken) class +- [`preg_last_error_msg`](https://php.net/preg_last_error_msg) +- [`str_contains`](https://php.net/str_contains) +- [`str_starts_with`](https://php.net/str_starts_with) +- [`str_ends_with`](https://php.net/str_ends_with) +- [`get_resource_id`](https://php.net/get_resource_id) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php new file mode 100644 index 00000000..2b955423 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#[Attribute(Attribute::TARGET_CLASS)] +final class Attribute +{ + public const TARGET_CLASS = 1; + public const TARGET_FUNCTION = 2; + public const TARGET_METHOD = 4; + public const TARGET_PROPERTY = 8; + public const TARGET_CLASS_CONSTANT = 16; + public const TARGET_PARAMETER = 32; + public const TARGET_ALL = 63; + public const IS_REPEATABLE = 64; + + /** @var int */ + public $flags; + + public function __construct(int $flags = self::TARGET_ALL) + { + $this->flags = $flags; + } +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php new file mode 100644 index 00000000..bd1212f6 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80000 && extension_loaded('tokenizer')) { + class PhpToken extends Symfony\Polyfill\Php80\PhpToken + { + } +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php new file mode 100644 index 00000000..7c62d750 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80000) { + interface Stringable + { + /** + * @return string + */ + public function __toString(); + } +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php new file mode 100644 index 00000000..01c6c6c8 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80000) { + class UnhandledMatchError extends Error + { + } +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/ValueError.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/ValueError.php new file mode 100644 index 00000000..783dbc28 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/Resources/stubs/ValueError.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80000) { + class ValueError extends Error + { + } +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/bootstrap.php b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/bootstrap.php new file mode 100644 index 00000000..e5f7dbc1 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/bootstrap.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php80 as p; + +if (\PHP_VERSION_ID >= 80000) { + return; +} + +if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) { + define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN); +} + +if (!function_exists('fdiv')) { + function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); } +} +if (!function_exists('preg_last_error_msg')) { + function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); } +} +if (!function_exists('str_contains')) { + function str_contains(?string $haystack, ?string $needle): bool { return p\Php80::str_contains($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('str_starts_with')) { + function str_starts_with(?string $haystack, ?string $needle): bool { return p\Php80::str_starts_with($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('str_ends_with')) { + function str_ends_with(?string $haystack, ?string $needle): bool { return p\Php80::str_ends_with($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('get_debug_type')) { + function get_debug_type($value): string { return p\Php80::get_debug_type($value); } +} +if (!function_exists('get_resource_id')) { + function get_resource_id($resource): int { return p\Php80::get_resource_id($resource); } +} diff --git a/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/composer.json b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/composer.json new file mode 100644 index 00000000..f1801f40 --- /dev/null +++ b/fixtures/set035-composer-files-autoload/composer-variable-access/vendor/symfony/polyfill-php80/composer.json @@ -0,0 +1,40 @@ +{ + "name": "symfony/polyfill-php80", + "type": "library", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php80\\": "" }, + "files": [ "bootstrap.php" ], + "classmap": [ "Resources/stubs" ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/fixtures/set035-composer-files-autoload/composer.json b/fixtures/set035-composer-files-autoload/composer.json index 2b787405..6621e0f3 100644 --- a/fixtures/set035-composer-files-autoload/composer.json +++ b/fixtures/set035-composer-files-autoload/composer.json @@ -2,5 +2,10 @@ "bin": "index.php", "require": { "guzzlehttp/guzzle": "^6.5.8" + }, + "autoload": { + "files": [ + "check-non-prefixed-files.php" + ] } } diff --git a/fixtures/set035-composer-files-autoload/index.php b/fixtures/set035-composer-files-autoload/index.php index 4c4e5268..6e3233b2 100644 --- a/fixtures/set035-composer-files-autoload/index.php +++ b/fixtures/set035-composer-files-autoload/index.php @@ -12,6 +12,16 @@ echo 'Autoload Scoped code.' . PHP_EOL; require __DIR__ . '/scoped-guzzle5-include/index.php'; +// Autoload a scoped app. This is to mimic autoloading a scoped app which contains +// an already scoped dependency. +// It is not a real-life scenario in this peculiar case, but it should mimic +// the issue of PHPStan being scoped and executed with: +// - This original intent of this test which is checking colliding hash files. +// - The case of PHPStan which needs to access the Composer global variables. +// It would probably be clearer to have a separate test for this but it was easier to +// fit it here with a confusing explanation instead. +require __DIR__.'/scoped-composer-variable-access/index.php'; + // Autoload the project autoload. This will trigger the autoloading of the files. // Due to Composer creating a hash based on the package name & file path, the // Guzzle file `functions.php` which contains the _non scoped_ facts but in fact diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 72d15eb1..01aeb03e 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -2,7 +2,8 @@ parameters: checkMissingIterableValueType: false checkGenericClassInNonGenericObjectType: false - excludePaths: [] + excludePaths: + - tests/Autoload/AutoloadDumperTest.php ignoreErrors: - message: '#Cannot cast array\\|string to string\.#' diff --git a/src/Autoload/ComposerFileHasher.php b/src/Autoload/ComposerFileHasher.php new file mode 100644 index 00000000..ea8a24b6 --- /dev/null +++ b/src/Autoload/ComposerFileHasher.php @@ -0,0 +1,82 @@ +, + * Pádraic Brady + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Humbug\PhpScoper\Autoload; + +use Symfony\Component\Filesystem\Path; +use function array_map; +use function md5; +use function preg_match; +use function sprintf; + +final class ComposerFileHasher +{ + private const ROOT_PACKAGE_NAME = '__root__'; + private const PACKAGE_PATH_REGEX = '~^%s/(?[^/]+?/[^/]+?)/(?.+?)$~'; + + public static function create( + string $vendorDir, + string $rootDir, + array $filePaths, + ): self { + $vendorDirRelativeToRoot = Path::makeRelative($vendorDir, $rootDir); + + $packagePathRegex = sprintf( + self::PACKAGE_PATH_REGEX, + $vendorDirRelativeToRoot, + ); + + return new self( + $rootDir, + $filePaths, + $packagePathRegex, + ); + } + + public function __construct( + private string $rootDir, + private array $filePaths, + private string $packagePathRegex, + ) { + } + + /** + * @return string[] + */ + public function generateHashes(): array + { + return array_map( + $this->generateHash(...), + $this->filePaths, + ); + } + + /** + * @see \Composer\Autoload::getFileIdentifier() + */ + private function generateHash(string $filePath): string + { + $relativePath = Path::makeRelative($filePath, $this->rootDir); + + if (1 === preg_match($this->packagePathRegex, $relativePath, $matches)) { + $vendor = $matches['vendor']; + $path = $matches['path']; + } else { + $vendor = self::ROOT_PACKAGE_NAME; + $path = $relativePath; + } + + return md5($vendor.':'.$path); + } +} diff --git a/src/Autoload/ScoperAutoloadGenerator.php b/src/Autoload/ScoperAutoloadGenerator.php index a104604c..c5258115 100644 --- a/src/Autoload/ScoperAutoloadGenerator.php +++ b/src/Autoload/ScoperAutoloadGenerator.php @@ -54,8 +54,10 @@ function humbug_phpscoper_expose_class($exposed, $prefixed) { /** @var non-empty-string */ private static string $eol; - public function __construct(private readonly SymbolsRegistry $registry) - { + public function __construct( + private readonly SymbolsRegistry $registry, + private readonly array $excludedComposerAutoloadFileHashes, + ) { self::$eol = chr(10); } @@ -82,6 +84,16 @@ public function dump(): string $statements = implode(self::$eol, $statements); + $excludedComposerAutoloadFiles = count($this->excludedComposerAutoloadFileHashes) === 0 + ? '[]' + : sprintf( + "['%s']", + implode( + "', '", + $this->excludedComposerAutoloadFileHashes, + ), + ); + if ($wrapInNamespace) { $dump = <<getExcludedFilesWithContents(), 0), + ); + $fileHashes = $fileHashGenerator->generateHashes(); - if (null !== $vendorDir) { - $autoload = (new ScoperAutoloadGenerator($symbolsRegistry))->dump(); + $autoload = (new ScoperAutoloadGenerator($symbolsRegistry, $fileHashes))->dump(); $this->fileSystem->dumpFile( $vendorDir.DIRECTORY_SEPARATOR.'scoper-autoload.php', diff --git a/tests/Autoload/AutoloadDumperTest.php b/tests/Autoload/AutoloadDumperTest.php index 65f7fc3c..7f156877 100644 --- a/tests/Autoload/AutoloadDumperTest.php +++ b/tests/Autoload/AutoloadDumperTest.php @@ -30,11 +30,13 @@ final class AutoloadDumperTest extends TestCase */ public function test_it_can_generate_the_autoload( SymbolsRegistry $symbolsRegistry, + array $excludedComposerAutoloadFileHashes, string $autoloadContents, string $expected, ): void { $actual = AutoloadDumper::generateAutoloadStatements( $symbolsRegistry, + $excludedComposerAutoloadFileHashes, $autoloadContents, ); @@ -45,6 +47,7 @@ public static function autoloadProvider(): iterable { yield 'no symbols' => [ new SymbolsRegistry(), + [], <<<'PHP' [ + self::createRegistry( + [ + 'bar' => 'Humbug\bar', + 'foo' => 'Humbug\foo', + ], + [], + ), + ['a610a8e036135f992c6edfb10ca9f4e9', 'e252736c6babb7c097ab6692dbcb2a5a'], + <<<'PHP' + , + * Pádraic Brady + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Autoload; + +use Humbug\PhpScoper\Autoload\ComposerFileHasher; +use PHPUnit\Framework\TestCase; + +/** + * @covers \Humbug\PhpScoper\Autoload\ComposerFileHasher + * @internal + */ +final class ComposerFileHasherTest extends TestCase +{ + /** + * @dataProvider filesProvider + */ + public function test_it_can_get_the_composer_hash_of_the_files( + string $vendorDir, + string $rootDir, + array $filePaths, + array $expected, + ): void { + $hasher = ComposerFileHasher::create($vendorDir, $rootDir, $filePaths); + + $actual = $hasher->generateHashes(); + + self::assertSame($expected, $actual); + } + + public static function filesProvider(): iterable + { + yield [ + '/path/to/project/vendor', + '/path/to/project', + [ + '/path/to/project/src/App.php', + '/path/to/project/vendor/humbug/box/src/Box.php', + ], + [ + md5('__root__:src/App.php'), + md5('humbug/box:src/Box.php'), + ], + ]; + } +} diff --git a/tests/Autoload/ScoperAutoloadGeneratorTest.php b/tests/Autoload/ScoperAutoloadGeneratorTest.php index 52388333..8d7404f1 100644 --- a/tests/Autoload/ScoperAutoloadGeneratorTest.php +++ b/tests/Autoload/ScoperAutoloadGeneratorTest.php @@ -26,9 +26,12 @@ class ScoperAutoloadGeneratorTest extends TestCase /** * @dataProvider provideRegistry */ - public function test_generate_the_autoload(SymbolsRegistry $registry, string $expected): void - { - $generator = new ScoperAutoloadGenerator($registry); + public function test_generate_the_autoload( + SymbolsRegistry $registry, + array $fileHashes, + string $expected, + ): void { + $generator = new ScoperAutoloadGenerator($registry, $fileHashes); $actual = $generator->dump(); @@ -39,6 +42,7 @@ public static function provideRegistry(): iterable { yield 'empty registry' => [ new SymbolsRegistry(), + [], <<<'PHP' [ + new SymbolsRegistry(), + ['a610a8e036135f992c6edfb10ca9f4e9', 'e252736c6babb7c097ab6692dbcb2a5a'], + <<<'PHP' + [ + self::createRegistry( + [ + 'Acme\bar' => 'Humbug\Acme\bar', + 'Acme\foo' => 'Humbug\Acme\foo', + 'Emca\baz' => 'Humbug\Emca\baz', + ], + [], + ), + ['a610a8e036135f992c6edfb10ca9f4e9', 'e252736c6babb7c097ab6692dbcb2a5a'], + <<<'PHP' + 'Humbug\A\Foo', ], ), + [], <<<'PHP' 'Humbug\Bar', ], ), + [], <<<'PHP' 'Humbug\A\Foo', ], ), + [], <<<'PHP' count()) { @@ -39,7 +40,10 @@ public static function generateAutoloadStatements( } $autoloadContents = self::extractInlinedAutoloadContents($autoloadContents); - $scoperStatements = self::getOriginalScoperAutoloaderContents($symbolsRegistry); + $scoperStatements = self::getOriginalScoperAutoloaderContents( + $symbolsRegistry, + $excludedComposerAutoloadFileHashes, + ); $indentedAutoloadContents = self::fixInlinedAutoloadIndent( $autoloadContents, @@ -52,11 +56,7 @@ public static function generateAutoloadStatements( $scoperStatements, ); - return preg_replace( - '/\n{2,}/m', - PHP_EOL.PHP_EOL, - $mergedAutoloadContents, - ); + return self::cleanupDuplicateLineReturns($mergedAutoloadContents); } private static function extractInlinedAutoloadContents(string $autoloadContents): string @@ -72,9 +72,13 @@ private static function extractInlinedAutoloadContents(string $autoloadContents) return $autoloadContents; } - private static function getOriginalScoperAutoloaderContents(SymbolsRegistry $symbolsRegistry): string + private static function getOriginalScoperAutoloaderContents( + SymbolsRegistry $symbolsRegistry, + array $excludedComposerAutoloadFileHashes, + ): string { - $scoperStatements = (new ScoperAutoloadGenerator($symbolsRegistry))->dump(); + $generator = new ScoperAutoloadGenerator($symbolsRegistry, $excludedComposerAutoloadFileHashes); + $scoperStatements = $generator->dump(); return preg_replace( '/scoper\-autoload\.php \@generated by PhpScoper/', @@ -103,4 +107,13 @@ private static function fixInlinedAutoloadIndent(string $autoloadContents, strin return implode(PHP_EOL, $indentedLines); } + + private static function cleanupDuplicateLineReturns(string $value): string + { + return preg_replace( + '/\n{2,}/m', + PHP_EOL.PHP_EOL, + $value, + ); + } } diff --git a/vendor-hotfix/Box.php b/vendor-hotfix/Box.php new file mode 100644 index 00000000..710466bf --- /dev/null +++ b/vendor-hotfix/Box.php @@ -0,0 +1,454 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use Amp\MultiReasonException; +use BadMethodCallException; +use Countable; +use Fidry\FileSystem\FS; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\Compactor\Compactors; +use KevinGH\Box\Compactor\PhpScoper; +use KevinGH\Box\Compactor\Placeholder; +use KevinGH\Box\Phar\CompressionAlgorithm; +use KevinGH\Box\PhpScoper\NullScoper; +use KevinGH\Box\PhpScoper\Scoper; +use Phar; +use RecursiveDirectoryIterator; +use RuntimeException; +use SplFileInfo; +use Webmozart\Assert\Assert; +use function Amp\ParallelFunctions\parallelMap; +use function Amp\Promise\wait; +use function array_filter; +use function array_map; +use function array_unshift; +use function chdir; +use function dirname; +use function extension_loaded; +use function file_exists; +use function getcwd; +use function is_object; +use function openssl_pkey_export; +use function openssl_pkey_get_details; +use function openssl_pkey_get_private; +use function sprintf; + +/** + * Box is a utility class to generate a PHAR. + * + * @private + */ +final class Box implements Countable +{ + private Compactors $compactors; + private Placeholder $placeholderCompactor; + private MapFile $mapFile; + private Scoper $scoper; + private bool $buffering = false; + + /** + * @var array Relative file path as key and file contents as value + */ + private array $bufferedFiles = []; + + private function __construct( + private readonly Phar $phar, + private readonly string $pharFilePath, + ) { + $this->compactors = new Compactors(); + $this->placeholderCompactor = new Placeholder([]); + $this->mapFile = new MapFile(getcwd(), []); + $this->scoper = new NullScoper(); + } + + /** + * Creates a new PHAR and Box instance. + * + * @param string $pharFilePath The PHAR file name + * @param int $pharFlags Flags to pass to the Phar parent class RecursiveDirectoryIterator + * @param string $pharAlias Alias with which the Phar archive should be referred to in calls to stream functionality + * + * @see RecursiveDirectoryIterator + */ + public static function create(string $pharFilePath, int $pharFlags = 0, ?string $pharAlias = null): self + { + // Ensure the parent directory of the PHAR file exists as `new \Phar()` does not create it and would fail + // otherwise. + FS::mkdir(dirname($pharFilePath)); + + return new self( + new Phar($pharFilePath, $pharFlags, $pharAlias), + $pharFilePath, + ); + } + + public function startBuffering(): void + { + Assert::false($this->buffering, 'The buffering must be ended before starting it again'); + + $this->buffering = true; + + $this->phar->startBuffering(); + } + + /** + * @param callable(SymbolsRegistry, string): void $dumpAutoload + */ + public function endBuffering(?callable $dumpAutoload): void + { + Assert::true($this->buffering, 'The buffering must be started before ending it'); + + $dumpAutoload ??= static fn () => null; + $cwd = getcwd(); + + $tmp = FS::makeTmpDir('box', self::class); + chdir($tmp); + + if ([] === $this->bufferedFiles) { + $this->bufferedFiles = [ + '.box_empty' => 'A PHAR cannot be empty so Box adds this file to ensure the PHAR is created still.', + ]; + } + + try { + foreach ($this->bufferedFiles as $file => $contents) { + FS::dumpFile($file, $contents); + } + + if (null !== $dumpAutoload) { + $dumpAutoload( + $this->scoper->getSymbolsRegistry(), + $this->scoper->getPrefix(), + $this->scoper->getExcludedFilePaths(), + ); + } + + chdir($cwd); + + $this->phar->buildFromDirectory($tmp); + } finally { + FS::remove($tmp); + } + + $this->buffering = false; + + $this->phar->stopBuffering(); + } + + /** + * @param non-empty-string $normalizedVendorDir Normalized path ("/" path separator and no trailing "/") to the Composer vendor directory + */ + public function removeComposerArtefacts(string $normalizedVendorDir): void + { + Assert::false($this->buffering, 'The buffering must have ended before removing the Composer artefacts'); + + $composerFiles = [ + 'composer.json', + 'composer.lock', + $normalizedVendorDir.'/composer/installed.json', + ]; + + $this->phar->startBuffering(); + + foreach ($composerFiles as $composerFile) { + $localComposerFile = ($this->mapFile)($composerFile); + + $pharFilePath = sprintf( + 'phar://%s/%s', + $this->phar->getPath(), + $localComposerFile, + ); + + if (file_exists($pharFilePath)) { + $this->phar->delete($localComposerFile); + } + } + + $this->phar->stopBuffering(); + } + + public function compress(CompressionAlgorithm $compressionAlgorithm): ?string + { + Assert::false($this->buffering, 'Cannot compress files while buffering.'); + + $extensionRequired = $compressionAlgorithm->getRequiredExtension(); + + if (null !== $extensionRequired && false === extension_loaded($extensionRequired)) { + throw new RuntimeException( + sprintf( + 'Cannot compress the PHAR with the compression algorithm "%s": the extension "%s" is required but appear to not be loaded', + $compressionAlgorithm->name, + $extensionRequired, + ), + ); + } + + try { + if (CompressionAlgorithm::NONE === $compressionAlgorithm) { + $this->getPhar()->decompressFiles(); + } else { + $this->phar->compressFiles($compressionAlgorithm->value); + } + } catch (BadMethodCallException $exception) { + $exceptionMessage = 'unable to create temporary file' !== $exception->getMessage() + ? 'Could not compress the PHAR: '.$exception->getMessage() + : sprintf( + 'Could not compress the PHAR: the compression requires too many file descriptors to be opened (%s). Check your system limits or install the posix extension to allow Box to automatically configure it during the compression', + $this->phar->count(), + ); + + throw new RuntimeException($exceptionMessage, $exception->getCode(), $exception); + } + + return $extensionRequired; + } + + public function registerCompactors(Compactors $compactors): void + { + $compactorsArray = $compactors->toArray(); + + foreach ($compactorsArray as $index => $compactor) { + if ($compactor instanceof PhpScoper) { + $this->scoper = $compactor->getScoper(); + + continue; + } + + if ($compactor instanceof Placeholder) { + // Removes the known Placeholder compactors in favour of the Box one + unset($compactorsArray[$index]); + } + } + + array_unshift($compactorsArray, $this->placeholderCompactor); + + $this->compactors = new Compactors(...$compactorsArray); + } + + /** + * @param scalar[] $placeholders + */ + public function registerPlaceholders(array $placeholders): void + { + $message = 'Expected value "%s" to be a scalar or stringable object.'; + + foreach ($placeholders as $index => $placeholder) { + if (is_object($placeholder)) { + Assert::methodExists($placeholder, '__toString', $message); + + $placeholders[$index] = (string) $placeholder; + + break; + } + + Assert::scalar($placeholder, $message); + } + + $this->placeholderCompactor = new Placeholder($placeholders); + + $this->registerCompactors($this->compactors); + } + + public function registerFileMapping(MapFile $fileMapper): void + { + $this->mapFile = $fileMapper; + } + + public function registerStub(string $file): void + { + $contents = $this->placeholderCompactor->compact( + $file, + FS::getFileContents($file), + ); + + $this->phar->setStub($contents); + } + + /** + * @param array $files + * + * @throws MultiReasonException + */ + public function addFiles(array $files, bool $binary): void + { + Assert::true($this->buffering, 'Cannot add files if the buffering has not started.'); + + $files = array_map('strval', $files); + + if ($binary) { + foreach ($files as $file) { + $this->addFile($file, null, true); + } + + return; + } + + foreach ($this->processContents($files) as [$file, $contents]) { + $this->bufferedFiles[$file] = $contents; + } + } + + /** + * Adds the a file to the PHAR. The contents will first be compacted and have its placeholders + * replaced. + * + * @param null|string $contents If null the content of the file will be used + * @param bool $binary When true means the file content shouldn't be processed + * + * @return string File local path + */ + public function addFile(string $file, ?string $contents = null, bool $binary = false): string + { + Assert::true($this->buffering, 'Cannot add files if the buffering has not started.'); + + if (null === $contents) { + $contents = FS::getFileContents($file); + } + + $local = ($this->mapFile)($file); + + $this->bufferedFiles[$local] = $binary ? $contents : $this->compactors->compact($local, $contents); + + return $local; + } + + public function getPhar(): Phar + { + return $this->phar; + } + + /** + * Signs the PHAR using a private key file. + * + * @param string $file the private key file name + * @param null|string $password the private key password + */ + public function signUsingFile(string $file, ?string $password = null): void + { + $this->sign(FS::getFileContents($file), $password); + } + + /** + * Signs the PHAR using a private key. + * + * @param string $key The private key + * @param null|string $password The private key password + */ + public function sign(string $key, ?string $password): void + { + $pubKey = $this->pharFilePath.'.pubkey'; + + Assert::writable(dirname($pubKey)); + Assert::true(extension_loaded('openssl')); + + if (file_exists($pubKey)) { + Assert::file( + $pubKey, + 'Cannot create public key: %s already exists and is not a file.', + ); + } + + $resource = openssl_pkey_get_private($key, (string) $password); + + Assert::notSame(false, $resource, 'Could not retrieve the private key, check that the password is correct.'); + + openssl_pkey_export($resource, $private); + + $details = openssl_pkey_get_details($resource); + + $this->phar->setSignatureAlgorithm(Phar::OPENSSL, $private); + + FS::dumpFile($pubKey, $details['key']); + } + + /** + * @param string[] $files + * + * @throws MultiReasonException + * + * @return array array of tuples where the first element is the local file path (path inside the PHAR) and the + * second element is the processed contents + */ + private function processContents(array $files): array + { + $mapFile = $this->mapFile; + $compactors = $this->compactors; + $cwd = getcwd(); + + $processFile = static function (string $file) use ($cwd, $mapFile, $compactors): array { + chdir($cwd); + + // Keep the fully qualified call here since this function may be executed without the right autoloading + // mechanism + \KevinGH\Box\register_aliases(); + if (true === \KevinGH\Box\is_parallel_processing_enabled()) { + \KevinGH\Box\register_error_handler(); + } + + $contents = \Fidry\FileSystem\FS::getFileContents($file); + + $local = $mapFile($file); + + $processedContents = $compactors->compact($local, $contents); + + return [$local, $processedContents, $compactors->getScoperSymbolsRegistry()]; + }; + + if ($this->scoper instanceof NullScoper || false === is_parallel_processing_enabled()) { + return array_map($processFile, $files); + } + + // In the case of parallel processing, an issue is caused due to the statefulness nature of the PhpScoper + // symbols registry. + // + // Indeed, the PhpScoper symbols registry stores the records of exposed/excluded classes and functions. If nothing is done, + // then the symbols registry retrieved in the end will here will be "blank" since the updated symbols registries are the ones + // from the workers used for the parallel processing. + // + // In order to avoid that, the symbols registries will be returned as a result as well in order to be able to merge + // all the symbols registries into one. + // + // This process is allowed thanks to the nature of the state of the symbols registries: having redundant classes or + // functions registered can easily be deal with so merging all those different states is actually + // straightforward. + $tuples = wait(parallelMap($files, $processFile)); + + if ([] === $tuples) { + return []; + } + + $filesWithContents = []; + $symbolRegistries = []; + + foreach ($tuples as [$local, $processedContents, $symbolRegistry]) { + $filesWithContents[] = [$local, $processedContents]; + $symbolRegistries[] = $symbolRegistry; + } + + $this->compactors->registerSymbolsRegistry( + SymbolsRegistry::createFromRegistries(array_filter($symbolRegistries)), + ); + + return $filesWithContents; + } + + public function count(): int + { + Assert::false($this->buffering, 'Cannot count the number of files in the PHAR when buffering'); + + return $this->phar->count(); + } +} diff --git a/vendor-hotfix/Compile.php b/vendor-hotfix/Compile.php new file mode 100644 index 00000000..7a0f9e9e --- /dev/null +++ b/vendor-hotfix/Compile.php @@ -0,0 +1,961 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Amp\MultiReasonException; +use Fidry\Console\Command\Command; +use Fidry\Console\Command\CommandAware; +use Fidry\Console\Command\CommandAwareness; +use Fidry\Console\Command\Configuration as CommandConfiguration; +use Fidry\Console\ExitCode; +use Fidry\Console\Input\IO; +use Fidry\FileSystem\FileSystem; +use Fidry\FileSystem\FS; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\Amp\FailureCollector; +use KevinGH\Box\Box; +use KevinGH\Box\Compactor\Compactor; +use KevinGH\Box\Composer\CompilerPsrLogger; +use KevinGH\Box\Composer\ComposerConfiguration; +use KevinGH\Box\Composer\ComposerOrchestrator; +use KevinGH\Box\Composer\ComposerProcessFactory; +use KevinGH\Box\Composer\IncompatibleComposerVersion; +use KevinGH\Box\Configuration\Configuration; +use KevinGH\Box\Console\Logger\CompilerLogger; +use KevinGH\Box\Console\MessageRenderer; +use KevinGH\Box\MapFile; +use KevinGH\Box\Phar\CompressionAlgorithm; +use KevinGH\Box\RequirementChecker\DecodedComposerJson; +use KevinGH\Box\RequirementChecker\DecodedComposerLock; +use KevinGH\Box\RequirementChecker\RequirementsDumper; +use KevinGH\Box\StubGenerator; +use RuntimeException; +use stdClass; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Filesystem\Path; +use Webmozart\Assert\Assert; +use function array_map; +use function array_shift; +use function count; +use function decoct; +use function explode; +use function file_exists; +use function filesize; +use function implode; +use function is_callable; +use function is_string; +use function KevinGH\Box\bump_open_file_descriptor_limit; +use function KevinGH\Box\check_php_settings; +use function KevinGH\Box\disable_parallel_processing; +use function KevinGH\Box\format_size; +use function KevinGH\Box\format_time; +use function memory_get_peak_usage; +use function memory_get_usage; +use function microtime; +use function putenv; +use function Safe\getcwd; +use function sprintf; +use function var_export; +use const KevinGH\Box\BOX_ALLOW_XDEBUG; +use const PHP_EOL; + +/** + * @private + */ +final class Compile implements CommandAware +{ + use CommandAwareness; + + public const NAME = 'compile'; + + private const HELP = <<<'HELP' + The %command.name% command will compile code in a new PHAR based on a variety of settings. + + This command relies on a configuration file for loading + PHAR packaging settings. If a configuration file is not + specified through the --config|-c option, one of + the following files will be used (in order): box.json, + box.json.dist + + The configuration file is actually a JSON object saved to a file. For more + information check the documentation online: + + https://github.com/humbug/box + + HELP; + + private const DEBUG_OPTION = 'debug'; + private const NO_PARALLEL_PROCESSING_OPTION = 'no-parallel'; + private const NO_RESTART_OPTION = 'no-restart'; + private const DEV_OPTION = 'dev'; + private const NO_CONFIG_OPTION = 'no-config'; + private const WITH_DOCKER_OPTION = 'with-docker'; + private const COMPOSER_BIN_OPTION = 'composer-bin'; + private const ALLOW_COMPOSER_COMPOSER_CHECK_FAILURE_OPTION = 'allow-composer-check-failure'; + + private const DEBUG_DIR = '.box_dump'; + + public function __construct(private string $header) + { + } + + public function getConfiguration(): CommandConfiguration + { + return new CommandConfiguration( + self::NAME, + '🔨 Compiles an application into a PHAR', + self::HELP, + [], + [ + new InputOption( + self::DEBUG_OPTION, + null, + InputOption::VALUE_NONE, + 'Dump the files added to the PHAR in a `'.self::DEBUG_DIR.'` directory', + ), + new InputOption( + self::NO_PARALLEL_PROCESSING_OPTION, + null, + InputOption::VALUE_NONE, + 'Disable the parallel processing', + ), + new InputOption( + self::NO_RESTART_OPTION, + null, + InputOption::VALUE_NONE, + 'Do not restart the PHP process. Box restarts the process by default to disable xdebug and set `phar.readonly=0`', + ), + new InputOption( + self::DEV_OPTION, + null, + InputOption::VALUE_NONE, + 'Skips the compression step', + ), + new InputOption( + self::NO_CONFIG_OPTION, + null, + InputOption::VALUE_NONE, + 'Ignore the config file even when one is specified with the --config option', + ), + new InputOption( + self::WITH_DOCKER_OPTION, + null, + InputOption::VALUE_NONE, + 'Generates a Dockerfile', + ), + new InputOption( + self::COMPOSER_BIN_OPTION, + null, + InputOption::VALUE_REQUIRED, + 'Composer binary to use', + ), + new InputOption( + self::ALLOW_COMPOSER_COMPOSER_CHECK_FAILURE_OPTION, + null, + InputOption::VALUE_NONE, + 'To continue even if an unsupported Composer version is detected', + ), + ConfigOption::getOptionInput(), + ChangeWorkingDirOption::getOptionInput(), + ], + ); + } + + public function execute(IO $io): int + { + if ($io->getOption(self::NO_RESTART_OPTION)->asBoolean()) { + putenv(BOX_ALLOW_XDEBUG.'=1'); + } + + $debug = $io->getOption(self::DEBUG_OPTION)->asBoolean(); + + if ($debug) { + $io->setVerbosity(OutputInterface::VERBOSITY_DEBUG); + } + + check_php_settings($io); + + if ($io->getOption(self::NO_PARALLEL_PROCESSING_OPTION)->asBoolean()) { + disable_parallel_processing(); + $io->writeln( + '[debug] Disabled parallel processing', + OutputInterface::VERBOSITY_DEBUG, + ); + } + + ChangeWorkingDirOption::changeWorkingDirectory($io); + + $io->writeln($this->header); + + $config = $io->getOption(self::NO_CONFIG_OPTION)->asBoolean() + ? Configuration::create(null, new stdClass()) + : ConfigOption::getConfig($io, true); + $config->setComposerBin(self::getComposerBin($io)); + $path = $config->getOutputPath(); + + $logger = new CompilerLogger($io); + + $startTime = microtime(true); + + $logger->logStartBuilding($path); + + $this->removeExistingArtifacts($config, $logger, $debug); + + // Adding files might result in opening a lot of files. Either because not parallelized or when creating the + // workers for parallelization. + // As a result, we bump the file descriptor to an arbitrary number to ensure this process can run correctly + $restoreLimit = bump_open_file_descriptor_limit(2048, $io); + + try { + $box = $this->createPhar($config, $logger, $io, $debug); + } finally { + $restoreLimit(); + } + + self::correctPermissions($path, $config, $logger); + + self::logEndBuilding($config, $logger, $io, $box, $path, $startTime); + + if ($io->getOption(self::WITH_DOCKER_OPTION)->asBoolean()) { + return $this->generateDockerFile($io); + } + + return ExitCode::SUCCESS; + } + + private function createPhar( + Configuration $config, + CompilerLogger $logger, + IO $io, + bool $debug, + ): Box { + $box = Box::create($config->getTmpOutputPath()); + $composerOrchestrator = new ComposerOrchestrator( + ComposerProcessFactory::create( + $config->getComposerBin(), + $io, + ), + new CompilerPsrLogger($logger), + new FileSystem(), + ); + + self::checkComposerVersion($composerOrchestrator, $config, $logger, $io); + + $box->startBuffering(); + + self::registerReplacementValues($config, $box, $logger); + self::registerCompactors($config, $box, $logger); + self::registerFileMapping($config, $box, $logger); + + // Registering the main script _before_ adding the rest if of the files is _very_ important. The temporary + // file used for debugging purposes and the Composer dump autoloading will not work correctly otherwise. + $main = self::registerMainScript($config, $box, $logger); + + $check = self::registerRequirementsChecker($config, $box, $logger); + + self::addFiles($config, $box, $logger, $io); + + self::registerStub($config, $box, $main, $check, $logger); + self::configureMetadata($config, $box, $logger); + + self::commit($box, $composerOrchestrator, $config, $logger); + + self::checkComposerFiles($box, $config, $logger); + + if ($debug) { + $box->getPhar()->extractTo(self::DEBUG_DIR, null, true); + } + + self::configureCompressionAlgorithm( + $config, + $box, + $io->getOption(self::DEV_OPTION)->asBoolean(), + $io, + $logger, + ); + + self::signPhar($config, $box, $config->getTmpOutputPath(), $io, $logger); + + if ($config->getTmpOutputPath() !== $config->getOutputPath()) { + FS::rename($config->getTmpOutputPath(), $config->getOutputPath()); + } + + return $box; + } + + private static function getComposerBin(IO $io): ?string + { + $composerBin = $io->getOption(self::COMPOSER_BIN_OPTION)->asNullableNonEmptyString(); + + return null === $composerBin ? null : Path::makeAbsolute($composerBin, getcwd()); + } + + private function removeExistingArtifacts(Configuration $config, CompilerLogger $logger, bool $debug): void + { + $path = $config->getOutputPath(); + + if ($debug) { + FS::remove(self::DEBUG_DIR); + + FS::dumpFile( + self::DEBUG_DIR.'/.box_configuration', + ConfigurationExporter::export($config), + ); + } + + if (false === file_exists($path)) { + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Removing the existing PHAR "%s"', + $path, + ), + ); + + FS::remove($path); + } + + private static function checkComposerVersion( + ComposerOrchestrator $composerOrchestrator, + Configuration $config, + CompilerLogger $logger, + IO $io, + ): void { + if (!$config->dumpAutoload()) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Skipping the Composer compatibility check: the autoloader is not dumped', + ); + + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Checking Composer compatibility', + ); + + try { + $composerOrchestrator->checkVersion(); + + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + 'Supported version detected', + ); + } catch (IncompatibleComposerVersion $incompatibleComposerVersion) { + if ($io->getOption(self::ALLOW_COMPOSER_COMPOSER_CHECK_FAILURE_OPTION)->asBoolean()) { + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + 'Warning! Incompatible composer version detected: '.$incompatibleComposerVersion->getMessage(), + ); + + return; // Swallow the exception + } + + throw $incompatibleComposerVersion; + } + } + + private static function registerReplacementValues(Configuration $config, Box $box, CompilerLogger $logger): void + { + $values = $config->getReplacements(); + + if (0 === count($values)) { + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Setting replacement values', + ); + + foreach ($values as $key => $value) { + $logger->log( + CompilerLogger::PLUS_PREFIX, + sprintf( + '%s: %s', + $key, + $value, + ), + ); + } + + $box->registerPlaceholders($values); + } + + private static function registerCompactors(Configuration $config, Box $box, CompilerLogger $logger): void + { + $compactors = $config->getCompactors(); + + if (0 === count($compactors)) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'No compactor to register', + ); + + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Registering compactors', + ); + + $logCompactors = static function (Compactor $compactor) use ($logger): void { + $compactorClassParts = explode('\\', $compactor::class); + + if (str_starts_with($compactorClassParts[0], '_HumbugBox')) { + // Keep the non prefixed class name for the user + array_shift($compactorClassParts); + } + + $logger->log( + CompilerLogger::PLUS_PREFIX, + implode('\\', $compactorClassParts), + ); + }; + + array_map($logCompactors, $compactors->toArray()); + + $box->registerCompactors($compactors); + } + + private static function registerFileMapping(Configuration $config, Box $box, CompilerLogger $logger): void + { + $fileMapper = $config->getFileMapper(); + + self::logMap($fileMapper, $logger); + + $box->registerFileMapping($fileMapper); + } + + private static function addFiles(Configuration $config, Box $box, CompilerLogger $logger, IO $io): void + { + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, 'Adding binary files'); + + $count = count($config->getBinaryFiles()); + + $box->addFiles($config->getBinaryFiles(), true); + + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + 0 === $count + ? 'No file found' + : sprintf('%d file(s)', $count), + ); + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Auto-discover files? %s', + $config->hasAutodiscoveredFiles() ? 'Yes' : 'No', + ), + ); + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Exclude dev files? %s', + $config->excludeDevFiles() ? 'Yes' : 'No', + ), + ); + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, 'Adding files'); + + $count = count($config->getFiles()); + + self::addFilesWithErrorHandling($config, $box, $io); + + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + 0 === $count + ? 'No file found' + : sprintf('%d file(s)', $count), + ); + } + + private static function addFilesWithErrorHandling(Configuration $config, Box $box, IO $io): void + { + try { + $box->addFiles($config->getFiles(), false); + + return; + } catch (MultiReasonException $ampFailure) { + // Continue + } + + // This exception is handled a different way to give me meaningful feedback to the user + $io->error([ + 'An Amp\Parallel error occurred. To diagnostic if it is an Amp error related, you may try again with "--no-parallel".', + 'Reason(s) of the failure:', + ...FailureCollector::collectReasons($ampFailure), + ]); + + throw $ampFailure; + } + + private static function registerMainScript(Configuration $config, Box $box, CompilerLogger $logger): ?string + { + if (false === $config->hasMainScript()) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'No main script path configured', + ); + + return null; + } + + $main = $config->getMainScriptPath(); + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Adding main file: %s', + $main, + ), + ); + + $localMain = $box->addFile( + $main, + $config->getMainScriptContents(), + ); + + $relativeMain = Path::makeRelative($main, $config->getBasePath()); + + if ($localMain !== $relativeMain) { + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + $localMain, + ); + } + + return $localMain; + } + + private static function registerRequirementsChecker(Configuration $config, Box $box, CompilerLogger $logger): bool + { + if (false === $config->checkRequirements()) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Skip requirements checker', + ); + + return false; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Adding requirements checker', + ); + + $checkFiles = RequirementsDumper::dump( + new DecodedComposerJson($config->getDecodedComposerJsonContents() ?? []), + new DecodedComposerLock($config->getDecodedComposerLockContents() ?? []), + $config->getCompressionAlgorithm(), + ); + + foreach ($checkFiles as $fileWithContents) { + [$file, $contents] = $fileWithContents; + + $box->addFile('.box/'.$file, $contents, true); + } + + return true; + } + + private static function registerStub( + Configuration $config, + Box $box, + ?string $main, + bool $checkRequirements, + CompilerLogger $logger, + ): void { + if ($config->isStubGenerated()) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Generating new stub', + ); + + $stub = self::createStub($config, $main, $checkRequirements, $logger); + + $box->getPhar()->setStub($stub); + + return; + } + + if (null !== ($stub = $config->getStubPath())) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Using stub file: %s', + $stub, + ), + ); + + $box->registerStub($stub); + + return; + } + + $aliasWasAdded = $box->getPhar()->setAlias($config->getAlias()); + + Assert::true( + $aliasWasAdded, + sprintf( + 'The alias "%s" is invalid. See Phar::setAlias() documentation for more information.', + $config->getAlias(), + ), + ); + + $box->getPhar()->setDefaultStub($main); + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Using default stub', + ); + } + + private static function configureMetadata(Configuration $config, Box $box, CompilerLogger $logger): void + { + if (null !== ($metadata = $config->getMetadata())) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Setting metadata', + ); + + if (is_callable($metadata)) { + $metadata = $metadata(); + } + + $logger->log( + CompilerLogger::MINUS_PREFIX, + is_string($metadata) ? $metadata : var_export($metadata, true), + ); + + $box->getPhar()->setMetadata($metadata); + } + } + + private static function commit( + Box $box, + ComposerOrchestrator $composerOrchestrator, + Configuration $config, + CompilerLogger $logger, + ): void { + $message = $config->dumpAutoload() + ? 'Dumping the Composer autoloader' + : 'Skipping dumping the Composer autoloader'; + + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, $message); + + $excludeDevFiles = $config->excludeDevFiles(); + + $box->endBuffering( + $config->dumpAutoload() + ? static fn (SymbolsRegistry $symbolsRegistry, string $prefix, array $excludeScoperFiles) => $composerOrchestrator->dumpAutoload( + $symbolsRegistry, + $prefix, + $excludeDevFiles, + $excludeScoperFiles, + ) + : null, + ); + } + + private static function checkComposerFiles(Box $box, Configuration $config, CompilerLogger $logger): void + { + $message = $config->excludeComposerFiles() + ? 'Removing the Composer dump artefacts' + : 'Keep the Composer dump artefacts'; + + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, $message); + + if ($config->excludeComposerFiles()) { + $box->removeComposerArtefacts( + ComposerConfiguration::retrieveVendorDir( + $config->getDecodedComposerJsonContents() ?? [], + ), + ); + } + } + + private static function configureCompressionAlgorithm( + Configuration $config, + Box $box, + bool $dev, + IO $io, + CompilerLogger $logger, + ): void { + $algorithm = $config->getCompressionAlgorithm(); + + if (CompressionAlgorithm::NONE === $algorithm) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'No compression', + ); + + return; + } + + if ($dev) { + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, 'Dev mode detected: skipping the compression'); + + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Compressing with the algorithm "%s"', + $algorithm->name, + ), + ); + + $restoreLimit = bump_open_file_descriptor_limit(count($box), $io); + + try { + $extension = $box->compress($algorithm); + + if (null !== $extension) { + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + sprintf( + 'Warning: the extension "%s" will now be required to execute the PHAR', + $extension, + ), + ); + } + } catch (RuntimeException $exception) { + $io->error($exception->getMessage()); + + // Continue: the compression failure should not result in completely bailing out the compilation process + } finally { + $restoreLimit(); + } + } + + private static function signPhar( + Configuration $config, + Box $box, + string $path, + IO $io, + CompilerLogger $logger, + ): void { + // Sign using private key when applicable + FS::remove($path.'.pubkey'); + + $key = $config->getPrivateKeyPath(); + + if (null === $key) { + $box->getPhar()->setSignatureAlgorithm( + $config->getSigningAlgorithm()->value, + ); + + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Signing using a private key', + ); + + $passphrase = $config->getPrivateKeyPassphrase(); + + if ($config->promptForPrivateKey()) { + if (false === $io->isInteractive()) { + throw new RuntimeException( + sprintf( + 'Accessing to the private key "%s" requires a passphrase but none provided. Either ' + .'provide one or run this command in interactive mode.', + $key, + ), + ); + } + + $question = new Question('Private key passphrase'); + $question->setHidden(false); + $question->setHiddenFallback(false); + + $passphrase = $io->askQuestion($question); + + $io->writeln(''); + } + + $box->signUsingFile($key, $passphrase); + } + + private static function correctPermissions(string $path, Configuration $config, CompilerLogger $logger): void + { + if (null !== ($chmod = $config->getFileMode())) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Setting file permissions to %s', + '0'.decoct($chmod), + ), + ); + + FS::chmod($path, $chmod); + } + } + + private static function createStub( + Configuration $config, + ?string $main, + bool $checkRequirements, + CompilerLogger $logger, + ): string { + $shebang = $config->getShebang(); + $bannerPath = $config->getStubBannerPath(); + $bannerContents = $config->getStubBannerContents(); + + if (null !== $shebang) { + $logger->log( + CompilerLogger::MINUS_PREFIX, + sprintf( + 'Using shebang line: %s', + $shebang, + ), + ); + } else { + $logger->log( + CompilerLogger::MINUS_PREFIX, + 'No shebang line', + ); + } + + if (null !== $bannerPath) { + $logger->log( + CompilerLogger::MINUS_PREFIX, + sprintf( + 'Using custom banner from file: %s', + $bannerPath, + ), + ); + } elseif (null !== $bannerContents) { + $logger->log( + CompilerLogger::MINUS_PREFIX, + 'Using banner:', + ); + + $bannerLines = explode("\n", $bannerContents); + + foreach ($bannerLines as $bannerLine) { + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + $bannerLine, + ); + } + } + + return StubGenerator::generateStub( + $config->getAlias(), + $bannerContents, + $main, + $config->isInterceptFileFuncs(), + $shebang, + $checkRequirements, + ); + } + + private static function logMap(MapFile $fileMapper, CompilerLogger $logger): void + { + $map = $fileMapper->getMap(); + + if (0 === count($map)) { + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Mapping paths', + ); + + foreach ($map as $item) { + foreach ($item as $match => $replace) { + if ('' === $match) { + $match = '(all)'; + $replace .= '/'; + } + + $logger->log( + CompilerLogger::MINUS_PREFIX, + sprintf( + '%s > %s', + $match, + $replace, + ), + ); + } + } + } + + private static function logEndBuilding( + Configuration $config, + CompilerLogger $logger, + IO $io, + Box $box, + string $path, + float $startTime, + ): void { + $logger->log( + CompilerLogger::STAR_PREFIX, + 'Done.', + ); + $io->newLine(); + + MessageRenderer::render($io, $config->getRecommendations(), $config->getWarnings()); + + $io->comment( + sprintf( + 'PHAR: %s (%s)', + $box->count() > 1 ? $box->count().' files' : $box->count().' file', + format_size( + filesize($path), + ), + ) + .PHP_EOL + .'You can inspect the generated PHAR with the "info" command.', + ); + + $io->comment( + sprintf( + 'Memory usage: %s (peak: %s), time: %s', + format_size(memory_get_usage()), + format_size(memory_get_peak_usage()), + format_time(microtime(true) - $startTime), + ), + ); + } + + private function generateDockerFile(IO $io): int + { + $input = new StringInput(''); + $input->setInteractive(false); + + return $this->getDockerCommand()->execute( + new IO($input, $io->getOutput()), + ); + } + + private function getDockerCommand(): Command + { + return $this->getCommandRegistry()->findCommand(GenerateDockerFile::NAME); + } +} diff --git a/vendor-hotfix/ComposerOrchestrator.php b/vendor-hotfix/ComposerOrchestrator.php new file mode 100644 index 00000000..db259dd7 --- /dev/null +++ b/vendor-hotfix/ComposerOrchestrator.php @@ -0,0 +1,186 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use Composer\Semver\Semver; +use Fidry\Console\Input\IO; +use Fidry\FileSystem\FileSystem; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\NotInstantiable; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use RuntimeException; +use Symfony\Component\Process\Exception\ProcessFailedException; +use function sprintf; +use function trim; +use const PHP_EOL; + +/** + * @private + */ +final class ComposerOrchestrator +{ + use NotInstantiable; + + public const SUPPORTED_VERSION_CONSTRAINTS = '^2.2.0'; + + private string $detectedVersion; + + public static function create(): self + { + return new self( + ComposerProcessFactory::create(io: IO::createNull()), + new NullLogger(), + new FileSystem(), + ); + } + + public function __construct( + private ComposerProcessFactory $processFactory, + private LoggerInterface $logger, + private FileSystem $fileSystem, + ) { + } + + /** + * @throws UndetectableComposerVersion + */ + public function getVersion(): string + { + if (isset($this->detectedVersion)) { + return $this->detectedVersion; + } + + $getVersionProcess = $this->processFactory->getVersionProcess(); + + $this->logger->info($getVersionProcess->getCommandLine()); + + $getVersionProcess->run(); + + if (false === $getVersionProcess->isSuccessful()) { + throw UndetectableComposerVersion::forFailedProcess($getVersionProcess); + } + + $output = $getVersionProcess->getOutput(); + + if (1 !== preg_match('/Composer version (\S+?) /', $output, $match)) { + throw UndetectableComposerVersion::forOutput( + $getVersionProcess, + $output, + ); + } + + $this->detectedVersion = $match[1]; + + return $this->detectedVersion; + } + + /** + * @throws UndetectableComposerVersion + * @throws IncompatibleComposerVersion + */ + public function checkVersion(): void + { + $version = $this->getVersion(); + + $this->logger->info( + sprintf( + 'Version detected: %s (Box requires %s)', + $version, + self::SUPPORTED_VERSION_CONSTRAINTS, + ), + ); + + if (!Semver::satisfies($version, self::SUPPORTED_VERSION_CONSTRAINTS)) { + throw IncompatibleComposerVersion::create($version, self::SUPPORTED_VERSION_CONSTRAINTS); + } + } + + public function dumpAutoload( + SymbolsRegistry $symbolsRegistry, + string $prefix, + bool $excludeDevFiles, + array $excludedComposerAutoloadFileHashes, + ): void { + $this->dumpAutoloader(true === $excludeDevFiles); + + if ('' === $prefix) { + return; + } + + $autoloadFile = $this->getVendorDir().'/autoload.php'; + + $autoloadContents = AutoloadDumper::generateAutoloadStatements( + $symbolsRegistry, + $excludedComposerAutoloadFileHashes, + $this->fileSystem->getFileContents($autoloadFile), + ); + + $this->fileSystem->dumpFile($autoloadFile, $autoloadContents); + } + + public function getVendorDir(): string + { + $vendorDirProcess = $this->processFactory->getVendorDirProcess(); + + $this->logger->info($vendorDirProcess->getCommandLine()); + + $vendorDirProcess->run(); + + if (false === $vendorDirProcess->isSuccessful()) { + throw new RuntimeException( + 'Could not retrieve the vendor dir.', + 0, + new ProcessFailedException($vendorDirProcess), + ); + } + + return trim($vendorDirProcess->getOutput()); + } + + private function dumpAutoloader(bool $noDev): void + { + $dumpAutoloadProcess = $this->processFactory->getDumpAutoloaderProcess($noDev); + + $this->logger->info($dumpAutoloadProcess->getCommandLine()); + + $dumpAutoloadProcess->run(); + + if (false === $dumpAutoloadProcess->isSuccessful()) { + throw new RuntimeException( + 'Could not dump the autoloader.', + 0, + new ProcessFailedException($dumpAutoloadProcess), + ); + } + + $output = $dumpAutoloadProcess->getOutput(); + $errorOutput = $dumpAutoloadProcess->getErrorOutput(); + + if ('' !== $output) { + $this->logger->info( + 'STDOUT output:'.PHP_EOL.$output, + ['stdout' => $output], + ); + } + + if ('' !== $errorOutput) { + $this->logger->info( + 'STDERR output:'.PHP_EOL.$errorOutput, + ['stderr' => $errorOutput], + ); + } + } +} diff --git a/vendor-hotfix/NullScoper.php b/vendor-hotfix/NullScoper.php new file mode 100644 index 00000000..affe05f6 --- /dev/null +++ b/vendor-hotfix/NullScoper.php @@ -0,0 +1,53 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Symbol\SymbolsRegistry; + +/** + * @private + */ +final class NullScoper implements Scoper +{ + public function __construct( + private SymbolsRegistry $symbolsRegistry = new SymbolsRegistry(), + ) { + } + + public function scope(string $filePath, string $contents): string + { + return $contents; + } + + public function changeSymbolsRegistry(SymbolsRegistry $symbolsRegistry): void + { + $this->symbolsRegistry = $symbolsRegistry; + } + + public function getSymbolsRegistry(): SymbolsRegistry + { + return $this->symbolsRegistry; + } + + public function getPrefix(): string + { + return ''; + } + + public function getExcludedFilePaths(): array + { + return []; + } +} diff --git a/vendor-hotfix/Scoper.php b/vendor-hotfix/Scoper.php new file mode 100644 index 00000000..2c0662ec --- /dev/null +++ b/vendor-hotfix/Scoper.php @@ -0,0 +1,38 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Symbol\SymbolsRegistry; + +interface Scoper +{ + /** + * Scope AKA. apply the given prefix to the file in the appropriate way. + * + * @param string $filePath File to scope + * @param string $contents Contents of the file to scope + * + * @return string Contents of the file with the prefix applied + */ + public function scope(string $filePath, string $contents): string; + + public function changeSymbolsRegistry(SymbolsRegistry $symbolsRegistry): void; + + public function getSymbolsRegistry(): SymbolsRegistry; + + public function getPrefix(): string; + + public function getExcludedFilePaths(): array; +} diff --git a/vendor-hotfix/SerializableScoper.php b/vendor-hotfix/SerializableScoper.php new file mode 100644 index 00000000..152c00e8 --- /dev/null +++ b/vendor-hotfix/SerializableScoper.php @@ -0,0 +1,122 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Configuration\Configuration as PhpScoperConfiguration; +use Humbug\PhpScoper\Container as PhpScoperContainer; +use Humbug\PhpScoper\Scoper\Scoper as PhpScoperScoper; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use function count; + +/** + * @private + */ +final class SerializableScoper implements Scoper +{ + private PhpScoperConfiguration $scoperConfig; + private PhpScoperContainer $scoperContainer; + private PhpScoperScoper $scoper; + private SymbolsRegistry $symbolsRegistry; + + /** + * @var list + */ + public array $excludedFilePaths; + + public function __construct( + PhpScoperConfiguration $scoperConfig, + string ...$excludedFilePaths, + ) { + $this->scoperConfig = $scoperConfig->withPatcher( + PatcherFactory::createSerializablePatchers($scoperConfig->getPatcher()) + ); + $this->excludedFilePaths = $excludedFilePaths; + $this->symbolsRegistry = new SymbolsRegistry(); + } + + public function scope(string $filePath, string $contents): string + { + return $this->getScoper()->scope( + $filePath, + $contents, + ); + } + + public function changeSymbolsRegistry(SymbolsRegistry $symbolsRegistry): void + { + $this->symbolsRegistry = $symbolsRegistry; + + unset($this->scoper); + } + + public function getSymbolsRegistry(): SymbolsRegistry + { + return $this->symbolsRegistry; + } + + public function getPrefix(): string + { + return $this->scoperConfig->getPrefix(); + } + + private function getScoper(): PhpScoperScoper + { + if (isset($this->scoper)) { + return $this->scoper; + } + + if (!isset($this->scoperContainer)) { + $this->scoperContainer = new PhpScoperContainer(); + } + + $this->scoper = $this->createScoper(); + + return $this->scoper; + } + + public function __wakeup(): void + { + // We need to make sure that a fresh Scoper & PHP-Parser Parser/Lexer + // is used within a sub-process. + // Otherwise, there is a risk of data corruption or that a compatibility + // layer of some sorts (such as the tokens for PHP-Paser) is not + // triggered in the sub-process resulting in obscure errors + unset($this->scoper, $this->scoperContainer); + } + + private function createScoper(): PhpScoperScoper + { + $scoper = $this->scoperContainer + ->getScoperFactory() + ->createScoper( + $this->scoperConfig, + $this->symbolsRegistry, + ); + + if (0 === count($this->excludedFilePaths)) { + return $scoper; + } + + return new ExcludedFilesScoper( + $scoper, + ...$this->excludedFilePaths, + ); + } + + public function getExcludedFilePaths(): array + { + return $this->excludedFilePaths; + } +}