diff --git a/.github/workflows/close-stale-support.yml b/.github/workflows/close-stale-support.yml index 76348915b..6d92d8b59 100644 --- a/.github/workflows/close-stale-support.yml +++ b/.github/workflows/close-stale-support.yml @@ -13,7 +13,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v3 + - uses: actions/stale@v5 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 180 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93d6e7c56..c45cb57b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,14 +81,14 @@ jobs: # This step requires a secret token with `pull` access to composer/docker. The default # secrets.GITHUB_TOKEN is scoped to this repository only which is not sufficient. - name: "Open issue @ Docker repository" - uses: actions/github-script@v2 + uses: actions/github-script@v6 with: github-token: ${{ secrets.PUBLIC_REPO_ACCESS_TOKEN }} script: | // github.ref value looks like 'refs/tags/TAG', cleanup const tag = "${{ github.ref }}".replace(/refs\/tags\//, ''); // create new issue on Docker repository - github.issues.create({ + github.rest.issues.create({ owner: "${{ github.repository_owner }}", repo: "docker", title: `New Composer tag: ${ tag }`, diff --git a/CHANGELOG.md b/CHANGELOG.md index baf972ae2..46ad54018 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### [2.2.8] 2022-03-15 + + * Fixed `files` autoloading sort order to be fully deterministic (#10617) + * Fixed pool optimization pass edge cases (#10579) + * Fixed `require` command failing when `self.version` is used as constraint (#10593) + * Fixed --no-ansi / undecorated output still showing color in repo warnings (#10601) + * Performance improvement in pool optimization step (composer/semver#131) + ### [2.2.7] 2022-02-25 * Allow installation together with composer/xdebug-handler ^3 (#10528) @@ -1394,6 +1402,7 @@ * Initial release +[2.2.8]: https://github.com/composer/composer/compare/2.2.7...2.2.8 [2.2.7]: https://github.com/composer/composer/compare/2.2.6...2.2.7 [2.2.6]: https://github.com/composer/composer/compare/2.2.5...2.2.6 [2.2.5]: https://github.com/composer/composer/compare/2.2.4...2.2.5 diff --git a/composer.lock b/composer.lock index 46984de0e..ac2f20f67 100644 --- a/composer.lock +++ b/composer.lock @@ -224,16 +224,16 @@ }, { "name": "composer/semver", - "version": "3.2.9", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "a951f614bd64dcd26137bc9b7b2637ddcfc57649" + "reference": "f79c90ad4e9b41ac4dfc5d77bf398cf61fbd718b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/a951f614bd64dcd26137bc9b7b2637ddcfc57649", - "reference": "a951f614bd64dcd26137bc9b7b2637ddcfc57649", + "url": "https://api.github.com/repos/composer/semver/zipball/f79c90ad4e9b41ac4dfc5d77bf398cf61fbd718b", + "reference": "f79c90ad4e9b41ac4dfc5d77bf398cf61fbd718b", "shasum": "" }, "require": { @@ -285,7 +285,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.2.9" + "source": "https://github.com/composer/semver/tree/3.3.0" }, "funding": [ { @@ -301,7 +301,7 @@ "type": "tidelift" } ], - "time": "2022-02-04T13:58:43+00:00" + "time": "2022-03-15T08:35:57+00:00" }, { "name": "composer/spdx-licenses", diff --git a/doc/articles/plugins.md b/doc/articles/plugins.md index fa783679b..fff0cdb22 100644 --- a/doc/articles/plugins.md +++ b/doc/articles/plugins.md @@ -323,6 +323,13 @@ hint to Composer that the plugin should be installed on its own before proceedin the rest of the package downloads. This slightly slows down the overall installation process however, so do not use it in plugins which do not absolutely require it. +## Plugin Autoloading + +Due to plugins being loaded by Composer at runtime, and to ensure that plugins which +depend on other packages can function correctly, a runtime autoloader is created whenever +a plugin is loaded. That autoloader is only configured to load with the plugin dependencies, +so you may not have access to all the packages which are installed. + [1]: ../04-schema.md#type [2]: ../04-schema.md#extra [3]: https://github.com/composer/composer/blob/main/src/Composer/Plugin/PluginInterface.php diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index ffb0cd83c..459653bfb 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -1334,7 +1334,7 @@ INITIALIZER; /** * Sorts packages by dependency weight * - * Packages of equal weight retain the original order + * Packages of equal weight are sorted alphabetically * * @param array $packageMap * @return array diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 3f4fa2965..373d0451a 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -225,6 +225,9 @@ EOT return 1; } + if ($constraint === 'self.version') { + continue; + } $versionParser->parseConstraints($constraint); } diff --git a/src/Composer/DependencyResolver/PoolOptimizer.php b/src/Composer/DependencyResolver/PoolOptimizer.php index e301f6ee7..d2f37c89c 100644 --- a/src/Composer/DependencyResolver/PoolOptimizer.php +++ b/src/Composer/DependencyResolver/PoolOptimizer.php @@ -110,21 +110,18 @@ class PoolOptimizer // Extract requested package requirements foreach ($request->getRequires() as $require => $constraint) { - $constraint = Intervals::compactConstraint($constraint); - $this->requireConstraintsPerPackage[$require][(string) $constraint] = $constraint; + $this->extractRequireConstraintsPerPackage($require, $constraint); } // First pass over all packages to extract information and mark package constraints irremovable foreach ($pool->getPackages() as $package) { // Extract package requirements foreach ($package->getRequires() as $link) { - $constraint = Intervals::compactConstraint($link->getConstraint()); - $this->requireConstraintsPerPackage[$link->getTarget()][(string) $constraint] = $constraint; + $this->extractRequireConstraintsPerPackage($link->getTarget(), $link->getConstraint()); } // Extract package conflicts foreach ($package->getConflicts() as $link) { - $constraint = Intervals::compactConstraint($link->getConstraint()); - $this->conflictConstraintsPerPackage[$link->getTarget()][(string) $constraint] = $constraint; + $this->extractConflictConstraintsPerPackage($link->getTarget(), $link->getConstraint()); } // Keep track of alias packages for every package so if either the alias or aliased is kept @@ -452,4 +449,55 @@ class PoolOptimizer } } } + + /** + * Disjunctive require constraints need to be considered in their own group. E.g. "^2.14 || ^3.3" needs to generate + * two require constraint groups in order for us to keep the best matching package for "^2.14" AND "^3.3" as otherwise, we'd + * only keep either one which can cause trouble (e.g. when using --prefer-lowest). + * + * @param string $package + * @param ConstraintInterface $constraint + * @return void + */ + private function extractRequireConstraintsPerPackage($package, ConstraintInterface $constraint) + { + foreach ($this->expandDisjunctiveMultiConstraints($constraint) as $expanded) { + $this->requireConstraintsPerPackage[$package][(string) $expanded] = $expanded; + } + } + + /** + * Disjunctive conflict constraints need to be considered in their own group. E.g. "^2.14 || ^3.3" needs to generate + * two conflict constraint groups in order for us to keep the best matching package for "^2.14" AND "^3.3" as otherwise, we'd + * only keep either one which can cause trouble (e.g. when using --prefer-lowest). + * + * @param string $package + * @param ConstraintInterface $constraint + * @return void + */ + private function extractConflictConstraintsPerPackage($package, ConstraintInterface $constraint) + { + foreach ($this->expandDisjunctiveMultiConstraints($constraint) as $expanded) { + $this->conflictConstraintsPerPackage[$package][(string) $expanded] = $expanded; + } + } + + /** + * + * @param ConstraintInterface $constraint + * @return ConstraintInterface[] + */ + private function expandDisjunctiveMultiConstraints(ConstraintInterface $constraint) + { + $constraint = Intervals::compactConstraint($constraint); + + if ($constraint instanceof MultiConstraint && $constraint->isDisjunctive()) { + // No need to call ourselves recursively here because Intervals::compactConstraint() ensures that there + // are no nested disjunctive MultiConstraint instances possible + return $constraint->getConstraints(); + } + + // Regular constraints and conjunctive MultiConstraints + return array($constraint); + } } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 5153a81dc..cd0ffb014 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -473,6 +473,11 @@ class Installer return $exitCode; } + // exists as of composer/semver 3.3.0 + if (method_exists('Composer\Semver\CompilingMatcher', 'clear')) { // @phpstan-ignore-line + \Composer\Semver\CompilingMatcher::clear(); + } + // write lock $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index b70e0682f..5cc57b182 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -428,6 +428,14 @@ class HttpDownloader */ public static function outputWarnings(IOInterface $io, string $url, $data): void { + $cleanMessage = function ($msg) use ($io) { + if (!$io->isDecorated()) { + $msg = Preg::replace('{'.chr(27).'\\[[;\d]*m}u', '', $msg); + } + + return $msg; + }; + // legacy warning/info keys foreach (array('warning', 'info') as $type) { if (empty($data[$type])) { @@ -443,7 +451,7 @@ class HttpDownloader } } - $io->writeError('<'.$type.'>'.ucfirst($type).' from '.Url::sanitize($url).': '.$data[$type].''); + $io->writeError('<'.$type.'>'.ucfirst($type).' from '.Url::sanitize($url).': '.$cleanMessage($data[$type]).''); } // modern Composer 2.2+ format with support for multiple warning/info messages @@ -461,7 +469,7 @@ class HttpDownloader continue; } - $io->writeError('<'.$type.'>'.ucfirst($type).' from '.Url::sanitize($url).': '.$spec['message'].''); + $io->writeError('<'.$type.'>'.ucfirst($type).' from '.Url::sanitize($url).': '.$cleanMessage($spec['message']).''); } } } diff --git a/src/Composer/Util/PackageSorter.php b/src/Composer/Util/PackageSorter.php index 83ae4da37..b5e9780ae 100644 --- a/src/Composer/Util/PackageSorter.php +++ b/src/Composer/Util/PackageSorter.php @@ -13,13 +13,14 @@ namespace Composer\Util; use Composer\Package\PackageInterface; +use Composer\Package\RootPackageInterface; class PackageSorter { /** * Sorts packages by dependency weight * - * Packages of equal weight retain the original order + * Packages of equal weight are sorted alphabetically * * @param PackageInterface[] $packages * @return PackageInterface[] sorted array @@ -29,7 +30,11 @@ class PackageSorter $usageList = array(); foreach ($packages as $package) { - foreach (array_merge($package->getRequires(), $package->getDevRequires()) as $link) { + $links = $package->getRequires(); + if ($package instanceof RootPackageInterface) { + $links = array_merge($links, $package->getDevRequires()); + } + foreach ($links as $link) { $target = $link->getTarget(); $usageList[$target][] = $package->getName(); } @@ -62,39 +67,26 @@ class PackageSorter return $weight; }; - $weightList = array(); + $weightedPackages = array(); foreach ($packages as $index => $package) { - $weight = $computeImportance($package->getName()); - $weightList[$index] = $weight; + $name = $package->getName(); + $weight = $computeImportance($name); + $weightedPackages[] = array('name' => $name, 'weight' => $weight, 'index' => $index); } - $stable_sort = function (&$array): void { - static $transform, $restore; - - $i = 0; - - if (!$transform) { - $transform = function (&$v, $k) use (&$i): void { - $v = array($v, ++$i, $k, $v); - }; - - $restore = function (&$v): void { - $v = $v[3]; - }; + usort($weightedPackages, function (array $a, array $b): int { + if ($a['weight'] !== $b['weight']) { + return $a['weight'] - $b['weight']; } - array_walk($array, $transform); - asort($array); - array_walk($array, $restore); - }; - - $stable_sort($weightList); + return strnatcasecmp($a['name'], $b['name']); + }); $sortedPackages = array(); - foreach (array_keys($weightList) as $index) { - $sortedPackages[] = $packages[$index]; + foreach ($weightedPackages as $pkg) { + $sortedPackages[] = $packages[$pkg['index']]; } return $sortedPackages; diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index 27c444bec..848dc9198 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -1010,6 +1010,14 @@ EOF; $packages[] = $c = new Package('c/lorem', '1.0', '1.0'); $packages[] = $e = new Package('e/e', '1.0', '1.0'); + // expected order: + // c requires nothing + // d requires c + // b requires c & d + // e requires c + // z requires c + // (b, e, z ordered alphabetically) + $z->setAutoload(array('files' => array('testA.php'))); $z->setRequires(array('c/lorem' => new Link('z/foo', 'c/lorem', new MatchAllConstraint()))); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_static_files_by_dependency.php b/tests/Composer/Test/Autoload/Fixtures/autoload_static_files_by_dependency.php index e3659aee3..addd6cc3f 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_static_files_by_dependency.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_static_files_by_dependency.php @@ -9,9 +9,9 @@ class ComposerStaticInitFilesAutoloadOrder public static $files = array ( 'bfdd693009729d60c830ff8d79129635' => __DIR__ . '/..' . '/c/lorem/testC.php', '61e6098c8cafe404d6cf19e59fc2b788' => __DIR__ . '/..' . '/d/d/testD.php', - '8bceec6fdc149a1a6acbf7ad3cfed51c' => __DIR__ . '/..' . '/z/foo/testA.php', 'c5466e580c6c2403f225c43b6a21a96f' => __DIR__ . '/..' . '/b/bar/testB.php', '69dfc37c40a853a7cbac6c9d2367c5f4' => __DIR__ . '/..' . '/e/e/testE.php', + '8bceec6fdc149a1a6acbf7ad3cfed51c' => __DIR__ . '/..' . '/z/foo/testA.php', 'ab280164f4754f5dfdb0721de84d737f' => __DIR__ . '/../..' . '/root2.php', ); diff --git a/tests/Composer/Test/DependencyResolver/Fixtures/pooloptimizer/basic-prefer-highest.test b/tests/Composer/Test/DependencyResolver/Fixtures/pooloptimizer/basic-prefer-highest.test index 91131b790..904d6c375 100644 --- a/tests/Composer/Test/DependencyResolver/Fixtures/pooloptimizer/basic-prefer-highest.test +++ b/tests/Composer/Test/DependencyResolver/Fixtures/pooloptimizer/basic-prefer-highest.test @@ -15,7 +15,7 @@ Test filters irrelevant package "package/b" in version 1.0.0 "name": "package/a", "version": "1.0.0", "require": { - "package/b": "^1.0" + "package/b": ">=1.0 <1.1 || ^1.2" } }, { @@ -25,6 +25,10 @@ Test filters irrelevant package "package/b" in version 1.0.0 { "name": "package/b", "version": "1.0.1" + }, + { + "name": "package/b", + "version": "1.2.0" } ] @@ -41,6 +45,10 @@ Test filters irrelevant package "package/b" in version 1.0.0 { "name": "package/b", "version": "1.0.1" + }, + { + "name": "package/b", + "version": "1.2.0" } ] diff --git a/tests/Composer/Test/DependencyResolver/Fixtures/pooloptimizer/complex-prefer-lowest.test b/tests/Composer/Test/DependencyResolver/Fixtures/pooloptimizer/complex-prefer-lowest.test new file mode 100644 index 000000000..74240e145 --- /dev/null +++ b/tests/Composer/Test/DependencyResolver/Fixtures/pooloptimizer/complex-prefer-lowest.test @@ -0,0 +1,55 @@ +--TEST-- +Test keeps package "package/b" in version 2.2.0 because for prefer-lowest either one might be relevant + +--REQUEST-- +{ + "require": { + "package/a": "^1.0" + }, + "preferLowest": true +} + + +--POOL-BEFORE-- +[ + { + "name": "package/a", + "version": "1.0.0", + "require": { + "package/b": "^1.0 || ^2.2" + } + }, + { + "name": "package/b", + "version": "1.0.0" + }, + { + "name": "package/b", + "version": "1.0.1" + }, + { + "name": "package/b", + "version": "2.2.0" + } +] + + +--POOL-AFTER-- +[ + { + "name": "package/a", + "version": "1.0.0", + "require": { + "package/b": "^1.0" + } + }, + { + "name": "package/b", + "version": "1.0.0" + }, + { + "name": "package/b", + "version": "2.2.0" + } +] + diff --git a/tests/Composer/Test/Fixtures/functional/installed-versions.test b/tests/Composer/Test/Fixtures/functional/installed-versions.test index fe9ef1e11..57e34bf83 100644 --- a/tests/Composer/Test/Fixtures/functional/installed-versions.test +++ b/tests/Composer/Test/Fixtures/functional/installed-versions.test @@ -11,7 +11,7 @@ update !!PreUpdate:["composer/ca-bundle","composer/composer","composer/metadata-minifier","composer/pcre","composer/semver","composer/spdx-licenses","composer/xdebug-handler","justinrainbow/json-schema","psr/container","psr/log","psr/log-implementation","react/promise","seld/jsonlint","seld/phar-utils","symfony/console","symfony/deprecation-contracts","symfony/filesystem","symfony/finder","symfony/polyfill-ctype","symfony/polyfill-intl-grapheme","symfony/polyfill-intl-normalizer","symfony/polyfill-mbstring","symfony/polyfill-php73","symfony/polyfill-php80","symfony/process","symfony/service-contracts","symfony/string"] !!Versions:console:%[2-8]\.\d+\.\d+.0%;process:%[2-8]\.\d+\.\d+.0%;filesystem:%[2-8]\.\d+\.\d+.0% Loading composer repositories with package information -Updating dependencies +%((Info|Warning) from .*\n)?%Updating dependencies Lock file operations: 6 installs, 0 updates, 0 removals - Locking plugin/a (1.1.1) - Locking plugin/b (2.2.2) diff --git a/tests/Composer/Test/Fixtures/functional/installed-versions2.test b/tests/Composer/Test/Fixtures/functional/installed-versions2.test index a2d5f3362..ce6aa9d7b 100644 --- a/tests/Composer/Test/Fixtures/functional/installed-versions2.test +++ b/tests/Composer/Test/Fixtures/functional/installed-versions2.test @@ -17,7 +17,7 @@ update plugin/* symfony/console symfony/filesystem symfony/process !!PreUpdate:["composer/ca-bundle","composer/composer","composer/metadata-minifier","composer/pcre","composer/semver","composer/spdx-licenses","composer/xdebug-handler","justinrainbow/json-schema","psr/container","psr/log","psr/log-implementation","react/promise","seld/jsonlint","seld/phar-utils","symfony/console","symfony/deprecation-contracts","symfony/filesystem","symfony/finder","symfony/polyfill-ctype","symfony/polyfill-intl-grapheme","symfony/polyfill-intl-normalizer","symfony/polyfill-mbstring","symfony/polyfill-php73","symfony/polyfill-php80","symfony/process","symfony/service-contracts","symfony/string","plugin/a","plugin/b","root/pkg"] !!Versions:console:%[2-8]\.\d+\.\d+.0%;process:%[2-8]\.\d+\.\d+.0%;filesystem:%[2-8]\.\d+\.\d+.0% Loading composer repositories with package information -Updating dependencies +%((Info|Warning) from .*\n)?%Updating dependencies Lock file operations: 0 installs, 5 updates, 0 removals - Upgrading plugin/a (1.1.1 => 1.1.2) - Upgrading plugin/b (2.2.2 => 2.2.3) diff --git a/tests/Composer/Test/Util/PackageSorterTest.php b/tests/Composer/Test/Util/PackageSorterTest.php index 67cdd13bb..981274316 100644 --- a/tests/Composer/Test/Util/PackageSorterTest.php +++ b/tests/Composer/Test/Util/PackageSorterTest.php @@ -99,6 +99,34 @@ class PackageSorterTest extends TestCase 'foo/bar6', ), ), + 'circular deps sorted alphabetically if weighted equally' => array( + array( + $this->createPackage('foo/bar1', array('circular/part1')), + $this->createPackage('foo/bar2', array('circular/part2')), + $this->createPackage('circular/part1', array('circular/part2')), + $this->createPackage('circular/part2', array('circular/part1')), + ), + array( + 'circular/part1', + 'circular/part2', + 'foo/bar1', + 'foo/bar2', + ), + ), + 'equal weight sorted alphabetically' => array( + array( + $this->createPackage('foo/bar10', array('foo/dep')), + $this->createPackage('foo/bar2', array('foo/dep')), + $this->createPackage('foo/baz', array('foo/dep')), + $this->createPackage('foo/dep', array()), + ), + array( + 'foo/dep', + 'foo/bar2', + 'foo/bar10', + 'foo/baz', + ), + ), ); }