diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f0ba92ae..e4af09c10 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: coverage: "none" extensions: "intl" ini-values: "memory_limit=-1" - php-version: "7.4" + php-version: "8.0" - name: "Install dependencies from composer.lock using composer binary provided by system" run: "composer install ${{ env.COMPOSER_FLAGS }}" diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index afa1b51b7..c090a34d8 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -268,7 +268,7 @@ class Problem }); if (!$nonLockedPackages) { - return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' in lock file but not in remote repositories, make sure you avoid updating this package to keep the one from lock file.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' in the lock file but not in remote repositories, make sure you avoid updating this package to keep the one from the lock file.'); } return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but these were not loaded, likely because '.(self::hasMultipleNames($packages) ? 'they conflict' : 'it conflicts').' with another require.'); @@ -404,6 +404,13 @@ class Problem } } + if ($nextRepo instanceof LockArrayRepository) { + $singular = count($higherRepoPackages) === 1; + + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', + 'found '.self::getPackageList($nextRepoPackages, $isVerbose).' in the lock file and '.self::getPackageList($higherRepoPackages, $isVerbose).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' but ' . ($singular ? 'it does' : 'these do') . ' not match your '.$reason.' and ' . ($singular ? 'is' : 'are') . ' therefore not installable. Make sure you either fix the '.$reason.' or avoid updating this package to keep the one from the lock file.'); + } + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages, $isVerbose).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages, $isVerbose).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your '.$reason.' and are therefore not installable. See https://getcomposer.org/repoprio for details and assistance.'); } diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index ab711ac66..cc667e692 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -107,6 +107,34 @@ abstract class ArchiveDownloader extends FileDownloader return iterator_to_array($finder); }; + $renameRecursively = null; + /** + * Renames (and recursively merges if needed) a folder into another one + * + * For custom installers, where packages may share paths, and given Composer 2's parallelism, we need to make sure + * that the source directory gets merged into the target one if the target exists. Otherwise rename() by default would + * put the source into the target e.g. src/ => target/src/ (assuming target exists) instead of src/ => target/ + * + * @param string $from Directory + * @param string $to Directory + * @return void + */ + $renameRecursively = function ($from, $to) use ($filesystem, $getFolderContent, $package, &$renameRecursively) { + $contentDir = $getFolderContent($from); + + // move files back out of the temp dir + foreach ($contentDir as $file) { + $file = (string) $file; + if (is_dir($to . '/' . basename($file))) { + if (!is_dir($file)) { + throw new \RuntimeException('Installing '.$package.' would lead to overwriting the '.$to.'/'.basename($file).' directory with a file from the package, invalid operation.'); + } + $renameRecursively($file, $to . '/' . basename($file)); + } else { + $filesystem->rename($file, $to . '/' . basename($file)); + } + } + }; $renameAsOne = false; if (!file_exists($path) || ($filesystem->isDirEmpty($path) && $filesystem->removeDirectory($path))) { @@ -126,15 +154,12 @@ abstract class ArchiveDownloader extends FileDownloader $filesystem->rename($extractedDir, $path); } else { // only one dir in the archive, extract its contents out of it + $from = $temporaryDir; if ($singleDirAtTopLevel) { - $contentDir = $getFolderContent((string) reset($contentDir)); + $from = (string) reset($contentDir); } - // move files back out of the temp dir - foreach ($contentDir as $file) { - $file = (string) $file; - $filesystem->rename($file, $path . '/' . basename($file)); - } + $renameRecursively($from, $path); } $promise = $filesystem->removeDirectoryAsync($temporaryDir); diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 7ee10bbb7..236390066 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -27,6 +27,7 @@ use Composer\Plugin\PostFileDownloadEvent; use Composer\Plugin\PreFileDownloadEvent; use Composer\EventDispatcher\EventDispatcher; use Composer\Util\Filesystem; +use Composer\Util\Silencer; use Composer\Util\HttpDownloader; use Composer\Util\Url as UrlUtil; use Composer\Util\ProcessExecutor; @@ -313,6 +314,16 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $this->filesystem->emptyDirectory($path); $this->filesystem->ensureDirectoryExists($path); $this->filesystem->rename($this->getFileName($package, $path), $path . '/' . pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME)); + + if ($package->getBinaries()) { + // Single files can not have a mode set like files in archives + // so we make sure if the file is a binary that it is executable + foreach ($package->getBinaries() as $bin) { + if (file_exists($path . '/' . $bin) && !is_executable($path . '/' . $bin)) { + Silencer::call('chmod', $path . '/' . $bin, 0777 & ~umask()); + } + } + } } /** diff --git a/src/Composer/Installer/BinaryInstaller.php b/src/Composer/Installer/BinaryInstaller.php index ef935c308..5a6e38805 100644 --- a/src/Composer/Installer/BinaryInstaller.php +++ b/src/Composer/Installer/BinaryInstaller.php @@ -192,15 +192,19 @@ class BinaryInstaller $binFile = basename($binPath); $binContents = file_get_contents($bin); + // For php files, we generate a PHP proxy instead of a shell one, + // which allows calling the proxy with a custom php process if (preg_match('{^(?:#!(?:/usr)?/bin/env php|#!(?:/usr)?/bin/php|loader->loadPackages($packages, 'Composer\Package\CompletePackage'); + $packageInstances = $this->loader->loadPackages($packages, 'Composer\Package\CompletePackage'); - foreach ($packages as $package) { + foreach ($packageInstances as $package) { if (isset($this->sourceMirrors[$package->getSourceType()])) { $package->setSourceMirrors($this->sourceMirrors[$package->getSourceType()]); } @@ -1063,7 +1063,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->configurePackageTransportOptions($package); } - return $packages; + return $packageInstances; } catch (\Exception $e) { throw new \RuntimeException('Could not load packages '.(isset($packages[0]['name']) ? $packages[0]['name'] : json_encode($packages)).' in '.$this->getRepoName().($source ? ' from '.$source : '').': ['.get_class($e).'] '.$e->getMessage(), 0, $e); } diff --git a/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test b/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test index 8f21a285a..3e37004d5 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test +++ b/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test @@ -43,7 +43,7 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - locked/pkg dev-master requires locked/dependency 1.0.0 -> found locked/dependency[1.0.0] in lock file but not in remote repositories, make sure you avoid updating this package to keep the one from lock file. + - locked/pkg dev-master requires locked/dependency 1.0.0 -> found locked/dependency[1.0.0] in the lock file but not in remote repositories, make sure you avoid updating this package to keep the one from the lock file. - locked/pkg is locked to version dev-master and an update of this package was not requested. Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions. diff --git a/tests/Composer/Test/Fixtures/installer/conflict-with-all-dependencies-option-dont-recommend-to-use-it.test b/tests/Composer/Test/Fixtures/installer/conflict-with-all-dependencies-option-dont-recommend-to-use-it.test index d1003fd9d..1f5120880 100644 --- a/tests/Composer/Test/Fixtures/installer/conflict-with-all-dependencies-option-dont-recommend-to-use-it.test +++ b/tests/Composer/Test/Fixtures/installer/conflict-with-all-dependencies-option-dont-recommend-to-use-it.test @@ -43,7 +43,7 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - locked/pkg dev-master requires locked/dependency 1.0.0 -> found locked/dependency[1.0.0] in lock file but not in remote repositories, make sure you avoid updating this package to keep the one from lock file. + - locked/pkg dev-master requires locked/dependency 1.0.0 -> found locked/dependency[1.0.0] in the lock file but not in remote repositories, make sure you avoid updating this package to keep the one from the lock file. - locked/pkg is locked to version dev-master and an update of this package was not requested. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-at-all-in-remote.test b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-at-all-in-remote.test new file mode 100644 index 000000000..c98d34dc6 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-at-all-in-remote.test @@ -0,0 +1,52 @@ +--TEST-- +Update package which is in lock file but not in remote repo at all should show this error correctly +--COMPOSER-- +{ + "minimum-stability": "dev", + "repositories": [ + {"type": "package", "package": [ + {"name": "main/dep", "version": "1.0.0", "require": {"locked/dep": "^2.1"}} + ]} + ], + "require": { + "main/dep": "*" + } +} +--LOCK-- +{ + "packages": [ + { + "name": "main/dep", "version": "1.0.0", + "require": {"locked/dep": "^2.1"}, + "type": "library" + }, + { + "name": "locked/dep", "version": "2.1.0", + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update main/dep --with-all-dependencies + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires main/dep * -> satisfiable by main/dep[1.0.0]. + - main/dep 1.0.0 requires locked/dep ^2.1 -> found locked/dep[2.1.0] in the lock file but not in remote repositories, make sure you avoid updating this package to keep the one from the lock file. + +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-in-remote-due-to-min-stability.test b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-in-remote-due-to-min-stability.test new file mode 100644 index 000000000..0c66d1dc3 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-in-remote-due-to-min-stability.test @@ -0,0 +1,54 @@ +--TEST-- +Update package which is in lock file but not remote repo due to min-stability should show this error correctly +--COMPOSER-- +{ + "minimum-stability": "stable", + "repositories": [ + {"type": "package", "package": [ + {"name": "main/dep", "version": "1.0.0", "require": {"locked/dep": "^2.1"}}, + {"name": "locked/dep", "version": "2.x-dev"}, + {"name": "locked/dep", "version": "2.0.5"} + ]} + ], + "require": { + "main/dep": "*" + } +} +--LOCK-- +{ + "packages": [ + { + "name": "main/dep", "version": "1.0.0", + "require": {"locked/dep": "^2.1"}, + "type": "library" + }, + { + "name": "locked/dep", "version": "2.1.0", + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update main/dep --with-all-dependencies + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires main/dep * -> satisfiable by main/dep[1.0.0]. + - main/dep 1.0.0 requires locked/dep ^2.1 -> found locked/dep[2.1.0] in the lock file and locked/dep[2.x-dev] from package repo (defining 3 packages) but it does not match your minimum-stability and is therefore not installable. Make sure you either fix the minimum-stability or avoid updating this package to keep the one from the lock file. + +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-in-remote.test b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-in-remote.test new file mode 100644 index 000000000..0d1e680e7 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-in-remote.test @@ -0,0 +1,53 @@ +--TEST-- +Update package which is in lock file but not in remote repo in the correct version should show this error correctly +--COMPOSER-- +{ + "minimum-stability": "dev", + "repositories": [ + {"type": "package", "package": [ + {"name": "main/dep", "version": "1.0.0", "require": {"locked/dep": "^2.1"}}, + {"name": "locked/dep", "version": "2.0.5"} + ]} + ], + "require": { + "main/dep": "*" + } +} +--LOCK-- +{ + "packages": [ + { + "name": "main/dep", "version": "1.0.0", + "require": {"locked/dep": "^2.1"}, + "type": "library" + }, + { + "name": "locked/dep", "version": "2.1.0", + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update main/dep --with-all-dependencies + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires main/dep * -> satisfiable by main/dep[1.0.0]. + - main/dep 1.0.0 requires locked/dep ^2.1 -> found locked/dep[2.1.0] in the lock file and locked/dep[2.0.5] from package repo (defining 2 packages) but it does not match your constraint and is therefore not installable. Make sure you either fix the constraint or avoid updating this package to keep the one from the lock file. + +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-package-present-in-lower-repo-prio-but-not-main-due-to-min-stability.test b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lower-repo-prio-but-not-main-due-to-min-stability.test new file mode 100644 index 000000000..2ccd48e80 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lower-repo-prio-but-not-main-due-to-min-stability.test @@ -0,0 +1,57 @@ +--TEST-- +Update package which is in lower prio repo but not main repo due to min-stability should show this error correctly +--COMPOSER-- +{ + "minimum-stability": "stable", + "repositories": [ + {"type": "package", "package": [ + {"name": "main/dep", "version": "1.0.0", "require": {"lower/dep": "^2.1"}}, + {"name": "lower/dep", "version": "2.x-dev"}, + {"name": "lower/dep", "version": "2.0.5"} + ]}, + {"type": "package", "package": [ + {"name": "lower/dep", "version": "2.1.0"} + ]} + ], + "require": { + "main/dep": "*" + } +} +--LOCK-- +{ + "packages": [ + { + "name": "main/dep", "version": "1.0.0", + "require": {"lower/dep": "^2.1"}, + "type": "library" + }, + { + "name": "lower/dep", "version": "2.1.0", + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update main/dep --with-all-dependencies + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires main/dep * -> satisfiable by main/dep[1.0.0]. + - main/dep 1.0.0 requires lower/dep ^2.1 -> satisfiable by lower/dep[2.1.0] from package repo (defining 1 package) but lower/dep[2.x-dev] from package repo (defining 3 packages) has higher repository priority. The packages with higher priority do not match your minimum-stability and are therefore not installable. See https://getcomposer.org/repoprio for details and assistance. + +--EXPECT--