1
0
Fork 0

Merge branch '2.0'

pull/8740/head
Jordi Boggiano 2020-04-07 09:39:00 +02:00
commit 87757de6bc
No known key found for this signature in database
GPG Key ID: 7BBD42C429EC80BC
356 changed files with 12289 additions and 6336 deletions

1
.gitattributes vendored
View File

@ -15,3 +15,4 @@
.travis.yml export-ignore
appveyor.yml export-ignore
phpunit.xml.dist export-ignore
/phpstan/ export-ignore

View File

@ -1,6 +1,6 @@
language: php
dist: trusty
dist: bionic
git:
depth: 5
@ -9,27 +9,40 @@ cache:
directories:
- $HOME/.composer/cache
addons:
apt:
packages:
- parallel
matrix:
include:
- php: 5.3
dist: precise
- php: 5.4
dist: trusty
- php: 5.5
dist: trusty
- php: 5.6
dist: xenial
- php: 7.0
dist: xenial
- php: 7.1
dist: xenial
- php: 7.2
dist: xenial
- php: 7.3
- php: nightly
dist: xenial
# Regular 7.4 build with locked deps
- php: 7.4
env:
- SYMFONY_PHPUNIT_VERSION=7.5
# High deps check
- php: 7.4
env:
- deps=high
- SYMFONY_PHPUNIT_VERSION=7.5
# PHPStan checks
- php: 7.4
env:
- deps=high
- PHPSTAN=1
- SYMFONY_PHPUNIT_VERSION=7.5
- php: nightly
fast_finish: true
allow_failures:
- php: nightly
@ -44,9 +57,9 @@ before_install:
install:
# flags to pass to install
- flags="--ansi --prefer-dist --no-interaction --optimize-autoloader --no-suggest --no-progress"
- flags="--ansi --prefer-dist --no-interaction --optimize-autoloader --no-progress"
# update deps to latest in case of high deps build
- if [ "$deps" == "high" ]; then composer config platform.php 7.2.4; composer update $flags; fi
- if [ "$deps" == "high" ]; then composer config platform.php 7.4.0; composer update $flags; fi
# install dependencies using system provided composer binary
- composer install $flags
# install dependencies using composer from source
@ -58,9 +71,13 @@ before_script:
- git config --global user.email travis@example.com
script:
- ./vendor/bin/simple-phpunit
# run test suite directories in parallel using GNU parallel
# - ls -d tests/Composer/Test/* | grep -v TestCase.php | parallel --gnu --keep-order 'echo "Running {} tests"; ./vendor/bin/phpunit -c tests/complete.phpunit.xml --colors=always {} || (echo -e "\e[41mFAILED\e[0m {}" && exit 1);'
- if [[ $PHPSTAN == "1" ]]; then
bin/composer require --dev phpstan/phpstan:^0.12 phpunit/phpunit:^7.5 --no-update &&
bin/composer update phpstan/* phpunit/* sebastian/* --with-dependencies &&
vendor/bin/phpstan analyse --configuration=phpstan/config.neon;
else
vendor/bin/simple-phpunit;
fi
before_deploy:
- php -d phar.readonly=0 bin/compile
@ -73,4 +90,4 @@ deploy:
on:
tags: true
repo: composer/composer
php: '7.2'
php: '7.3'

View File

@ -3,7 +3,7 @@ clone_depth: 5
environment:
# This sets the PHP version (from Chocolatey)
PHPCI_CHOCO_VERSION: 7.3.1
PHPCI_CHOCO_VERSION: 7.3.14
PHPCI_CACHE: C:\tools\phpci
PHPCI_PHP: C:\tools\phpci\php
PHPCI_COMPOSER: C:\tools\phpci\composer
@ -25,6 +25,15 @@ install:
- IF %PHP%==0 cinst composer -i -y --ia "/DEV=%PHPCI_COMPOSER%"
- php -v
- IF %PHP%==0 (composer --version) ELSE (composer self-update)
- IF %PHP%==0 cd %PHPCI_PHP%
- IF %PHP%==0 copy php.ini-production php.ini /Y
- IF %PHP%==0 echo date.timezone="UTC" >> php.ini
- IF %PHP%==0 echo extension_dir=ext >> php.ini
- IF %PHP%==0 echo extension=php_openssl.dll >> php.ini
- IF %PHP%==0 echo extension=php_mbstring.dll >> php.ini
- IF %PHP%==0 echo extension=php_fileinfo.dll >> php.ini
- IF %PHP%==0 echo extension=php_intl.dll >> php.ini
- IF %PHP%==0 echo extension=php_curl.dll >> php.ini
- cd %APPVEYOR_BUILD_FOLDER%
- composer install --prefer-dist --no-progress

View File

@ -24,7 +24,7 @@
"require": {
"php": "^5.3.2 || ^7.0",
"composer/ca-bundle": "^1.0",
"composer/semver": "^1.0",
"composer/semver": "^2.0@dev",
"composer/spdx-licenses": "^1.2",
"composer/xdebug-handler": "^1.1",
"justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",
@ -34,7 +34,8 @@
"symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0",
"symfony/filesystem": "^2.7 || ^3.0 || ^4.0 || ^5.0",
"symfony/finder": "^2.7 || ^3.0 || ^4.0 || ^5.0",
"symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0"
"symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0",
"react/promise": "^1.2 || ^2.7"
},
"conflict": {
"symfony/console": "2.8.38"
@ -55,7 +56,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "1.10-dev"
"dev-master": "2.0-dev"
}
},
"autoload": {
@ -65,8 +66,13 @@
},
"autoload-dev": {
"psr-4": {
"Composer\\Test\\": "tests/Composer/Test"
}
"Composer\\Test\\": "tests/Composer/Test",
"Composer\\PHPStanRules\\": "phpstan/Rules/src",
"Composer\\PHPStanRulesTests\\": "phpstan/Rules/tests"
},
"classmap": [
"phpstan/Rules/tests/data"
]
},
"bin": [
"bin/composer"

131
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "cc6f9640996dfad00a5b03a8be01a571",
"content-hash": "a0a9399315ac0b612d4296b8df745112",
"packages": [
{
"name": "composer/ca-bundle",
@ -60,20 +60,25 @@
"ssl",
"tls"
],
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/ca-bundle/issues",
"source": "https://github.com/composer/ca-bundle/tree/master"
},
"time": "2020-01-13T10:02:55+00:00"
},
{
"name": "composer/semver",
"version": "1.5.1",
"version": "2.0.x-dev",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de"
"reference": "4df5ff3249f01018504939d66040d8d2b783d820"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de",
"reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de",
"url": "https://api.github.com/repos/composer/semver/zipball/4df5ff3249f01018504939d66040d8d2b783d820",
"reference": "4df5ff3249f01018504939d66040d8d2b783d820",
"shasum": ""
},
"require": {
@ -85,7 +90,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
"dev-master": "2.x-dev"
}
},
"autoload": {
@ -121,7 +126,22 @@
"validation",
"versioning"
],
"time": "2020-01-13T12:06:48+00:00"
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/2.0"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2020-03-11T13:41:23+00:00"
},
{
"name": "composer/spdx-licenses",
@ -181,6 +201,11 @@
"spdx",
"validator"
],
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/spdx-licenses/issues",
"source": "https://github.com/composer/spdx-licenses/tree/1.5.3"
},
"time": "2020-02-14T07:44:31+00:00"
},
{
@ -225,6 +250,11 @@
"Xdebug",
"performance"
],
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/xdebug-handler/issues",
"source": "https://github.com/composer/xdebug-handler/tree/master"
},
"funding": [
{
"url": "https://packagist.com",
@ -353,6 +383,44 @@
},
"time": "2019-11-01T11:05:21+00:00"
},
{
"name": "react/promise",
"version": "v1.2.1",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise.git",
"reference": "eefff597e67ff66b719f8171480add3c91474a1e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise/zipball/eefff597e67ff66b719f8171480add3c91474a1e",
"reference": "eefff597e67ff66b719f8171480add3c91474a1e",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
}
},
"autoload": {
"psr-0": {
"React\\Promise": "src/"
},
"files": [
"src/React/Promise/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A lightweight implementation of CommonJS Promises/A for PHP",
"time": "2016-03-07T13:46:50+00:00"
},
{
"name": "seld/jsonlint",
"version": "1.7.2",
@ -448,6 +516,10 @@
"keywords": [
"phar"
],
"support": {
"issues": "https://github.com/Seldaek/phar-utils/issues",
"source": "https://github.com/Seldaek/phar-utils/tree/1.1.0"
},
"time": "2020-02-14T15:25:33+00:00"
},
{
@ -735,6 +807,9 @@
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/master"
},
"time": "2020-01-13T11:15:53+00:00"
},
{
@ -794,6 +869,9 @@
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/master"
},
"time": "2020-01-13T11:15:53+00:00"
},
{
@ -949,6 +1027,16 @@
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "mike.vanriel@naenius.com"
}
],
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
"source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/2.x"
},
"time": "2016-01-25T08:17:30+00:00"
},
{
@ -1012,6 +1100,10 @@
"spy",
"stub"
],
"support": {
"issues": "https://github.com/phpspec/prophecy/issues",
"source": "https://github.com/phpspec/prophecy/tree/v1.10.3"
},
"time": "2020-03-05T15:02:03+00:00"
},
{
@ -1076,6 +1168,10 @@
"compare",
"equality"
],
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"source": "https://github.com/sebastianbergmann/comparator/tree/1.2"
},
"time": "2017-01-29T09:50:25+00:00"
},
{
@ -1128,6 +1224,10 @@
"keywords": [
"diff"
],
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
"source": "https://github.com/sebastianbergmann/diff/tree/1.4"
},
"time": "2017-05-22T07:24:03+00:00"
},
{
@ -1195,6 +1295,10 @@
"export",
"exporter"
],
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"source": "https://github.com/sebastianbergmann/exporter/tree/master"
},
"time": "2016-11-19T08:54:04+00:00"
},
{
@ -1248,6 +1352,10 @@
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
"source": "https://github.com/sebastianbergmann/recursion-context/tree/master"
},
"time": "2016-11-19T07:33:16+00:00"
},
{
@ -1313,6 +1421,9 @@
],
"description": "Symfony PHPUnit Bridge",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/phpunit-bridge/tree/v3.4.38"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@ -1332,7 +1443,9 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {
"composer/semver": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@ -1342,5 +1455,5 @@
"platform-overrides": {
"php": "5.3.9"
},
"plugin-api-version": "1.1.0"
"plugin-api-version": "2.0.0"
}

View File

@ -159,7 +159,7 @@ php composer.phar update
> if the `composer.lock` has not been updated since changes were made to the
> `composer.json` that might affect dependency resolution.
If you only want to install or update one dependency, you can whitelist them:
If you only want to install, upgrade or remove one dependency, you can explicitly list it as an argument:
```sh
php composer.phar update monolog/monolog [...]

View File

@ -106,7 +106,6 @@ resolution.
* **--no-scripts:** Skips execution of scripts defined in `composer.json`.
* **--no-progress:** Removes the progress display that can mess with some
terminals or scripts which don't handle backspace characters.
* **--no-suggest:** Skips suggested packages in the output.
* **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster
autoloader. This is recommended especially for production, but can take
a bit of time to run so it is currently not done by default.
@ -156,9 +155,8 @@ php composer.phar update "vendor/*"
* **--no-scripts:** Skips execution of scripts defined in `composer.json`.
* **--no-progress:** Removes the progress display that can mess with some
terminals or scripts which don't handle backspace characters.
* **--no-suggest:** Skips suggested packages in the output.
* **--with-dependencies:** Add also dependencies of whitelisted packages to the whitelist, except those that are root requirements.
* **--with-all-dependencies:** Add also all dependencies of whitelisted packages to the whitelist, including those that are root requirements.
* **--with-dependencies:** Update also dependencies of packages in the argument list, except those which are root requirements.
* **--with-all-dependencies:** Update also dependencies of packages in the argument list, including those which are root requirements.
* **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster
autoloader. This is recommended especially for production, but can take
a bit of time to run so it is currently not done by default.
@ -198,11 +196,11 @@ If you do not specify a package, composer will prompt you to search for a packag
### Options
* **--dev:** Add packages to `require-dev`.
* **--dry-run:** Simulate the command without actually doing anything.
* **--prefer-source:** Install packages from `source` when available.
* **--prefer-dist:** Install packages from `dist` when available.
* **--no-progress:** Removes the progress display that can mess with some
terminals or scripts which don't handle backspace characters.
* **--no-suggest:** Skips suggested packages in the output.
* **--no-update:** Disables the automatic update of the dependencies.
* **--no-scripts:** Skips execution of scripts defined in `composer.json`.
* **--update-no-dev:** Run the dependency update with the `--no-dev` option.
@ -236,6 +234,7 @@ uninstalled.
### Options
* **--dev:** Remove packages from `require-dev`.
* **--dry-run:** Simulate the command without actually doing anything.
* **--no-progress:** Removes the progress display that can mess with some
terminals or scripts which don't handle backspace characters.
* **--no-update:** Disables the automatic update of the dependencies.
@ -408,16 +407,18 @@ Lists all packages suggested by currently installed set of packages. You can
optionally pass one or multiple package names in the format of `vendor/package`
to limit output to suggestions made by those packages only.
Use the `--by-package` or `--by-suggestion` flags to group the output by
Use the `--by-package` (default) or `--by-suggestion` flags to group the output by
the package offering the suggestions or the suggested packages respectively.
Use the `--verbose (-v)` flag to display the suggesting package and the suggestion reason.
This implies `--by-package --by-suggestion`, showing both lists.
If you only want a list of suggested package names, use `--list`.
### Options
* **--by-package:** Groups output by suggesting package.
* **--by-package:** Groups output by suggesting package (default).
* **--by-suggestion:** Groups output by suggested package.
* **--all:** Show suggestions from all dependencies, including transitive ones (by
default only direct dependencies' suggestions are shown).
* **--list:** Show only list of suggested package names.
* **--no-dev:** Excludes suggestions from `require-dev` packages.
## fund
@ -952,4 +953,9 @@ The env var accepts domains, IP addresses, and IP address blocks in CIDR
notation. You can restrict the filter to a particular port (e.g. `:80`). You
can also set it to `*` to ignore the proxy for all HTTP requests.
### COMPOSER_DISABLE_NETWORK
If set to `1`, disables network access (best effort). This can be used for debugging or
to run Composer on a plane or a starship with poor connectivity.
← [Libraries](02-libraries.md) | [Schema](04-schema.md) →

View File

@ -176,8 +176,8 @@ class AwsPlugin implements PluginInterface, EventSubscriberInterface
if ($protocol === 's3') {
$awsClient = new AwsClient($this->io, $this->composer->getConfig());
$s3RemoteFilesystem = new S3RemoteFilesystem($this->io, $event->getRemoteFilesystem()->getOptions(), $awsClient);
$event->setRemoteFilesystem($s3RemoteFilesystem);
$s3Downloader = new S3Downloader($this->io, $event->getHttpDownloader()->getOptions(), $awsClient);
$event->setHttpdownloader($s3Downloader);
}
}
}

View File

@ -43,8 +43,8 @@ Composer fires the following named events during its execution process:
### Installer Events
- **pre-dependencies-solving**: occurs before the dependencies are resolved.
- **post-dependencies-solving**: occurs after the dependencies have been resolved.
- **pre-operations-exec**: occurs before the install/upgrade/.. operations
are executed when installing a lock file.
### Package Events
@ -61,11 +61,13 @@ Composer fires the following named events during its execution process:
- **command**: occurs before any Composer Command is executed on the CLI. It
provides you with access to the input and output objects of the program.
- **pre-file-download**: occurs before files are downloaded and allows
you to manipulate the `RemoteFilesystem` object prior to downloading files
you to manipulate the `HttpDownloader` object prior to downloading files
based on the URL to be downloaded.
- **pre-command-run**: occurs before a command is executed and allows you to
manipulate the `InputInterface` object's options and arguments to tweak
a command's behavior.
- **pre-pool-create**: occurs before the Pool of packages is created, and lets
you filter the list of packages which is going to enter the Solver.
> **Note:** Composer makes no assumptions about the state of your dependencies
> prior to `install` or `update`. Therefore, you should not specify scripts

View File

@ -0,0 +1,46 @@
<?php declare(strict_types = 1);
namespace Composer\PHPStanRules;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
/**
* @phpstan-implements Rule<\PhpParser\Node\Expr\Variable>
*/
final class AnonymousFunctionWithThisRule implements Rule
{
/**
* @inheritDoc
*/
public function getNodeType(): string
{
return \PhpParser\Node\Expr\Variable::class;
}
/**
* @inheritDoc
*/
public function processNode(Node $node, Scope $scope): array
{
if (!\is_string($node->name) || $node->name !== 'this') {
return [];
}
if ($scope->isInClosureBind()) {
return [];
}
if (!$scope->isInClass()) {
// reported in other standard rule on level 0
return [];
}
if ($scope->isInAnonymousFunction()) {
return ['Using $this inside anonymous function is prohibited because of PHP 5.3 support.'];
}
return [];
}
}

View File

@ -0,0 +1,28 @@
<?php declare(strict_types = 1);
namespace Composer\PHPStanRulesTests;
use Composer\PHPStanRules\AnonymousFunctionWithThisRule;
use PHPStan\Testing\RuleTestCase;
/**
* @phpstan-extends RuleTestCase<AnonymousFunctionWithThisRule>
*/
final class AnonymousFunctionWithThisRuleTest extends RuleTestCase
{
/**
* @inheritDoc
*/
protected function getRule(): \PHPStan\Rules\Rule
{
return new AnonymousFunctionWithThisRule();
}
public function testWithThis(): void
{
$this->analyse([__DIR__ . '/data/method-with-this.php'], [
['Using $this inside anonymous function is prohibited because of PHP 5.3 support.', 13],
['Using $this inside anonymous function is prohibited because of PHP 5.3 support.', 17],
]);
}
}

View File

@ -0,0 +1,34 @@
<?php
class FirstClass
{
/**
* @var int
*/
private $firstProp = 9;
public function funMethod()
{
function() {
$this->firstProp;
};
call_user_func(function() {
$this->funMethod();
}, $this);
$bind = 'bind';
function() use($bind) {
};
}
}
function global_ok() {
$_SERVER['REMOTE_ADDR'];
}
function global_this() {
// not checked by our rule, it is checked with standard phpstan rule on level 0
$this['REMOTE_ADDR'];
}

5
phpstan/autoload.php Normal file
View File

@ -0,0 +1,5 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../src/bootstrap.php';

39
phpstan/config.neon Normal file
View File

@ -0,0 +1,39 @@
parameters:
autoload_files:
- autoload.php
level: 0
excludes_analyse:
- '../tests/Composer/Test/Fixtures/*'
- '../tests/Composer/Test/Autoload/Fixtures/*'
- '../tests/Composer/Test/Plugin/Fixtures/*'
ignoreErrors:
# ion cube is not installed
- '~^Function ioncube_loader_\w+ not found\.$~'
# variables from global scope
- '~^Undefined variable: \$vendorDir$~'
- '~^Undefined variable: \$baseDir$~'
# variable defined in eval
- '~^Undefined variable: \$res$~'
# erroneous detection of missing const, see https://github.com/phpstan/phpstan/issues/2960
- '~^Access to undefined constant ZipArchive::LIBZIP_VERSION.$~'
# we don't have different constructors for parent/child
- '~^Unsafe usage of new static\(\)\.$~'
# BC with older PHPUnit
- '~^Call to an undefined static method PHPUnit\\Framework\\TestCase::setExpectedException\(\)\.$~'
# hhvm should have support for $this in closures
-
count: 1
message: '~^Using \$this inside anonymous function is prohibited because of PHP 5\.3 support\.$~'
path: '../tests/Composer/Test/Repository/PlatformRepositoryTest.php'
paths:
- ../src
- ../tests
rules:
- Composer\PHPStanRules\AnonymousFunctionWithThisRule

View File

@ -113,10 +113,10 @@ class ClassMapGenerator
$classes = self::findClasses($filePath);
if (null !== $autoloadType) {
list($classes, $validClasses) = self::filterByNamespace($classes, $filePath, $namespace, $autoloadType, $basePath, $io);
$classes = self::filterByNamespace($classes, $filePath, $namespace, $autoloadType, $basePath, $io);
// if no valid class was found in the file then we do not mark it as scanned as it might still be matched by another rule later
if ($validClasses) {
if ($classes) {
$scannedFiles[$realPath] = true;
}
} else {
@ -126,8 +126,7 @@ class ClassMapGenerator
foreach ($classes as $class) {
// skip classes not within the given namespace prefix
// TODO enable in Composer v1.11 or 2.0 whichever comes first
if (/* null === $autoloadType && */ null !== $namespace && '' !== $namespace && 0 !== strpos($class, $namespace)) {
if (null === $autoloadType && null !== $namespace && '' !== $namespace && 0 !== strpos($class, $namespace)) {
continue;
}
@ -196,19 +195,15 @@ class ClassMapGenerator
// warn only if no valid classes, else silently skip invalid
if (empty($validClasses)) {
foreach ($rejectedClasses as $class) {
trigger_error(
"Class $class located in ".preg_replace('{^'.preg_quote(getcwd()).'}', '.', $filePath, 1)." does not comply with $namespaceType autoloading standard. It will not autoload anymore in Composer v2.0.",
E_USER_DEPRECATED
);
if ($io) {
$io->writeError("<warning>Class $class located in ".preg_replace('{^'.preg_quote(getcwd()).'}', '.', $filePath, 1)." does not comply with $namespaceType autoloading standard. Skipping.</warning>");
}
}
// TODO enable in Composer 2.0
//return array();
return array();
}
// TODO enable in Composer 2.0 & unskip test in AutoloadGeneratorTest::testPSRToClassMapIgnoresNonPSRClasses
//return $validClasses;
return array($classes, $validClasses);
return $validClasses;
}
/**

View File

@ -28,20 +28,20 @@ class Cache
private $io;
private $root;
private $enabled = true;
private $whitelist;
private $allowlist;
private $filesystem;
/**
* @param IOInterface $io
* @param string $cacheDir location of the cache
* @param string $whitelist List of characters that are allowed in path names (used in a regex character class)
* @param string $allowlist List of characters that are allowed in path names (used in a regex character class)
* @param Filesystem $filesystem optional filesystem instance
*/
public function __construct(IOInterface $io, $cacheDir, $whitelist = 'a-z0-9.', Filesystem $filesystem = null)
public function __construct(IOInterface $io, $cacheDir, $allowlist = 'a-z0-9.', Filesystem $filesystem = null)
{
$this->io = $io;
$this->root = rtrim($cacheDir, '/\\') . '/';
$this->whitelist = $whitelist;
$this->allowlist = $allowlist;
$this->filesystem = $filesystem ?: new Filesystem();
if (!self::isUsable($cacheDir)) {
@ -77,7 +77,7 @@ class Cache
public function read($file)
{
if ($this->enabled) {
$file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
$file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
if (file_exists($this->root . $file)) {
$this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG);
@ -91,7 +91,7 @@ class Cache
public function write($file, $contents)
{
if ($this->enabled) {
$file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
$file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
$this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG);
@ -129,7 +129,7 @@ class Cache
public function copyFrom($file, $source)
{
if ($this->enabled) {
$file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
$file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
$this->filesystem->ensureDirectoryExists(dirname($this->root . $file));
if (!file_exists($source)) {
@ -150,7 +150,7 @@ class Cache
public function copyTo($file, $target)
{
if ($this->enabled) {
$file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
$file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
if (file_exists($this->root . $file)) {
try {
touch($this->root . $file, filemtime($this->root . $file), time());
@ -177,7 +177,7 @@ class Cache
public function remove($file)
{
if ($this->enabled) {
$file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
$file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
if (file_exists($this->root . $file)) {
return $this->filesystem->unlink($this->root . $file);
}
@ -229,7 +229,7 @@ class Cache
public function sha1($file)
{
if ($this->enabled) {
$file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
$file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
if (file_exists($this->root . $file)) {
return sha1_file($this->root . $file);
}
@ -241,7 +241,7 @@ class Cache
public function sha256($file)
{
if ($this->enabled) {
$file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
$file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
if (file_exists($this->root . $file)) {
return hash_file('sha256', $this->root . $file);
}

View File

@ -22,6 +22,7 @@ use Composer\Script\ScriptEvents;
use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents;
use Composer\Util\Filesystem;
use Composer\Util\Loop;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@ -111,8 +112,9 @@ EOT
$archiveManager = $composer->getArchiveManager();
} else {
$factory = new Factory;
$downloadManager = $factory->createDownloadManager($io, $config);
$archiveManager = $factory->createArchiveManager($config, $downloadManager);
$httpDownloader = $factory->createHttpDownloader($io, $config);
$downloadManager = $factory->createDownloadManager($io, $config, $httpDownloader);
$archiveManager = $factory->createArchiveManager($config, $downloadManager, new Loop($httpDownloader));
}
if ($packageName) {

View File

@ -27,6 +27,8 @@ use Symfony\Component\Console\Command\Command;
/**
* Base class for Composer commands
*
* @method Application getApplication()
*
* @author Ryan Weaver <ryan@knplabs.com>
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
@ -46,7 +48,7 @@ abstract class BaseCommand extends Command
* @param bool $required
* @param bool|null $disablePlugins
* @throws \RuntimeException
* @return Composer
* @return Composer|null
*/
public function getComposer($required = true, $disablePlugins = null)
{
@ -173,7 +175,7 @@ abstract class BaseCommand extends Command
if ($input->getOption('prefer-source') || $input->getOption('prefer-dist') || ($keepVcsRequiresPreferSource && $input->hasOption('keep-vcs') && $input->getOption('keep-vcs'))) {
$preferSource = $input->getOption('prefer-source') || ($keepVcsRequiresPreferSource && $input->hasOption('keep-vcs') && $input->getOption('keep-vcs'));
$preferDist = $input->getOption('prefer-dist');
$preferDist = (bool) $input->getOption('prefer-dist');
}
return array($preferSource, $preferDist);

View File

@ -12,11 +12,12 @@
namespace Composer\Command;
use Composer\DependencyResolver\Pool;
use Composer\Package\Link;
use Composer\Package\PackageInterface;
use Composer\Repository\ArrayRepository;
use Composer\Repository\InstalledArrayRepository;
use Composer\Repository\CompositeRepository;
use Composer\Repository\RootPackageRepository;
use Composer\Repository\InstalledRepository;
use Composer\Repository\PlatformRepository;
use Composer\Repository\RepositoryFactory;
use Composer\Plugin\CommandEvent;
@ -71,15 +72,12 @@ class BaseDependencyCommand extends BaseCommand
$commandEvent = new CommandEvent(PluginEvents::COMMAND, $this->getName(), $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
// Prepare repositories and set up a pool
$platformOverrides = $composer->getConfig()->get('platform') ?: array();
$repository = new CompositeRepository(array(
new ArrayRepository(array($composer->getPackage())),
$installedRepo = new InstalledRepository(array(
new RootPackageRepository($composer->getPackage()),
$composer->getRepositoryManager()->getLocalRepository(),
new PlatformRepository(array(), $platformOverrides),
));
$pool = new Pool();
$pool->addRepository($repository);
// Parse package name and constraint
list($needle, $textConstraint) = array_pad(
@ -89,17 +87,17 @@ class BaseDependencyCommand extends BaseCommand
);
// Find packages that are or provide the requested package first
$packages = $pool->whatProvides(strtolower($needle));
$packages = $installedRepo->findPackagesWithReplacersAndProviders($needle);
if (empty($packages)) {
throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle));
}
// If the version we ask for is not installed then we need to locate it in remote repos and add it.
// This is needed for why-not to resolve conflicts from an uninstalled version against installed packages.
if (!$repository->findPackage($needle, $textConstraint)) {
if (!$installedRepo->findPackage($needle, $textConstraint)) {
$defaultRepos = new CompositeRepository(RepositoryFactory::defaultRepos($this->getIO()));
if ($match = $defaultRepos->findPackage($needle, $textConstraint)) {
$repository->addRepository(new ArrayRepository(array(clone $match)));
$installedRepo->addRepository(new InstalledArrayRepository(array(clone $match)));
}
}
@ -126,7 +124,7 @@ class BaseDependencyCommand extends BaseCommand
$recursive = $renderTree || $input->getOption(self::OPTION_RECURSIVE);
// Resolve dependencies
$results = $repository->getDependents($needles, $constraint, $inverted, $recursive);
$results = $installedRepo->getDependents($needles, $constraint, $inverted, $recursive);
if (empty($results)) {
$extra = (null !== $constraint) ? sprintf(' in versions %smatching %s', $inverted ? 'not ' : '', $textConstraint) : '';
$this->getIO()->writeError(sprintf(

View File

@ -20,6 +20,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Composer\Repository\PlatformRepository;
use Composer\Repository\InstalledRepository;
class CheckPlatformReqsCommand extends BaseCommand
{
@ -48,12 +49,13 @@ EOT
$requires = $composer->getPackage()->getRequires();
if ($input->getOption('no-dev')) {
$dependencies = $composer->getLocker()->getLockedRepository(!$input->getOption('no-dev'))->getPackages();
$installedRepo = $composer->getLocker()->getLockedRepository(!$input->getOption('no-dev'));
$dependencies = $installedRepo->getPackages();
} else {
$dependencies = $composer->getRepositoryManager()->getLocalRepository()->getPackages();
$installedRepo = $composer->getRepositoryManager()->getLocalRepository();
// fallback to lockfile if installed repo is empty
if (!$dependencies) {
$dependencies = $composer->getLocker()->getLockedRepository(true)->getPackages();
if (!$installedRepo->getPackages()) {
$installedRepo = $composer->getLocker()->getLockedRepository(true);
}
$requires += $composer->getPackage()->getDevRequires();
}
@ -61,7 +63,8 @@ EOT
$requires[$require] = array($link);
}
foreach ($dependencies as $package) {
$installedRepo = new InstalledRepository(array($installedRepo));
foreach ($installedRepo->getPackages() as $package) {
foreach ($package->getRequires() as $require => $link) {
$requires[$require][] = $link;
}
@ -69,19 +72,9 @@ EOT
ksort($requires);
$platformRepo = new PlatformRepository(array(), array());
$currentPlatformPackages = $platformRepo->getPackages();
$currentPlatformPackageMap = array();
/**
* @var PackageInterface $currentPlatformPackage
*/
foreach ($currentPlatformPackages as $currentPlatformPackage) {
$currentPlatformPackageMap[$currentPlatformPackage->getName()] = $currentPlatformPackage;
}
$installedRepo->addRepository(new PlatformRepository(array(), array()));
$results = array();
$exitCode = 0;
/**
@ -89,42 +82,62 @@ EOT
*/
foreach ($requires as $require => $links) {
if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $require)) {
if (isset($currentPlatformPackageMap[$require])) {
$pass = true;
$version = $currentPlatformPackageMap[$require]->getVersion();
foreach ($links as $link) {
if (!$link->getConstraint()->matches(new Constraint('=', $version))) {
$results[] = array(
$currentPlatformPackageMap[$require]->getPrettyName(),
$currentPlatformPackageMap[$require]->getPrettyVersion(),
$link,
'<error>failed</error>',
);
$pass = false;
$exitCode = max($exitCode, 1);
$candidates = $installedRepo->findPackagesWithReplacersAndProviders($require);
if ($candidates) {
$reqResults = array();
foreach ($candidates as $candidate) {
if ($candidate->getName() === $require) {
$candidateConstraint = new Constraint('=', $candidate->getVersion());
$candidateConstraint->setPrettyString($candidate->getPrettyVersion());
} else {
foreach (array_merge($candidate->getProvides(), $candidate->getReplaces()) as $link) {
if ($link->getTarget() === $require) {
$candidateConstraint = $link->getConstraint();
break;
}
}
}
foreach ($links as $link) {
if (!$link->getConstraint()->matches($candidateConstraint)) {
$reqResults[] = array(
$candidate->getName() === $require ? $candidate->getPrettyName() : $require,
$candidateConstraint->getPrettyString(),
$link,
'<error>failed</error>'.($candidate->getName() === $require ? '' : ' <comment>provided by '.$candidate->getPrettyName().'</comment>'),
);
// skip to next candidate
continue 2;
}
}
}
if ($pass) {
$results[] = array(
$currentPlatformPackageMap[$require]->getPrettyName(),
$currentPlatformPackageMap[$require]->getPrettyVersion(),
$candidate->getName() === $require ? $candidate->getPrettyName() : $require,
$candidateConstraint->getPrettyString(),
null,
'<info>success</info>',
'<info>success</info>'.($candidate->getName() === $require ? '' : ' <comment>provided by '.$candidate->getPrettyName().'</comment>'),
);
}
} else {
$results[] = array(
$require,
'n/a',
$links[0],
'<error>missing</error>',
);
$exitCode = max($exitCode, 2);
// candidate matched, skip to next requirement
continue 2;
}
// show the first error from every failed candidate
$results = array_merge($results, $reqResults);
$exitCode = max($exitCode, 1);
continue;
}
$results[] = array(
$require,
'n/a',
$links[0],
'<error>missing</error>',
);
$exitCode = max($exitCode, 2);
}
}

View File

@ -236,7 +236,7 @@ EOT
}
$settingKey = $input->getArgument('setting-key');
if (!$settingKey) {
if (!$settingKey || !is_string($settingKey)) {
return 0;
}

View File

@ -20,7 +20,6 @@ use Composer\Installer\InstallationManager;
use Composer\Installer\SuggestedPackagesReporter;
use Composer\IO\IOInterface;
use Composer\Package\BasePackage;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\Package\Version\VersionSelector;
use Composer\Package\AliasPackage;
@ -28,6 +27,7 @@ use Composer\Repository\RepositoryFactory;
use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository;
use Composer\Repository\InstalledFilesystemRepository;
use Composer\Repository\RepositorySet;
use Composer\Script\ScriptEvents;
use Composer\Util\Silencer;
use Symfony\Component\Console\Input\InputArgument;
@ -38,6 +38,7 @@ use Symfony\Component\Finder\Finder;
use Composer\Json\JsonFile;
use Composer\Config\JsonConfigSource;
use Composer\Util\Filesystem;
use Composer\Util\Loop;
use Composer\Package\Version\VersionParser;
/**
@ -182,8 +183,6 @@ EOT
$composer = Factory::create($io, null, $disablePlugins);
}
$composer->getDownloadManager()->setOutputProgress(!$noProgress);
$fs = new Filesystem();
if ($noScripts === false) {
@ -334,8 +333,8 @@ EOT
throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities)));
}
$pool = new Pool($stability);
$pool->addRepository($sourceRepo);
$repositorySet = new RepositorySet($stability);
$repositorySet->addRepository($sourceRepo);
$phpVersion = null;
$prettyPhpVersion = null;
@ -349,7 +348,7 @@ EOT
}
// find the latest version if there are multiple
$versionSelector = new VersionSelector($pool);
$versionSelector = new VersionSelector($repositorySet);
$package = $versionSelector->findBestCandidate($name, $packageVersion, $phpVersion, $stability);
if (!$package) {
@ -384,15 +383,17 @@ EOT
$package = $package->getAliasOf();
}
$dm = $this->createDownloadManager($io, $config);
$factory = new Factory();
$httpDownloader = $factory->createHttpDownloader($io, $config);
$dm = $factory->createDownloadManager($io, $config, $httpDownloader);
$dm->setPreferSource($preferSource)
->setPreferDist($preferDist)
->setOutputProgress(!$noProgress);
->setPreferDist($preferDist);
$projectInstaller = new ProjectInstaller($directory, $dm);
$im = $this->createInstallationManager();
$im = $factory->createInstallationManager(new Loop($httpDownloader), $io);
$im->addInstaller($projectInstaller);
$im->install(new InstalledFilesystemRepository(new JsonFile('php://memory')), new InstallOperation($package));
$im->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), array(new InstallOperation($package)));
$im->notifyInstalls($io);
// collect suggestions
@ -408,16 +409,4 @@ EOT
return $installedFromVcs;
}
protected function createDownloadManager(IOInterface $io, Config $config)
{
$factory = new Factory();
return $factory->createDownloadManager($io, $config);
}
protected function createInstallationManager()
{
return new InstallationManager();
}
}

View File

@ -22,7 +22,7 @@ use Composer\Plugin\PluginEvents;
use Composer\Util\ConfigValidator;
use Composer\Util\IniHelper;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\Util\StreamContextFactory;
use Composer\SelfUpdate\Keys;
use Composer\SelfUpdate\Versions;
@ -35,8 +35,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
class DiagnoseCommand extends BaseCommand
{
/** @var RemoteFilesystem */
protected $rfs;
/** @var HttpDownloader */
protected $httpDownloader;
/** @var ProcessExecutor */
protected $process;
@ -86,7 +86,7 @@ EOT
$config->merge(array('config' => array('secure-http' => false)));
$config->prohibitUrlByConfig('http://repo.packagist.org', new NullIO);
$this->rfs = Factory::createRemoteFilesystem($io, $config);
$this->httpDownloader = Factory::createHttpDownloader($io, $config);
$this->process = new ProcessExecutor($io);
$io->write('Checking platform settings: ', false);
@ -156,7 +156,7 @@ EOT
$this->outputResult($this->checkVersion($config));
}
$io->write(sprintf('Composer version: <comment>%s</comment>', Composer::VERSION));
$io->write(sprintf('Composer version: <comment>%s</comment>', Composer::getVersion()));
$platformOverrides = $config->get('platform') ?: array();
$platformRepo = new PlatformRepository(array(), $platformOverrides);
@ -229,7 +229,7 @@ EOT
}
try {
$this->rfs->getContents('packagist.org', $proto . '://repo.packagist.org/packages.json', false);
$this->httpDownloader->get($proto . '://repo.packagist.org/packages.json');
} catch (TransportException $e) {
if (false !== strpos($e->getMessage(), 'cafile')) {
$result[] = '<error>[' . get_class($e) . '] ' . $e->getMessage() . '</error>';
@ -256,11 +256,11 @@ EOT
$protocol = extension_loaded('openssl') ? 'https' : 'http';
try {
$json = json_decode($this->rfs->getContents('packagist.org', $protocol . '://repo.packagist.org/packages.json', false), true);
$json = $this->httpDownloader->get($protocol . '://repo.packagist.org/packages.json')->decodeJson();
$hash = reset($json['provider-includes']);
$hash = $hash['sha256'];
$path = str_replace('%hash%', $hash, key($json['provider-includes']));
$provider = $this->rfs->getContents('packagist.org', $protocol . '://repo.packagist.org/'.$path, false);
$provider = $this->httpDownloader->get($protocol . '://repo.packagist.org/'.$path)->getBody();
if (hash('sha256', $provider) !== $hash) {
return 'It seems that your proxy is modifying http traffic on the fly';
@ -288,10 +288,10 @@ EOT
$url = 'http://repo.packagist.org/packages.json';
try {
$this->rfs->getContents('packagist.org', $url, false);
$this->httpDownloader->get($url);
} catch (TransportException $e) {
try {
$this->rfs->getContents('packagist.org', $url, false, array('http' => array('request_fulluri' => false)));
$this->httpDownloader->get($url, array('http' => array('request_fulluri' => false)));
} catch (TransportException $e) {
return 'Unable to assess the situation, maybe packagist.org is down ('.$e->getMessage().')';
}
@ -322,10 +322,10 @@ EOT
$url = 'https://api.github.com/repos/Seldaek/jsonlint/zipball/1.0.0';
try {
$this->rfs->getContents('github.com', $url, false);
$this->httpDownloader->get($url);
} catch (TransportException $e) {
try {
$this->rfs->getContents('github.com', $url, false, array('http' => array('request_fulluri' => false)));
$this->httpDownloader->get($url, array('http' => array('request_fulluri' => false)));
} catch (TransportException $e) {
return 'Unable to assess the situation, maybe github is down ('.$e->getMessage().')';
}
@ -347,7 +347,7 @@ EOT
try {
$url = $domain === 'github.com' ? 'https://api.'.$domain.'/' : 'https://'.$domain.'/api/v3/';
return $this->rfs->getContents($domain, $url, false, array(
return $this->httpDownloader->get($url, array(
'retry-auth-failure' => false,
)) ? true : 'Unexpected error';
} catch (\Exception $e) {
@ -377,8 +377,7 @@ EOT
}
$url = $domain === 'github.com' ? 'https://api.'.$domain.'/rate_limit' : 'https://'.$domain.'/api/rate_limit';
$json = $this->rfs->getContents($domain, $url, false, array('retry-auth-failure' => false));
$data = json_decode($json, true);
$data = $this->httpDownloader->get($url, array('retry-auth-failure' => false))->decodeJson();
return $data['resources']['core'];
}
@ -431,7 +430,7 @@ EOT
return $result;
}
$versionsUtil = new Versions($config, $this->rfs);
$versionsUtil = new Versions($config, $this->httpDownloader);
$latest = $versionsUtil->getLatest();
if (Composer::VERSION !== $latest['version'] && Composer::VERSION !== '@package_version@') {
@ -613,20 +612,6 @@ EOT
$text .= "Install either of them or recompile php without --disable-iconv";
break;
case 'unicode':
$text = PHP_EOL."The detect_unicode setting must be disabled.".PHP_EOL;
$text .= "Add the following to the end of your `php.ini`:".PHP_EOL;
$text .= " detect_unicode = Off";
$displayIniMessage = true;
break;
case 'suhosin':
$text = PHP_EOL."The suhosin.executor.include.whitelist setting is incorrect.".PHP_EOL;
$text .= "Add the following to the end of your `php.ini` or suhosin.ini (Example path [for Debian]: /etc/php5/cli/conf.d/suhosin.ini):".PHP_EOL;
$text .= " suhosin.executor.include.whitelist = phar ".$current;
$displayIniMessage = true;
break;
case 'php':
$text = PHP_EOL."Your PHP ({$current}) is too old, you must upgrade to PHP 5.3.2 or higher.";
break;
@ -729,7 +714,7 @@ EOT
/**
* Check if allow_url_fopen is ON
*
* @return bool|string
* @return true|string
*/
private function checkConnectivity()
{

View File

@ -14,7 +14,7 @@ namespace Composer\Command;
use Composer\Package\CompletePackageInterface;
use Composer\Repository\RepositoryInterface;
use Composer\Repository\ArrayRepository;
use Composer\Repository\RootPackageRepository;
use Composer\Repository\RepositoryFactory;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
@ -157,7 +157,7 @@ EOT
if ($composer) {
return array_merge(
array(new ArrayRepository(array($composer->getPackage()))), // root package
array(new RootPackageRepository($composer->getPackage())), // root package
array($composer->getRepositoryManager()->getLocalRepository()), // installed packages
$composer->getRepositoryManager()->getRepositories() // remotes
);

View File

@ -12,7 +12,6 @@
namespace Composer\Command;
use Composer\DependencyResolver\Pool;
use Composer\Factory;
use Composer\Json\JsonFile;
use Composer\Package\BasePackage;
@ -22,6 +21,7 @@ use Composer\Package\Version\VersionSelector;
use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository;
use Composer\Repository\RepositoryFactory;
use Composer\Repository\RepositorySet;
use Composer\Util\ProcessExecutor;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
@ -42,8 +42,8 @@ class InitCommand extends BaseCommand
/** @var array */
private $gitConfig;
/** @var Pool[] */
private $pools;
/** @var RepositorySet[] */
private $repositorySets;
/**
* {@inheritdoc}
@ -86,8 +86,8 @@ EOT
{
$io = $this->getIO();
$whitelist = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license');
$options = array_filter(array_intersect_key($input->getOptions(), array_flip($whitelist)));
$allowlist = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license');
$options = array_filter(array_intersect_key($input->getOptions(), array_flip($allowlist)));
if (isset($options['author'])) {
$options['authors'] = $this->formatAuthors($options['author']);
@ -688,16 +688,16 @@ EOT
return false !== filter_var($email, FILTER_VALIDATE_EMAIL);
}
private function getPool(InputInterface $input, $minimumStability = null)
private function getRepositorySet(InputInterface $input, $minimumStability = null)
{
$key = $minimumStability ?: 'default';
if (!isset($this->pools[$key])) {
$this->pools[$key] = $pool = new Pool($minimumStability ?: $this->getMinimumStability($input));
$pool->addRepository($this->getRepos());
if (!isset($this->repositorySets[$key])) {
$this->repositorySets[$key] = $repositorySet = new RepositorySet($minimumStability ?: $this->getMinimumStability($input));
$repositorySet->addRepository($this->getRepos());
}
return $this->pools[$key];
return $this->repositorySets[$key];
}
private function getMinimumStability(InputInterface $input)
@ -733,8 +733,8 @@ EOT
*/
private function findBestVersionAndNameForPackage(InputInterface $input, $name, $phpVersion, $preferredStability = 'stable', $requiredVersion = null, $minimumStability = null, $fixed = null)
{
// find the latest version allowed in this pool
$versionSelector = new VersionSelector($this->getPool($input, $minimumStability));
// find the latest version allowed in this repo set
$versionSelector = new VersionSelector($this->getRepositorySet($input, $minimumStability));
$ignorePlatformReqs = $input->hasOption('ignore-platform-reqs') && $input->getOption('ignore-platform-reqs');
// ignore phpVersion if platform requirements are ignored

View File

@ -44,7 +44,6 @@ class InstallCommand extends BaseCommand
new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'),
new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'),
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'),
new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'),
new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'),
new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'),
@ -86,7 +85,6 @@ EOT
}
$composer = $this->getComposer(true, $input->getOption('no-plugins'));
$composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
@ -108,7 +106,6 @@ EOT
->setDevMode(!$input->getOption('no-dev'))
->setDumpAutoloader(!$input->getOption('no-autoloader'))
->setRunScripts(!$input->getOption('no-scripts'))
->setSkipSuggest($input->getOption('no-suggest'))
->setOptimizeAutoloader($optimize)
->setClassMapAuthoritative($authoritative)
->setApcuAutoloader($apcu)

View File

@ -13,6 +13,7 @@
namespace Composer\Command;
use Composer\Config\JsonConfigSource;
use Composer\DependencyResolver\Request;
use Composer\Installer;
use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents;
@ -38,6 +39,7 @@ class RemoveCommand extends BaseCommand
->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.'),
new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'),
new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'),
new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'),
@ -92,26 +94,44 @@ EOT
}
}
$dryRun = $input->getOption('dry-run');
$toRemove = array();
foreach ($packages as $package) {
if (isset($composer[$type][$package])) {
$json->removeLink($type, $composer[$type][$package]);
if ($dryRun) {
$toRemove[$type][] = $composer[$type][$package];
} else {
$json->removeLink($type, $composer[$type][$package]);
}
} elseif (isset($composer[$altType][$package])) {
$io->writeError('<warning>' . $composer[$altType][$package] . ' could not be found in ' . $type . ' but it is present in ' . $altType . '</warning>');
if ($io->isInteractive()) {
if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [<comment>yes</comment>]? ', true)) {
$json->removeLink($altType, $composer[$altType][$package]);
if ($dryRun) {
$toRemove[$altType][] = $composer[$altType][$package];
} else {
$json->removeLink($altType, $composer[$altType][$package]);
}
}
}
} elseif (isset($composer[$type]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$type]))) {
foreach ($matches as $matchedPackage) {
$json->removeLink($type, $matchedPackage);
if ($dryRun) {
$toRemove[$type][] = $matchedPackage;
} else {
$json->removeLink($type, $matchedPackage);
}
}
} elseif (isset($composer[$altType]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$altType]))) {
foreach ($matches as $matchedPackage) {
$io->writeError('<warning>' . $matchedPackage . ' could not be found in ' . $type . ' but it is present in ' . $altType . '</warning>');
if ($io->isInteractive()) {
if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [<comment>yes</comment>]? ', true)) {
$json->removeLink($altType, $matchedPackage);
if ($dryRun) {
$toRemove[$altType][] = $matchedPackage;
} else {
$json->removeLink($altType, $matchedPackage);
}
}
}
}
@ -127,7 +147,21 @@ EOT
// Update packages
$this->resetComposer();
$composer = $this->getComposer(true, $input->getOption('no-plugins'));
$composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
if ($dryRun) {
$rootPackage = $composer->getPackage();
$links = array(
'require' => $rootPackage->getRequires(),
'require-dev' => $rootPackage->getDevRequires(),
);
foreach ($toRemove as $type => $packages) {
foreach ($packages as $package) {
unset($links[$type][$package]);
}
}
$rootPackage->setRequires($links['require']);
$rootPackage->setDevRequires($links['require-dev']);
}
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
@ -146,10 +180,11 @@ EOT
->setClassMapAuthoritative($authoritative)
->setApcuAutoloader($apcu)
->setUpdate(true)
->setUpdateWhitelist($packages)
->setWhitelistTransitiveDependencies(!$input->getOption('no-update-with-dependencies'))
->setUpdateAllowList($packages)
->setUpdateAllowTransitiveDependencies($input->getOption('no-update-with-dependencies') ? Request::UPDATE_ONLY_LISTED : Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE)
->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs'))
->setRunScripts(!$input->getOption('no-scripts'))
->setDryRun($dryRun)
;
$status = $install->run();

View File

@ -12,6 +12,7 @@
namespace Composer\Command;
use Composer\DependencyResolver\Request;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
@ -21,6 +22,8 @@ use Composer\Installer;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
use Composer\Package\Version\VersionParser;
use Composer\Package\Loader\ArrayLoader;
use Composer\Package\BasePackage;
use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents;
use Composer\Repository\CompositeRepository;
@ -35,9 +38,14 @@ use Composer\Util\Silencer;
class RequireCommand extends InitCommand
{
private $newlyCreated;
private $firstRequire;
private $json;
private $file;
private $composerBackup;
/** @var string file name */
private $lock;
/** @var ?string contents before modification if the lock file exists */
private $lockBackup;
protected function configure()
{
@ -47,16 +55,19 @@ class RequireCommand extends InitCommand
->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'),
new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'),
new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'),
new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'),
new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
new InputOption('fixed', null, InputOption::VALUE_NONE, 'Write fixed version to the composer.json.'),
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'),
new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'),
new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'),
new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'),
new InputOption('update-with-dependencies', null, InputOption::VALUE_NONE, 'Allows inherited dependencies to be updated, except those that are root requirements.'),
new InputOption('update-with-all-dependencies', null, InputOption::VALUE_NONE, 'Allows all inherited dependencies to be updated, including those that are root requirements.'),
new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-dependencies'),
new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-all-dependencies'),
new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'),
new InputOption('prefer-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies.'),
new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest versions of dependencies.'),
@ -113,7 +124,9 @@ EOT
}
$this->json = new JsonFile($this->file);
$this->lock = Factory::getLockFile($this->file);
$this->composerBackup = file_get_contents($this->json->getPath());
$this->lockBackup = file_exists($this->lock) ? file_get_contents($this->lock) : null;
// check for writability by writing to the file as is_writable can not be trusted on network-mounts
// see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926
@ -186,7 +199,15 @@ EOT
$sortPackages = $input->getOption('sort-packages') || $composer->getConfig()->get('sort-packages');
if (!$this->updateFileCleanly($this->json, $requirements, $requireKey, $removeKey, $sortPackages)) {
$this->firstRequire = $this->newlyCreated;
if (!$this->firstRequire) {
$composerDefinition = $this->json->read();
if (empty($composerDefinition['require']) && empty($composerDefinition['require-dev'])) {
$this->firstRequire = true;
}
}
if (!$input->getOption('dry-run') && !$this->updateFileCleanly($this->json, $requirements, $requireKey, $removeKey, $sortPackages)) {
$composerDefinition = $this->json->read();
foreach ($requirements as $package => $version) {
$composerDefinition[$requireKey][$package] = $version;
@ -202,51 +223,78 @@ EOT
}
try {
return $this->doUpdate($input, $output, $io, $requirements);
return $this->doUpdate($input, $output, $io, $requirements, $requireKey, $removeKey);
} catch (\Exception $e) {
$this->revertComposerFile(false);
throw $e;
}
}
private function doUpdate(InputInterface $input, OutputInterface $output, IOInterface $io, array $requirements)
private function doUpdate(InputInterface $input, OutputInterface $output, IOInterface $io, array $requirements, $requireKey, $removeKey)
{
// Update packages
$this->resetComposer();
$composer = $this->getComposer(true, $input->getOption('no-plugins'));
$composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
if ($input->getOption('dry-run')) {
$rootPackage = $composer->getPackage();
$links = array(
'require' => $rootPackage->getRequires(),
'require-dev' => $rootPackage->getDevRequires(),
);
$loader = new ArrayLoader();
$newLinks = $loader->parseLinks($rootPackage->getName(), $rootPackage->getPrettyVersion(), BasePackage::$supportedLinkTypes[$requireKey]['description'], $requirements);
$links[$requireKey] = array_merge($links[$requireKey], $newLinks);
foreach ($requirements as $package => $constraint) {
unset($links[$removeKey][$package]);
}
$rootPackage->setRequires($links['require']);
$rootPackage->setDevRequires($links['require-dev']);
}
$updateDevMode = !$input->getOption('update-no-dev');
$optimize = $input->getOption('optimize-autoloader') || $composer->getConfig()->get('optimize-autoloader');
$authoritative = $input->getOption('classmap-authoritative') || $composer->getConfig()->get('classmap-authoritative');
$apcu = $input->getOption('apcu-autoloader') || $composer->getConfig()->get('apcu-autoloader');
$updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED;
if ($input->getOption('update-with-all-dependencies') || $input->getOption('with-all-dependencies')) {
$updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS;
} elseif ($input->getOption('update-with-dependencies') || $input->getOption('with-dependencies')) {
$updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE;
}
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
$install = Installer::create($io, $composer);
$install
->setDryRun($input->getOption('dry-run'))
->setVerbose($input->getOption('verbose'))
->setPreferSource($input->getOption('prefer-source'))
->setPreferDist($input->getOption('prefer-dist'))
->setDevMode($updateDevMode)
->setRunScripts(!$input->getOption('no-scripts'))
->setSkipSuggest($input->getOption('no-suggest'))
->setOptimizeAutoloader($optimize)
->setClassMapAuthoritative($authoritative)
->setApcuAutoloader($apcu)
->setUpdate(true)
->setUpdateWhitelist(array_keys($requirements))
->setWhitelistTransitiveDependencies($input->getOption('update-with-dependencies'))
->setWhitelistAllDependencies($input->getOption('update-with-all-dependencies'))
->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies)
->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs'))
->setPreferStable($input->getOption('prefer-stable'))
->setPreferLowest($input->getOption('prefer-lowest'))
->setDryRun($input->getOption('dry-run'))
;
// if no lock is present, or the file is brand new, we do not do a
// partial update as this is not supported by the Installer
if (!$this->firstRequire && $composer->getConfig()->get('lock')) {
$install->setUpdateAllowList(array_keys($requirements));
}
$status = $install->run();
if ($status !== 0) {
if ($status !== 0 || $input->getOption('dry-run')) {
$this->revertComposerFile(false);
}
@ -285,9 +333,19 @@ EOT
if ($this->newlyCreated) {
$io->writeError("\n".'<error>Installation failed, deleting '.$this->file.'.</error>');
unlink($this->json->getPath());
if (file_exists($this->lock)) {
unlink($this->lock);
}
} else {
$io->writeError("\n".'<error>Installation failed, reverting '.$this->file.' to its original content.</error>');
$msg = ' to its ';
if ($this->lockBackup) {
$msg = ' and '.$this->lock.' to their ';
}
$io->writeError("\n".'<error>Installation failed, reverting '.$this->file.$msg.'original content.</error>');
file_put_contents($this->json->getPath(), $this->composerBackup);
if ($this->lockBackup) {
file_put_contents($this->lock, $this->lockBackup);
}
}
if ($hardExit) {

View File

@ -77,9 +77,9 @@ EOT
}
$io = $this->getIO();
$remoteFilesystem = Factory::createRemoteFilesystem($io, $config);
$httpDownloader = Factory::createHttpDownloader($io, $config);
$versionsUtil = new Versions($config, $remoteFilesystem);
$versionsUtil = new Versions($config, $httpDownloader);
// switch channel if requested
foreach (array('stable', 'preview', 'snapshot') as $channel) {
@ -154,11 +154,11 @@ EOT
$updatingToTag = !preg_match('{^[0-9a-f]{40}$}', $updateVersion);
$io->write(sprintf("Updating to version <info>%s</info> (%s channel).", $updateVersion, $versionsUtil->getChannel()));
$io->write(sprintf("Upgrading to version <info>%s</info> (%s channel).", $updateVersion, $versionsUtil->getChannel()));
$remoteFilename = $baseUrl . ($updatingToTag ? "/download/{$updateVersion}/composer.phar" : '/composer.phar');
$signature = $remoteFilesystem->getContents(self::HOMEPAGE, $remoteFilename.'.sig', false);
$signature = $httpDownloader->get($remoteFilename.'.sig')->getBody();
$io->writeError(' ', false);
$remoteFilesystem->copy(self::HOMEPAGE, $remoteFilename, $tempFilename, !$input->getOption('no-progress'));
$httpDownloader->copy($remoteFilename, $tempFilename);
$io->writeError('');
if (!file_exists($tempFilename) || !$signature) {

View File

@ -14,7 +14,6 @@ namespace Composer\Command;
use Composer\Composer;
use Composer\DependencyResolver\DefaultPolicy;
use Composer\DependencyResolver\Pool;
use Composer\Json\JsonFile;
use Composer\Package\BasePackage;
use Composer\Package\CompletePackageInterface;
@ -23,12 +22,14 @@ use Composer\Package\Version\VersionParser;
use Composer\Package\Version\VersionSelector;
use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents;
use Composer\Repository\ArrayRepository;
use Composer\Repository\ComposerRepository;
use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository;
use Composer\Repository\RepositoryFactory;
use Composer\Repository\InstalledRepository;
use Composer\Repository\RepositoryInterface;
use Composer\Repository\RepositorySet;
use Composer\Repository\RootPackageRepository;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Semver;
use Composer\Spdx\SpdxLicenses;
@ -52,8 +53,8 @@ class ShowCommand extends BaseCommand
protected $versionParser;
protected $colors;
/** @var Pool */
private $pool;
/** @var RepositorySet */
private $repositorySet;
protected function configure()
{
@ -152,13 +153,14 @@ EOT
if ($input->getOption('self')) {
$package = $this->getComposer()->getPackage();
$repos = $installedRepo = new ArrayRepository(array($package));
$repos = $installedRepo = new InstalledRepository(array(new RootPackageRepository($package)));
} elseif ($input->getOption('platform')) {
$repos = $installedRepo = $platformRepo;
$repos = $installedRepo = new InstalledRepository(array($platformRepo));
} elseif ($input->getOption('available')) {
$installedRepo = $platformRepo;
$installedRepo = new InstalledRepository(array($platformRepo));
if ($composer) {
$repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories());
$installedRepo->addRepository($composer->getRepositoryManager()->getLocalRepository());
} else {
$defaultRepos = RepositoryFactory::defaultRepos($io);
$repos = new CompositeRepository($defaultRepos);
@ -166,15 +168,15 @@ EOT
}
} elseif ($input->getOption('all') && $composer) {
$localRepo = $composer->getRepositoryManager()->getLocalRepository();
$installedRepo = new CompositeRepository(array($localRepo, $platformRepo));
$installedRepo = new InstalledRepository(array($localRepo, $platformRepo));
$repos = new CompositeRepository(array_merge(array($installedRepo), $composer->getRepositoryManager()->getRepositories()));
} elseif ($input->getOption('all')) {
$defaultRepos = RepositoryFactory::defaultRepos($io);
$io->writeError('No composer.json found in the current directory, showing available packages from ' . implode(', ', array_keys($defaultRepos)));
$installedRepo = $platformRepo;
$installedRepo = new InstalledRepository(array($platformRepo));
$repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos));
} else {
$repos = $installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository();
$repos = $installedRepo = new InstalledRepository(array($this->getComposer()->getRepositoryManager()->getLocalRepository()));
$rootPkg = $this->getComposer()->getPackage();
if (!$installedRepo->getPackages() && ($rootPkg->getRequires() || $rootPkg->getDevRequires())) {
$io->writeError('<warning>No dependencies installed. Try running composer install or update.</warning>');
@ -313,16 +315,13 @@ EOT
foreach ($repos as $repo) {
if ($repo === $platformRepo) {
$type = 'platform';
} elseif (
$repo === $installedRepo
|| ($installedRepo instanceof CompositeRepository && in_array($repo, $installedRepo->getRepositories(), true))
) {
} elseif ($repo === $installedRepo || in_array($repo, $installedRepo->getRepositories(), true)) {
$type = 'installed';
} else {
$type = 'available';
}
if ($repo instanceof ComposerRepository && $repo->hasProviders()) {
foreach ($repo->getProviderNames() as $name) {
if ($repo instanceof ComposerRepository) {
foreach ($repo->getPackageNames() as $name) {
if (!$packageFilter || preg_match($packageFilter, $name)) {
$packages[$type][$name] = $name;
}
@ -528,32 +527,27 @@ EOT
/**
* finds a package by name and version if provided
*
* @param RepositoryInterface $installedRepo
* @param InstalledRepository $installedRepo
* @param RepositoryInterface $repos
* @param string $name
* @param ConstraintInterface|string $version
* @throws \InvalidArgumentException
* @return array array(CompletePackageInterface, array of versions)
*/
protected function getPackage(RepositoryInterface $installedRepo, RepositoryInterface $repos, $name, $version = null)
protected function getPackage(InstalledRepository $installedRepo, RepositoryInterface $repos, $name, $version = null)
{
$name = strtolower($name);
$constraint = is_string($version) ? $this->versionParser->parseConstraints($version) : $version;
$policy = new DefaultPolicy();
$pool = new Pool('dev');
$pool->addRepository($repos);
$repositorySet = new RepositorySet('dev');
$repositorySet->allowInstalledRepositories();
$repositorySet->addRepository($repos);
$matchedPackage = null;
$versions = array();
$matches = $pool->whatProvides($name, $constraint);
$matches = $repositorySet->findPackages($name, $constraint);
foreach ($matches as $index => $package) {
// skip providers/replacers
if ($package->getName() !== $name) {
unset($matches[$index]);
continue;
}
// select an exact match if it is in the installed repo and no specific version was required
if (null === $version && $installedRepo->hasPackage($package)) {
$matchedPackage = $package;
@ -563,8 +557,10 @@ EOT
$matches[$index] = $package->getId();
}
$pool = $repositorySet->createPoolForPackage($name);
// select preferred package according to policy rules
if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, array(), $matches)) {
if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, $matches)) {
$matchedPackage = $pool->literalToPackage($preferred[0]);
}
@ -576,10 +572,10 @@ EOT
*
* @param CompletePackageInterface $package
* @param array $versions
* @param RepositoryInterface $installedRepo
* @param InstalledRepository $installedRepo
* @param PackageInterface|null $latestPackage
*/
protected function printPackageInfo(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null)
protected function printPackageInfo(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null)
{
$io = $this->getIO();
@ -604,10 +600,10 @@ EOT
*
* @param CompletePackageInterface $package
* @param array $versions
* @param RepositoryInterface $installedRepo
* @param InstalledRepository $installedRepo
* @param PackageInterface|null $latestPackage
*/
protected function printMeta(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null)
protected function printMeta(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null)
{
$io = $this->getIO();
$io->write('<info>name</info> : ' . $package->getPrettyName());
@ -676,19 +672,21 @@ EOT
*
* @param CompletePackageInterface $package
* @param array $versions
* @param RepositoryInterface $installedRepo
* @param InstalledRepository $installedRepo
*/
protected function printVersions(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo)
protected function printVersions(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo)
{
uasort($versions, 'version_compare');
$versions = array_keys(array_reverse($versions));
$versions = array_keys($versions);
$versions = Semver::rsort($versions);
// highlight installed version
if ($installedRepo->hasPackage($package)) {
$installedVersion = $package->getPrettyVersion();
$key = array_search($installedVersion, $versions);
if (false !== $key) {
$versions[$key] = '<info>* ' . $installedVersion . '</info>';
if ($installedPackages = $installedRepo->findPackages($package->getName())) {
foreach ($installedPackages as $installedPackage) {
$installedVersion = $installedPackage->getPrettyVersion();
$key = array_search($installedVersion, $versions);
if (false !== $key) {
$versions[$key] = '<info>* ' . $installedVersion . '</info>';
}
}
}
@ -752,10 +750,10 @@ EOT
*
* @param CompletePackageInterface $package
* @param array $versions
* @param RepositoryInterface $installedRepo
* @param InstalledRepository $installedRepo
* @param PackageInterface|null $latestPackage
*/
protected function printPackageInfoAsJson(CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, PackageInterface $latestPackage = null)
protected function printPackageInfoAsJson(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null)
{
$json = array(
'name' => $package->getPrettyName(),
@ -975,15 +973,15 @@ EOT
/**
* Generate the package tree
*
* @param PackageInterface $package
* @param RepositoryInterface $installedRepo
* @param RepositoryInterface $distantRepos
* @param PackageInterface $package
* @param InstalledRepository $installedRepo
* @param RepositoryInterface $remoteRepos
* @return array
*/
protected function generatePackageTree(
PackageInterface $package,
RepositoryInterface $installedRepo,
RepositoryInterface $distantRepos
InstalledRepository $installedRepo,
RepositoryInterface $remoteRepos
) {
$requires = $package->getRequires();
ksort($requires);
@ -996,7 +994,7 @@ EOT
'version' => $require->getPrettyConstraint(),
);
$deepChildren = $this->addTree($requireName, $require, $installedRepo, $distantRepos, $packagesInTree);
$deepChildren = $this->addTree($requireName, $require, $installedRepo, $remoteRepos, $packagesInTree);
if ($deepChildren) {
$treeChildDesc['requires'] = $deepChildren;
@ -1020,10 +1018,10 @@ EOT
/**
* Display a package tree
*
* @param PackageInterface|string $package
* @param array $packagesInTree
* @param string $previousTreeBar
* @param int $level
* @param array|string $package
* @param array $packagesInTree
* @param string $previousTreeBar
* @param int $level
*/
protected function displayTree(
$package,
@ -1032,7 +1030,7 @@ EOT
$level = 1
) {
$previousTreeBar = str_replace('├', '│', $previousTreeBar);
if (isset($package['requires'])) {
if (is_array($package) && isset($package['requires'])) {
$requires = $package['requires'];
$treeBar = $previousTreeBar . ' ├';
$i = 0;
@ -1075,22 +1073,22 @@ EOT
*
* @param string $name
* @param PackageInterface|string $package
* @param RepositoryInterface $installedRepo
* @param RepositoryInterface $distantRepos
* @param InstalledRepository $installedRepo
* @param RepositoryInterface $remoteRepos
* @param array $packagesInTree
* @return array
*/
protected function addTree(
$name,
$package,
RepositoryInterface $installedRepo,
RepositoryInterface $distantRepos,
InstalledRepository $installedRepo,
RepositoryInterface $remoteRepos,
array $packagesInTree
) {
$children = array();
list($package, $versions) = $this->getPackage(
$installedRepo,
$distantRepos,
$remoteRepos,
$name,
$package->getPrettyConstraint() === 'self.version' ? $package->getConstraint() : $package->getPrettyConstraint()
);
@ -1107,7 +1105,7 @@ EOT
if (!in_array($requireName, $currentTree, true)) {
$currentTree[] = $requireName;
$deepChildren = $this->addTree($requireName, $require, $installedRepo, $distantRepos, $currentTree);
$deepChildren = $this->addTree($requireName, $require, $installedRepo, $remoteRepos, $currentTree);
if ($deepChildren) {
$treeChildDesc['requires'] = $deepChildren;
}
@ -1165,13 +1163,13 @@ EOT
* @param string $phpVersion
* @param bool $minorOnly
*
* @return PackageInterface|null
* @return PackageInterface|false
*/
private function findLatestPackage(PackageInterface $package, Composer $composer, $phpVersion, $minorOnly = false)
{
// find the latest version allowed in this pool
// find the latest version allowed in this repo set
$name = $package->getName();
$versionSelector = new VersionSelector($this->getPool($composer));
$versionSelector = new VersionSelector($this->getRepositorySet($composer));
$stability = $composer->getPackage()->getMinimumStability();
$flags = $composer->getPackage()->getStabilityFlags();
if (isset($flags[$name])) {
@ -1195,13 +1193,13 @@ EOT
return $versionSelector->findBestCandidate($name, $targetVersion, $phpVersion, $bestStability);
}
private function getPool(Composer $composer)
private function getRepositorySet(Composer $composer)
{
if (!$this->pool) {
$this->pool = new Pool($composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags());
$this->pool->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories()));
if (!$this->repositorySet) {
$this->repositorySet = new RepositorySet($composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags());
$this->repositorySet->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories()));
}
return $this->pool;
return $this->repositorySet;
}
}

View File

@ -90,7 +90,7 @@ EOT
// list packages
foreach ($installedRepo->getCanonicalPackages() as $package) {
$downloader = $dm->getDownloaderForInstalledPackage($package);
$downloader = $dm->getDownloaderForPackage($package);
$targetDir = $im->getInstallPath($package);
if ($downloader instanceof ChangeReportInterface) {

View File

@ -13,6 +13,9 @@
namespace Composer\Command;
use Composer\Repository\PlatformRepository;
use Composer\Repository\RootPackageRepository;
use Composer\Repository\CompositeRepository;
use Composer\Installer\SuggestedPackagesReporter;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@ -26,8 +29,10 @@ class SuggestsCommand extends BaseCommand
->setName('suggests')
->setDescription('Shows package suggestions.')
->setDefinition(array(
new InputOption('by-package', null, InputOption::VALUE_NONE, 'Groups output by suggesting package'),
new InputOption('by-package', null, InputOption::VALUE_NONE, 'Groups output by suggesting package (default)'),
new InputOption('by-suggestion', null, InputOption::VALUE_NONE, 'Groups output by suggested package'),
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show suggestions from all dependencies, including transitive ones'),
new InputOption('list', null, InputOption::VALUE_NONE, 'Show only list of suggested package names'),
new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Exclude suggestions from require-dev packages'),
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that you want to list suggestions from.'),
))
@ -36,118 +41,66 @@ class SuggestsCommand extends BaseCommand
The <info>%command.name%</info> command shows a sorted list of suggested packages.
Enabling <info>-v</info> implies <info>--by-package --by-suggestion</info>, showing both lists.
Read more at https://getcomposer.org/doc/03-cli.md#suggests
EOT
)
;
}
/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$lock = $this->getComposer()->getLocker()->getLockData();
$composer = $this->getComposer();
if (empty($lock)) {
throw new \RuntimeException('Lockfile seems to be empty?');
$installedRepos = array(
new RootPackageRepository(clone $composer->getPackage()),
);
$locker = $composer->getLocker();
if ($locker->isLocked()) {
$installedRepos[] = new PlatformRepository(array(), $locker->getPlatformOverrides());
$installedRepos[] = $locker->getLockedRepository(!$input->getOption('no-dev'));
} else {
$installedRepos[] = new PlatformRepository(array(), $composer->getConfig()->get('platform') ?: array());
$installedRepos[] = $composer->getRepositoryManager()->getLocalRepository();
}
$packages = $lock['packages'];
if (!$input->getOption('no-dev')) {
$packages += $lock['packages-dev'];
}
$installedRepo = new CompositeRepository($installedRepos);
$reporter = new SuggestedPackagesReporter($this->getIO());
$filter = $input->getArgument('packages');
// First assemble lookup list of packages that are installed, replaced or provided
$installed = array();
foreach ($packages as $package) {
$installed[] = $package['name'];
if (!empty($package['provide'])) {
$installed = array_merge($installed, array_keys($package['provide']));
}
if (!empty($package['replace'])) {
$installed = array_merge($installed, array_keys($package['replace']));
}
if (empty($filter) && !$input->getOption('all')) {
$filter = array_map(function ($link) {
return $link->getTarget();
}, array_merge($composer->getPackage()->getRequires(), $composer->getPackage()->getDevRequires()));
}
// Undub and sort the install list into a sorted lookup array
$installed = array_flip($installed);
ksort($installed);
// Init platform repo
$platform = new PlatformRepository(array(), $this->getComposer()->getConfig()->get('platform') ?: array());
// Next gather all suggestions that are not in that list
$suggesters = array();
$suggested = array();
foreach ($packages as $package) {
$packageName = $package['name'];
if ((!empty($filter) && !in_array($packageName, $filter)) || empty($package['suggest'])) {
foreach ($installedRepo->getPackages() as $package) {
if (!empty($filter) && !in_array($package->getName(), $filter)) {
continue;
}
foreach ($package['suggest'] as $suggestion => $reason) {
if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $suggestion) && null !== $platform->findPackage($suggestion, '*')) {
continue;
}
if (!isset($installed[$suggestion])) {
$suggesters[$packageName][$suggestion] = $reason;
$suggested[$suggestion][$packageName] = $reason;
}
}
}
ksort($suggesters);
ksort($suggested);
// Determine output mode
$mode = 0;
$reporter->addSuggestionsFromPackage($package);
}
// Determine output mode, default is by-package
$mode = SuggestedPackagesReporter::MODE_BY_PACKAGE;
$io = $this->getIO();
if ($input->getOption('by-package') || $io->isVerbose()) {
$mode |= 1;
}
// if by-suggestion is given we override the default
if ($input->getOption('by-suggestion')) {
$mode |= 2;
$mode = SuggestedPackagesReporter::MODE_BY_SUGGESTION;
}
// unless by-package is also present then we enable both
if ($input->getOption('by-package')) {
$mode |= SuggestedPackagesReporter::MODE_BY_PACKAGE;
}
// list is exclusive and overrides everything else
if ($input->getOption('list')) {
$mode = SuggestedPackagesReporter::MODE_LIST;
}
// Simple mode
if ($mode === 0) {
foreach (array_keys($suggested) as $suggestion) {
$io->write(sprintf('<info>%s</info>', $suggestion));
}
return 0;
}
// Grouped by package
if ($mode & 1) {
foreach ($suggesters as $suggester => $suggestions) {
$io->write(sprintf('<comment>%s</comment> suggests:', $suggester));
foreach ($suggestions as $suggestion => $reason) {
$io->write(sprintf(' - <info>%s</info>: %s', $suggestion, $reason ?: '*'));
}
$io->write('');
}
}
// Grouped by suggestion
if ($mode & 2) {
// Improve readability in full mode
if ($mode & 1) {
$io->write(str_repeat('-', 78));
}
foreach ($suggested as $suggestion => $suggesters) {
$io->write(sprintf('<comment>%s</comment> is suggested by:', $suggestion));
foreach ($suggesters as $suggester => $reason) {
$io->write(sprintf(' - <info>%s</info>: %s', $suggester, $reason ?: '*'));
}
$io->write('');
}
}
$reporter->output($mode, $installedRepo);
return 0;
}

View File

@ -13,6 +13,7 @@
namespace Composer\Command;
use Composer\Composer;
use Composer\DependencyResolver\Request;
use Composer\Installer;
use Composer\IO\IOInterface;
use Composer\Plugin\CommandEvent;
@ -48,9 +49,8 @@ class UpdateCommand extends BaseCommand
new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'),
new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'),
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'),
new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Add also dependencies of whitelisted packages to the whitelist, except those defined in root package.'),
new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Add also all dependencies of whitelisted packages to the whitelist, including those defined in root package.'),
new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Update also dependencies of packages in the argument list, except those which are root requirements.'),
new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Update also dependencies of packages in the argument list, including those which are root requirements.'),
new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'),
new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump.'),
new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'),
@ -121,7 +121,18 @@ EOT
}
}
$composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
// the arguments lock/nothing/mirrors are not package names but trigger a mirror update instead
// they are further mutually exclusive with listing actual package names
$filteredPackages = array_filter($packages, function ($package) {
return !in_array($package, array('lock', 'nothing', 'mirrors'), true);
});
$updateMirrors = $input->getOption('lock') || count($filteredPackages) != count($packages);
$packages = $filteredPackages;
if ($updateMirrors && !empty($packages)) {
$io->writeError('<error>You cannot simultaneously update only a selection of packages and regenerate the lock file metadata.</error>');
return -1;
}
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
@ -135,6 +146,13 @@ EOT
$authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative');
$apcu = $input->getOption('apcu-autoloader') || $config->get('apcu-autoloader');
$updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED;
if ($input->getOption('with-all-dependencies')) {
$updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS;
} elseif ($input->getOption('with-dependencies')) {
$updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE;
}
$install
->setDryRun($input->getOption('dry-run'))
->setVerbose($input->getOption('verbose'))
@ -143,14 +161,13 @@ EOT
->setDevMode(!$input->getOption('no-dev'))
->setDumpAutoloader(!$input->getOption('no-autoloader'))
->setRunScripts(!$input->getOption('no-scripts'))
->setSkipSuggest($input->getOption('no-suggest'))
->setOptimizeAutoloader($optimize)
->setClassMapAuthoritative($authoritative)
->setApcuAutoloader($apcu)
->setUpdate(true)
->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $packages)
->setWhitelistTransitiveDependencies($input->getOption('with-dependencies'))
->setWhitelistAllDependencies($input->getOption('with-all-dependencies'))
->setUpdateMirrors($updateMirrors)
->setUpdateAllowList($packages)
->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies)
->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs'))
->setPreferStable($input->getOption('prefer-stable'))
->setPreferLowest($input->getOption('prefer-lowest'))

View File

@ -124,6 +124,7 @@ class Compiler
->in(__DIR__.'/../../vendor/composer/ca-bundle/')
->in(__DIR__.'/../../vendor/composer/xdebug-handler/')
->in(__DIR__.'/../../vendor/psr/')
->in(__DIR__.'/../../vendor/react/')
->sort($finderSort)
;

View File

@ -53,7 +53,7 @@ class Composer
const VERSION = '@package_version@';
const BRANCH_ALIAS_VERSION = '@package_branch_alias_version@';
const RELEASE_DATE = '@release_date@';
const SOURCE_VERSION = '1.10-dev+source';
const SOURCE_VERSION = '2.0-dev+source';
public static function getVersion()
{

View File

@ -265,7 +265,7 @@ class JsonConfigSource implements ConfigSourceInterface
*
* @param array $array
* @param mixed $value
* @return array
* @return int
*/
private function arrayUnshiftRef(&$array, &$value)
{

View File

@ -230,6 +230,12 @@ class Application extends BaseApplication
if (function_exists('posix_getuid') && posix_getuid() === 0) {
if ($commandName !== 'self-update' && $commandName !== 'selfupdate') {
$io->writeError('<warning>Do not run Composer as root/super user! See https://getcomposer.org/root for details</warning>');
if ($io->isInteractive()) {
if (!$io->askConfirmation('<info>Continue as root/super user</info> [<comment>yes</comment>]? ', true)) {
return 1;
}
}
}
if ($uid = (int) getenv('SUDO_UID')) {
// Silently clobber any sudo credentials on the invoking user to avoid privilege escalations later on
@ -292,7 +298,7 @@ class Application extends BaseApplication
return $result;
} catch (ScriptExecutionException $e) {
return $e->getCode();
return (int) $e->getCode();
} catch (\Exception $e) {
$this->hintCommonErrors($e);
restore_error_handler();

View File

@ -44,54 +44,33 @@ class DefaultPolicy implements PolicyInterface
return $constraint->matchSpecific($version, true);
}
public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package, $mustMatchName = false)
public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null)
{
$packages = array();
$packages = $this->groupLiteralsByName($pool, $literals);
foreach ($pool->whatProvides($package->getName(), null, $mustMatchName) as $candidate) {
if ($candidate !== $package) {
$packages[] = $candidate;
}
}
return $packages;
}
public function getPriority(Pool $pool, PackageInterface $package)
{
return $pool->getPriority($package->getRepository());
}
public function selectPreferredPackages(Pool $pool, array $installedMap, array $literals, $requiredPackage = null)
{
$packages = $this->groupLiteralsByNamePreferInstalled($pool, $installedMap, $literals);
foreach ($packages as &$literals) {
foreach ($packages as &$nameLiterals) {
$policy = $this;
usort($literals, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) {
return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true);
usort($nameLiterals, function ($a, $b) use ($policy, $pool, $requiredPackage) {
return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true);
});
}
foreach ($packages as &$literals) {
$literals = $this->pruneToHighestPriorityOrInstalled($pool, $installedMap, $literals);
$literals = $this->pruneToBestVersion($pool, $literals);
$literals = $this->pruneRemoteAliases($pool, $literals);
foreach ($packages as &$sortedLiterals) {
$sortedLiterals = $this->pruneToBestVersion($pool, $sortedLiterals);
$sortedLiterals = $this->pruneRemoteAliases($pool, $sortedLiterals);
}
$selected = call_user_func_array('array_merge', $packages);
// now sort the result across all packages to respect replaces across packages
usort($selected, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) {
return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage);
usort($selected, function ($a, $b) use ($policy, $pool, $requiredPackage) {
return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage);
});
return $selected;
}
protected function groupLiteralsByNamePreferInstalled(Pool $pool, array $installedMap, $literals)
protected function groupLiteralsByName(Pool $pool, $literals)
{
$packages = array();
foreach ($literals as $literal) {
@ -100,12 +79,7 @@ class DefaultPolicy implements PolicyInterface
if (!isset($packages[$packageName])) {
$packages[$packageName] = array();
}
if (isset($installedMap[abs($literal)])) {
array_unshift($packages[$packageName], $literal);
} else {
$packages[$packageName][] = $literal;
}
$packages[$packageName][] = $literal;
}
return $packages;
@ -114,61 +88,49 @@ class DefaultPolicy implements PolicyInterface
/**
* @protected
*/
public function compareByPriorityPreferInstalled(Pool $pool, array $installedMap, PackageInterface $a, PackageInterface $b, $requiredPackage = null, $ignoreReplace = false)
public function compareByPriority(Pool $pool, PackageInterface $a, PackageInterface $b, $requiredPackage = null, $ignoreReplace = false)
{
if ($a->getRepository() === $b->getRepository()) {
// prefer aliases to the original package
if ($a->getName() === $b->getName()) {
$aAliased = $a instanceof AliasPackage;
$bAliased = $b instanceof AliasPackage;
if ($aAliased && !$bAliased) {
return -1; // use a
}
if (!$aAliased && $bAliased) {
return 1; // use b
}
// prefer aliases to the original package
if ($a->getName() === $b->getName()) {
$aAliased = $a instanceof AliasPackage;
$bAliased = $b instanceof AliasPackage;
if ($aAliased && !$bAliased) {
return -1; // use a
}
if (!$ignoreReplace) {
// return original, not replaced
if ($this->replaces($a, $b)) {
return 1; // use b
}
if ($this->replaces($b, $a)) {
return -1; // use a
}
// for replacers not replacing each other, put a higher prio on replacing
// packages with the same vendor as the required package
if ($requiredPackage && false !== ($pos = strpos($requiredPackage, '/'))) {
$requiredVendor = substr($requiredPackage, 0, $pos);
$aIsSameVendor = substr($a->getName(), 0, $pos) === $requiredVendor;
$bIsSameVendor = substr($b->getName(), 0, $pos) === $requiredVendor;
if ($bIsSameVendor !== $aIsSameVendor) {
return $aIsSameVendor ? -1 : 1;
}
}
if (!$aAliased && $bAliased) {
return 1; // use b
}
// priority equal, sort by package id to make reproducible
if ($a->id === $b->id) {
return 0;
}
return ($a->id < $b->id) ? -1 : 1;
}
if (isset($installedMap[$a->id])) {
return -1;
if (!$ignoreReplace) {
// return original, not replaced
if ($this->replaces($a, $b)) {
return 1; // use b
}
if ($this->replaces($b, $a)) {
return -1; // use a
}
// for replacers not replacing each other, put a higher prio on replacing
// packages with the same vendor as the required package
if ($requiredPackage && false !== ($pos = strpos($requiredPackage, '/'))) {
$requiredVendor = substr($requiredPackage, 0, $pos);
$aIsSameVendor = substr($a->getName(), 0, $pos) === $requiredVendor;
$bIsSameVendor = substr($b->getName(), 0, $pos) === $requiredVendor;
if ($bIsSameVendor !== $aIsSameVendor) {
return $aIsSameVendor ? -1 : 1;
}
}
}
if (isset($installedMap[$b->id])) {
return 1;
// priority equal, sort by package id to make reproducible
if ($a->id === $b->id) {
return 0;
}
return ($this->getPriority($pool, $a) > $this->getPriority($pool, $b)) ? -1 : 1;
return ($a->id < $b->id) ? -1 : 1;
}
/**
@ -218,37 +180,6 @@ class DefaultPolicy implements PolicyInterface
return $bestLiterals;
}
/**
* Assumes that installed packages come first and then all highest priority packages
*/
protected function pruneToHighestPriorityOrInstalled(Pool $pool, array $installedMap, array $literals)
{
$selected = array();
$priority = null;
foreach ($literals as $literal) {
$package = $pool->literalToPackage($literal);
if (isset($installedMap[$package->id])) {
$selected[] = $literal;
continue;
}
if (null === $priority) {
$priority = $this->getPriority($pool, $package);
}
if ($this->getPriority($pool, $package) != $priority) {
break;
}
$selected[] = $literal;
}
return $selected;
}
/**
* Assumes that locally aliased (in root package requires) packages take priority over branch-alias ones
*

View File

@ -23,14 +23,13 @@ class GenericRule extends Rule
protected $literals;
/**
* @param array $literals
* @param int $reason A RULE_* constant describing the reason for generating this rule
* @param Link|PackageInterface $reasonData
* @param array $job The job this rule was created from
* @param array $literals
* @param int|null $reason A RULE_* constant describing the reason for generating this rule
* @param Link|PackageInterface|int|null $reasonData
*/
public function __construct(array $literals, $reason, $reasonData, $job = null)
public function __construct(array $literals, $reason, $reasonData)
{
parent::__construct($reason, $reasonData, $job);
parent::__construct($reason, $reasonData);
// sort all packages ascending by id
sort($literals);

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\DependencyResolver;
use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\Package\AliasPackage;
use Composer\Package\Link;
use Composer\Package\PackageInterface;
use Composer\Repository\PlatformRepository;
use Composer\Repository\RepositoryInterface;
use Composer\Semver\Constraint\Constraint;
/**
* @author Nils Adermann <naderman@naderman.de>
*/
class LocalRepoTransaction extends Transaction
{
public function __construct(RepositoryInterface $lockedRepository, $localRepository)
{
parent::__construct(
$localRepository->getPackages(),
$lockedRepository->getPackages()
);
}
}

View File

@ -0,0 +1,133 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\DependencyResolver;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\Package\AliasPackage;
use Composer\Package\RootAliasPackage;
use Composer\Package\RootPackageInterface;
use Composer\Repository\ArrayRepository;
use Composer\Repository\RepositoryInterface;
use Composer\Test\Repository\ArrayRepositoryTest;
/**
* @author Nils Adermann <naderman@naderman.de>
*/
class LockTransaction extends Transaction
{
/**
* packages in current lock file, platform repo or otherwise present
* @var array
*/
protected $presentMap;
/**
* Packages which cannot be mapped, platform repo, root package, other fixed repos
* @var array
*/
protected $unlockableMap;
/**
* @var array
*/
protected $resultPackages;
public function __construct(Pool $pool, $presentMap, $unlockableMap, $decisions)
{
$this->presentMap = $presentMap;
$this->unlockableMap = $unlockableMap;
$this->setResultPackages($pool, $decisions);
parent::__construct($this->presentMap, $this->resultPackages['all']);
}
// TODO make this a bit prettier instead of the two text indexes?
public function setResultPackages(Pool $pool, Decisions $decisions)
{
$this->resultPackages = array('all' => array(), 'non-dev' => array(), 'dev' => array());
foreach ($decisions as $i => $decision) {
$literal = $decision[Decisions::DECISION_LITERAL];
if ($literal > 0) {
$package = $pool->literalToPackage($literal);
$this->resultPackages['all'][] = $package;
if (!isset($this->unlockableMap[$package->id])) {
$this->resultPackages['non-dev'][] = $package;
}
}
}
}
public function setNonDevPackages(LockTransaction $extractionResult)
{
$packages = $extractionResult->getNewLockPackages(false);
$this->resultPackages['dev'] = $this->resultPackages['non-dev'];
$this->resultPackages['non-dev'] = array();
foreach ($packages as $package) {
foreach ($this->resultPackages['dev'] as $i => $resultPackage) {
// TODO this comparison is probably insufficient, aliases, what about modified versions? I guess they aren't possible?
if ($package->getName() == $resultPackage->getName()) {
$this->resultPackages['non-dev'][] = $resultPackage;
unset($this->resultPackages['dev'][$i]);
}
}
}
}
// TODO additionalFixedRepository needs to be looked at here as well?
public function getNewLockPackages($devMode, $updateMirrors = false)
{
$packages = array();
foreach ($this->resultPackages[$devMode ? 'dev' : 'non-dev'] as $package) {
if (!($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) {
// if we're just updating mirrors we need to reset references to the same as currently "present" packages' references to keep the lock file as-is
// we do not reset references if the currently present package didn't have any, or if the type of VCS has changed
if ($updateMirrors && !isset($this->presentMap[spl_object_hash($package)])) {
foreach ($this->presentMap as $presentPackage) {
if ($package->getName() == $presentPackage->getName() &&
$package->getVersion() == $presentPackage->getVersion() &&
$presentPackage->getSourceReference() &&
$presentPackage->getSourceType() === $package->getSourceType()
) {
$package->setSourceDistReferences($presentPackage->getSourceReference());
}
}
}
$packages[] = $package;
}
}
return $packages;
}
/**
* Checks which of the given aliases from composer.json are actually in use for the lock file
*/
public function getAliases($aliases)
{
$usedAliases = array();
foreach ($this->resultPackages['all'] as $package) {
if ($package instanceof AliasPackage) {
if (isset($aliases[$package->getName()])) {
$usedAliases[$package->getName()] = $aliases[$package->getName()];
}
}
}
return $usedAliases;
}
}

View File

@ -0,0 +1,105 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\DependencyResolver;
use Composer\Package\PackageInterface;
use Composer\Package\Link;
/**
* @author Nils Adermann <naderman@naderman.de>
*
* MultiConflictRule([A, B, C]) acts as Rule([-A, -B]), Rule([-A, -C]), Rule([-B, -C])
*/
class MultiConflictRule extends Rule
{
protected $literals;
/**
* @param array $literals
* @param int $reason A RULE_* constant describing the reason for generating this rule
* @param Link|PackageInterface $reasonData
*/
public function __construct(array $literals, $reason, $reasonData)
{
parent::__construct($reason, $reasonData);
if (count($literals) < 3) {
throw new \RuntimeException("multi conflict rule requires at least 3 literals");
}
// sort all packages ascending by id
sort($literals);
$this->literals = $literals;
}
public function getLiterals()
{
return $this->literals;
}
public function getHash()
{
$data = unpack('ihash', md5('c:'.implode(',', $this->literals), true));
return $data['hash'];
}
/**
* Checks if this rule is equal to another one
*
* Ignores whether either of the rules is disabled.
*
* @param Rule $rule The rule to check against
* @return bool Whether the rules are equal
*/
public function equals(Rule $rule)
{
if ($rule instanceof MultiConflictRule) {
return $this->literals === $rule->getLiterals();
}
return false;
}
public function isAssertion()
{
return false;
}
public function disable()
{
throw new \RuntimeException("Disabling multi conflict rules is not possible. Please contact composer at https://github.com/composer/composer to let us debug what lead to this situation.");
}
/**
* Formats a rule as a string of the format (Literal1|Literal2|...)
*
* @return string
*/
public function __toString()
{
// TODO multi conflict?
$result = $this->isDisabled() ? 'disabled(multi(' : '(multi(';
foreach ($this->literals as $i => $literal) {
if ($i != 0) {
$result .= '|';
}
$result .= $literal;
}
$result .= '))';
return $result;
}
}

View File

@ -47,20 +47,28 @@ class InstallOperation extends SolverOperation
}
/**
* Returns job type.
* Returns operation type.
*
* @return string
*/
public function getJobType()
public function getOperationType()
{
return 'install';
}
/**
* {@inheritDoc}
*/
public function show($lock)
{
return ($lock ? 'Locking ' : 'Installing ').$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')';
}
/**
* {@inheritDoc}
*/
public function __toString()
{
return 'Installing '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')';
return $this->show(false);
}
}

View File

@ -48,20 +48,28 @@ class MarkAliasInstalledOperation extends SolverOperation
}
/**
* Returns job type.
* Returns operation type.
*
* @return string
*/
public function getJobType()
public function getOperationType()
{
return 'markAliasInstalled';
}
/**
* {@inheritDoc}
*/
public function show($lock)
{
return 'Marking '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().') as installed, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->package->getAliasOf()->getFullPrettyVersion().')';
}
/**
* {@inheritDoc}
*/
public function __toString()
{
return 'Marking '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).') as installed, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->formatVersion($this->package->getAliasOf()).')';
return $this->show(false);
}
}

View File

@ -48,20 +48,28 @@ class MarkAliasUninstalledOperation extends SolverOperation
}
/**
* Returns job type.
* Returns operation type.
*
* @return string
*/
public function getJobType()
public function getOperationType()
{
return 'markAliasUninstalled';
}
/**
* {@inheritDoc}
*/
public function show($lock)
{
return 'Marking '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().') as uninstalled, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->package->getAliasOf()->getFullPrettyVersion().')';
}
/**
* {@inheritDoc}
*/
public function __toString()
{
return 'Marking '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).') as uninstalled, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->formatVersion($this->package->getAliasOf()).')';
return $this->show(false);
}
}

View File

@ -20,11 +20,11 @@ namespace Composer\DependencyResolver\Operation;
interface OperationInterface
{
/**
* Returns job type.
* Returns operation type.
*
* @return string
*/
public function getJobType();
public function getOperationType();
/**
* Returns operation reason.
@ -33,6 +33,14 @@ interface OperationInterface
*/
public function getReason();
/**
* Serializes the operation in a human readable format
*
* @param $lock bool Whether this is an operation on the lock file
* @return string
*/
public function show($lock);
/**
* Serializes the operation in a human readable format
*

View File

@ -43,8 +43,9 @@ abstract class SolverOperation implements OperationInterface
return $this->reason;
}
protected function formatVersion(PackageInterface $package)
{
return $package->getFullPrettyVersion();
}
/**
* @param $lock bool Whether this is an operation on the lock file
* @return string
*/
abstract public function show($lock);
}

View File

@ -47,20 +47,28 @@ class UninstallOperation extends SolverOperation
}
/**
* Returns job type.
* Returns operation type.
*
* @return string
*/
public function getJobType()
public function getOperationType()
{
return 'uninstall';
}
/**
* {@inheritDoc}
*/
public function show($lock)
{
return 'Removing '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')';
}
/**
* {@inheritDoc}
*/
public function __toString()
{
return 'Uninstalling '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')';
return $this->show(false);
}
}

View File

@ -61,23 +61,41 @@ class UpdateOperation extends SolverOperation
}
/**
* Returns job type.
* Returns operation type.
*
* @return string
*/
public function getJobType()
public function getOperationType()
{
return 'update';
}
/**
* {@inheritDoc}
*/
public function show($lock)
{
$fromVersion = $this->initialPackage->getFullPrettyVersion();
$toVersion = $this->targetPackage->getFullPrettyVersion();
if ($fromVersion === $toVersion && $this->initialPackage->getSourceReference() !== $this->targetPackage->getSourceReference()) {
$fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF);
$toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF);
} elseif ($fromVersion === $toVersion && $this->initialPackage->getDistReference() !== $this->targetPackage->getDistReference()) {
$fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF);
$toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF);
}
$actionName = VersionParser::isUpgrade($this->initialPackage->getVersion(), $this->targetPackage->getVersion()) ? 'Upgrading' : 'Downgrading';
return $actionName.' '.$this->initialPackage->getPrettyName().' ('.$fromVersion.' => '.$toVersion.')';
}
/**
* {@inheritDoc}
*/
public function __toString()
{
$actionName = VersionParser::isUpgrade($this->initialPackage->getVersion(), $this->targetPackage->getVersion()) ? 'Updating' : 'Downgrading';
return $actionName.' '.$this->initialPackage->getPrettyName().' ('.$this->formatVersion($this->initialPackage).') to '.
$this->targetPackage->getPrettyName(). ' ('.$this->formatVersion($this->targetPackage).')';
return $this->show(false);
}
}

View File

@ -20,8 +20,5 @@ use Composer\Package\PackageInterface;
interface PolicyInterface
{
public function versionCompare(PackageInterface $a, PackageInterface $b, $operator);
public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package);
public function selectPreferredPackages(Pool $pool, array $installedMap, array $literals, $requiredPackage = null);
public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null);
}

View File

@ -12,143 +12,56 @@
namespace Composer\DependencyResolver;
use Composer\Package\BasePackage;
use Composer\Package\AliasPackage;
use Composer\Package\Version\VersionParser;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\EmptyConstraint;
use Composer\Repository\RepositoryInterface;
use Composer\Repository\CompositeRepository;
use Composer\Repository\ComposerRepository;
use Composer\Repository\InstalledRepositoryInterface;
use Composer\Repository\PlatformRepository;
use Composer\Package\PackageInterface;
/**
* A package pool contains repositories that provide packages.
* A package pool contains all packages for dependency resolution
*
* @author Nils Adermann <naderman@naderman.de>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class Pool implements \Countable
{
const MATCH_NAME = -1;
const MATCH_NONE = 0;
const MATCH = 1;
const MATCH_PROVIDE = 2;
const MATCH_REPLACE = 3;
const MATCH_FILTERED = 4;
protected $repositories = array();
protected $providerRepos = array();
protected $packages = array();
protected $packageByName = array();
protected $packageByExactName = array();
protected $acceptableStabilities;
protected $stabilityFlags;
protected $versionParser;
protected $providerCache = array();
protected $filterRequires;
protected $whitelist = null;
protected $id = 1;
protected $unacceptableFixedPackages;
public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array())
public function __construct(array $packages = array(), array $unacceptableFixedPackages = array())
{
$this->versionParser = new VersionParser;
$this->acceptableStabilities = array();
foreach (BasePackage::$stabilities as $stability => $value) {
if ($value <= BasePackage::$stabilities[$minimumStability]) {
$this->acceptableStabilities[$stability] = $value;
}
}
$this->stabilityFlags = $stabilityFlags;
$this->filterRequires = $filterRequires;
foreach ($filterRequires as $name => $constraint) {
if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name)) {
unset($this->filterRequires[$name]);
}
}
$this->setPackages($packages);
$this->unacceptableFixedPackages = $unacceptableFixedPackages;
}
public function setWhitelist($whitelist)
private function setPackages(array $packages)
{
$this->whitelist = $whitelist;
$this->providerCache = array();
}
$id = 1;
/**
* Adds a repository and its packages to this package pool
*
* @param RepositoryInterface $repo A package repository
* @param array $rootAliases
*/
public function addRepository(RepositoryInterface $repo, $rootAliases = array())
{
if ($repo instanceof CompositeRepository) {
$repos = $repo->getRepositories();
} else {
$repos = array($repo);
}
foreach ($packages as $package) {
$this->packages[] = $package;
foreach ($repos as $repo) {
$this->repositories[] = $repo;
$package->id = $id++;
$this->packageByExactName[$package->getName()][$package->id] = $package;
$exempt = $repo instanceof PlatformRepository || $repo instanceof InstalledRepositoryInterface;
if ($repo instanceof ComposerRepository && $repo->hasProviders()) {
$this->providerRepos[] = $repo;
$repo->setRootAliases($rootAliases);
$repo->resetPackageIds();
} else {
foreach ($repo->getPackages() as $package) {
$names = $package->getNames();
$stability = $package->getStability();
if ($exempt || $this->isPackageAcceptable($names, $stability)) {
$package->setId($this->id++);
$this->packages[] = $package;
$this->packageByExactName[$package->getName()][$package->id] = $package;
foreach ($names as $provided) {
$this->packageByName[$provided][] = $package;
}
// handle root package aliases
$name = $package->getName();
if (isset($rootAliases[$name][$package->getVersion()])) {
$alias = $rootAliases[$name][$package->getVersion()];
if ($package instanceof AliasPackage) {
$package = $package->getAliasOf();
}
$aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']);
$aliasPackage->setRootPackageAlias(true);
$aliasPackage->setId($this->id++);
$package->getRepository()->addPackage($aliasPackage);
$this->packages[] = $aliasPackage;
$this->packageByExactName[$aliasPackage->getName()][$aliasPackage->id] = $aliasPackage;
foreach ($aliasPackage->getNames() as $name) {
$this->packageByName[$name][] = $aliasPackage;
}
}
}
}
foreach ($package->getNames() as $provided) {
$this->packageByName[$provided][] = $package;
}
}
}
public function getPriority(RepositoryInterface $repo)
{
$priority = array_search($repo, $this->repositories, true);
if (false === $priority) {
throw new \RuntimeException("Could not determine repository priority. The repository was not registered in the pool.");
}
return -$priority;
}
/**
* Retrieves the package object for a given package id.
*
@ -176,104 +89,52 @@ class Pool implements \Countable
* packages must match or null to return all
* @param bool $mustMatchName Whether the name of returned packages
* must match the given name
* @param bool $bypassFilters If enabled, filterRequires and stability matching is ignored
* @return PackageInterface[] A set of packages
*/
public function whatProvides($name, ConstraintInterface $constraint = null, $mustMatchName = false, $bypassFilters = false)
public function whatProvides($name, ConstraintInterface $constraint = null, $mustMatchName = false)
{
if ($bypassFilters) {
return $this->computeWhatProvides($name, $constraint, $mustMatchName, true);
}
$key = ((int) $mustMatchName).$constraint;
if (isset($this->providerCache[$name][$key])) {
return $this->providerCache[$name][$key];
}
return $this->providerCache[$name][$key] = $this->computeWhatProvides($name, $constraint, $mustMatchName, $bypassFilters);
return $this->providerCache[$name][$key] = $this->computeWhatProvides($name, $constraint, $mustMatchName);
}
/**
* @see whatProvides
*/
private function computeWhatProvides($name, $constraint, $mustMatchName = false, $bypassFilters = false)
private function computeWhatProvides($name, $constraint, $mustMatchName = false)
{
$candidates = array();
foreach ($this->providerRepos as $repo) {
foreach ($repo->whatProvides($this, $name, $bypassFilters) as $candidate) {
$candidates[] = $candidate;
if ($candidate->id < 1) {
$candidate->setId($this->id++);
$this->packages[$this->id - 2] = $candidate;
}
}
}
if ($mustMatchName) {
$candidates = array_filter($candidates, function ($candidate) use ($name) {
return $candidate->getName() == $name;
});
if (isset($this->packageByExactName[$name])) {
$candidates = array_merge($candidates, $this->packageByExactName[$name]);
$candidates = $this->packageByExactName[$name];
}
} elseif (isset($this->packageByName[$name])) {
$candidates = array_merge($candidates, $this->packageByName[$name]);
$candidates = $this->packageByName[$name];
}
$matches = $provideMatches = array();
$nameMatch = false;
$matches = array();
foreach ($candidates as $candidate) {
$aliasOfCandidate = null;
// alias packages are not white listed, make sure that the package
// being aliased is white listed
if ($candidate instanceof AliasPackage) {
$aliasOfCandidate = $candidate->getAliasOf();
}
if ($this->whitelist !== null && !$bypassFilters && (
(!($candidate instanceof AliasPackage) && !isset($this->whitelist[$candidate->id])) ||
($candidate instanceof AliasPackage && !isset($this->whitelist[$aliasOfCandidate->id]))
)) {
continue;
}
switch ($this->match($candidate, $name, $constraint, $bypassFilters)) {
switch ($this->match($candidate, $name, $constraint)) {
case self::MATCH_NONE:
break;
case self::MATCH_NAME:
$nameMatch = true;
break;
case self::MATCH:
$nameMatch = true;
$matches[] = $candidate;
break;
case self::MATCH_PROVIDE:
$provideMatches[] = $candidate;
break;
case self::MATCH_REPLACE:
$matches[] = $candidate;
break;
case self::MATCH_FILTERED:
break;
default:
throw new \UnexpectedValueException('Unexpected match type');
}
}
// if a package with the required name exists, we ignore providers
if ($nameMatch) {
return $matches;
}
return array_merge($matches, $provideMatches);
return $matches;
}
public function literalToPackage($literal)
@ -296,23 +157,6 @@ class Pool implements \Countable
return $prefix.' '.$package->getPrettyString();
}
public function isPackageAcceptable($name, $stability)
{
foreach ((array) $name as $n) {
// allow if package matches the global stability requirement and has no exception
if (!isset($this->stabilityFlags[$n]) && isset($this->acceptableStabilities[$stability])) {
return true;
}
// allow if package matches the package-specific stability flag
if (isset($this->stabilityFlags[$n]) && BasePackage::$stabilities[$stability] <= $this->stabilityFlags[$n]) {
return true;
}
}
return false;
}
/**
* Checks if the package matches the given constraint directly or through
* provided or replaced packages
@ -322,27 +166,19 @@ class Pool implements \Countable
* @param ConstraintInterface $constraint The constraint to verify
* @return int One of the MATCH* constants of this class or 0 if there is no match
*/
public function match($candidate, $name, ConstraintInterface $constraint = null, $bypassFilters)
public function match($candidate, $name, ConstraintInterface $constraint = null)
{
$candidateName = $candidate->getName();
$candidateVersion = $candidate->getVersion();
$isDev = $candidate->getStability() === 'dev';
$isAlias = $candidate instanceof AliasPackage;
if (!$bypassFilters && !$isDev && !$isAlias && isset($this->filterRequires[$name])) {
$requireFilter = $this->filterRequires[$name];
} else {
$requireFilter = new EmptyConstraint;
}
if ($candidateName === $name) {
$pkgConstraint = new Constraint('==', $candidateVersion);
if ($constraint === null || $constraint->matches($pkgConstraint)) {
return $requireFilter->matches($pkgConstraint) ? self::MATCH : self::MATCH_FILTERED;
return self::MATCH;
}
return self::MATCH_NAME;
return self::MATCH_NONE;
}
$provides = $candidate->getProvides();
@ -352,13 +188,13 @@ class Pool implements \Countable
if (isset($replaces[0]) || isset($provides[0])) {
foreach ($provides as $link) {
if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) {
return $requireFilter->matches($link->getConstraint()) ? self::MATCH_PROVIDE : self::MATCH_FILTERED;
return self::MATCH_PROVIDE;
}
}
foreach ($replaces as $link) {
if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) {
return $requireFilter->matches($link->getConstraint()) ? self::MATCH_REPLACE : self::MATCH_FILTERED;
return self::MATCH_REPLACE;
}
}
@ -366,13 +202,18 @@ class Pool implements \Countable
}
if (isset($provides[$name]) && ($constraint === null || $constraint->matches($provides[$name]->getConstraint()))) {
return $requireFilter->matches($provides[$name]->getConstraint()) ? self::MATCH_PROVIDE : self::MATCH_FILTERED;
return self::MATCH_PROVIDE;
}
if (isset($replaces[$name]) && ($constraint === null || $constraint->matches($replaces[$name]->getConstraint()))) {
return $requireFilter->matches($replaces[$name]->getConstraint()) ? self::MATCH_REPLACE : self::MATCH_FILTERED;
return self::MATCH_REPLACE;
}
return self::MATCH_NONE;
}
public function isUnacceptableFixedPackage(PackageInterface $package)
{
return in_array($package, $this->unacceptableFixedPackages, true);
}
}

View File

@ -0,0 +1,376 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\DependencyResolver;
use Composer\IO\IOInterface;
use Composer\Package\AliasPackage;
use Composer\Package\BasePackage;
use Composer\Package\Package;
use Composer\Package\PackageInterface;
use Composer\Package\Version\StabilityFilter;
use Composer\Repository\PlatformRepository;
use Composer\Repository\RootPackageRepository;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\EmptyConstraint;
use Composer\Semver\Constraint\MultiConstraint;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Plugin\PrePoolCreateEvent;
use Composer\Plugin\PluginEvents;
/**
* @author Nils Adermann <naderman@naderman.de>
*/
class PoolBuilder
{
private $acceptableStabilities;
private $stabilityFlags;
private $rootAliases;
private $rootReferences;
private $eventDispatcher;
private $io;
private $aliasMap = array();
private $nameConstraints = array();
private $loadedNames = array();
private $packages = array();
private $unacceptableFixedPackages = array();
private $updateAllowList = array();
private $skippedLoad = array();
private $updateAllowWarned = array();
public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, IOInterface $io, EventDispatcher $eventDispatcher = null)
{
$this->acceptableStabilities = $acceptableStabilities;
$this->stabilityFlags = $stabilityFlags;
$this->rootAliases = $rootAliases;
$this->rootReferences = $rootReferences;
$this->eventDispatcher = $eventDispatcher;
$this->io = $io;
}
public function buildPool(array $repositories, Request $request)
{
if ($request->getUpdateAllowList()) {
$this->updateAllowList = $request->getUpdateAllowList();
$this->warnAboutNonMatchingUpdateAllowList($request);
foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) {
if (!$this->isUpdateAllowed($lockedPackage)) {
$request->fixPackage($lockedPackage);
$lockedName = $lockedPackage->getName();
// remember which packages we skipped loading remote content for in this partial update
$this->skippedLoad[$lockedPackage->getName()] = $lockedName;
foreach ($lockedPackage->getReplaces() as $link) {
$this->skippedLoad[$link->getTarget()] = $lockedName;
}
}
}
}
$loadNames = array();
foreach ($request->getFixedPackages() as $package) {
$this->nameConstraints[$package->getName()] = null;
$this->loadedNames[$package->getName()] = true;
// replace means conflict, so if a fixed package replaces a name, no need to load that one, packages would conflict anyways
foreach ($package->getReplaces() as $link) {
$this->nameConstraints[$package->getName()] = null;
$this->loadedNames[$link->getTarget()] = true;
}
// TODO in how far can we do the above for conflicts? It's more tricky cause conflicts can be limited to
// specific versions while replace is a conflict with all versions of the name
if (
$package->getRepository() instanceof RootPackageRepository
|| $package->getRepository() instanceof PlatformRepository
|| StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $package->getNames(), $package->getStability())
) {
$loadNames += $this->loadPackage($request, $package, false);
} else {
$this->unacceptableFixedPackages[] = $package;
}
}
foreach ($request->getRequires() as $packageName => $constraint) {
// fixed packages have already been added, so if a root require needs one of them, no need to do anything
if (isset($this->loadedNames[$packageName])) {
continue;
}
$loadNames[$packageName] = $constraint;
$this->nameConstraints[$packageName] = $constraint ? new MultiConstraint(array($constraint), false) : null;
}
// clean up loadNames for anything we manually marked loaded above
foreach ($loadNames as $name => $void) {
if (isset($this->loadedNames[$name])) {
unset($loadNames[$name]);
}
}
while (!empty($loadNames)) {
foreach ($loadNames as $name => $void) {
$this->loadedNames[$name] = true;
}
$newLoadNames = array();
foreach ($repositories as $repository) {
// these repos have their packages fixed if they need to be loaded so we
// never need to load anything else from them
if ($repository instanceof PlatformRepository || $repository === $request->getLockedRepository()) {
continue;
}
$result = $repository->loadPackages($loadNames, $this->acceptableStabilities, $this->stabilityFlags);
foreach ($result['namesFound'] as $name) {
// avoid loading the same package again from other repositories once it has been found
unset($loadNames[$name]);
}
foreach ($result['packages'] as $package) {
$newLoadNames += $this->loadPackage($request, $package);
}
}
$loadNames = $newLoadNames;
}
// filter packages according to all the require statements collected for each package
foreach ($this->packages as $i => $package) {
// we check all alias related packages at once, so no need to check individual aliases
// isset also checks non-null value
if (!$package instanceof AliasPackage && isset($this->nameConstraints[$package->getName()])) {
$constraint = $this->nameConstraints[$package->getName()];
$aliasedPackages = array($i => $package);
if (isset($this->aliasMap[spl_object_hash($package)])) {
$aliasedPackages += $this->aliasMap[spl_object_hash($package)];
}
$found = false;
foreach ($aliasedPackages as $packageOrAlias) {
if ($constraint->matches(new Constraint('==', $packageOrAlias->getVersion()))) {
$found = true;
}
}
if (!$found) {
foreach ($aliasedPackages as $index => $packageOrAlias) {
unset($this->packages[$index]);
}
}
}
}
if ($this->eventDispatcher) {
$prePoolCreateEvent = new PrePoolCreateEvent(
PluginEvents::PRE_POOL_CREATE,
$repositories,
$request,
$this->acceptableStabilities,
$this->stabilityFlags,
$this->rootAliases,
$this->rootReferences,
$this->packages,
$this->unacceptableFixedPackages
);
$this->eventDispatcher->dispatch($prePoolCreateEvent->getName(), $prePoolCreateEvent);
$this->packages = $prePoolCreateEvent->getPackages();
$this->unacceptableFixedPackages = $prePoolCreateEvent->getUnacceptableFixedPackages();
}
$pool = new Pool($this->packages, $this->unacceptableFixedPackages);
$this->aliasMap = array();
$this->nameConstraints = array();
$this->loadedNames = array();
$this->packages = array();
$this->unacceptableFixedPackages = array();
return $pool;
}
private function loadPackage(Request $request, PackageInterface $package, $propagateUpdate = true)
{
end($this->packages);
$index = key($this->packages) + 1;
$this->packages[] = $package;
if ($package instanceof AliasPackage) {
$this->aliasMap[spl_object_hash($package->getAliasOf())][$index] = $package;
}
$name = $package->getName();
// we're simply setting the root references on all versions for a name here and rely on the solver to pick the
// right version. It'd be more work to figure out which versions and which aliases of those versions this may
// apply to
if (isset($this->rootReferences[$name])) {
// do not modify the references on already locked packages
if (!$request->isFixedPackage($package)) {
$package->setSourceDistReferences($this->rootReferences[$name]);
}
}
// if propogateUpdate is false we are loading a fixed package, root aliases do not apply as they are manually
// loaded as separate packages in this case
if ($propagateUpdate && isset($this->rootAliases[$name][$package->getVersion()])) {
$alias = $this->rootAliases[$name][$package->getVersion()];
if ($package instanceof AliasPackage) {
$basePackage = $package->getAliasOf();
} else {
$basePackage = $package;
}
$aliasPackage = new AliasPackage($basePackage, $alias['alias_normalized'], $alias['alias']);
$aliasPackage->setRootPackageAlias(true);
$this->packages[] = $aliasPackage;
$this->aliasMap[spl_object_hash($aliasPackage->getAliasOf())][$index+1] = $aliasPackage;
}
$loadNames = array();
foreach ($package->getRequires() as $link) {
$require = $link->getTarget();
if (!isset($this->loadedNames[$require])) {
$loadNames[$require] = null;
// if this is a partial update with transitive dependencies we need to unfix the package we now know is a
// dependency of another package which we are trying to update, and then attempt to load it again
} elseif ($propagateUpdate && $request->getUpdateAllowTransitiveDependencies() && isset($this->skippedLoad[$require])) {
if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $this->skippedLoad[$require])) {
$this->unfixPackage($request, $require);
$loadNames[$require] = null;
} elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $require) && !isset($this->updateAllowWarned[$require])) {
$this->updateAllowWarned[$require] = true;
$this->io->writeError('<warning>Dependency "'.$require.'" is also a root requirement. Package has not been listed as an update argument, so keeping locked at old version. Use --with-all-dependencies to include root dependencies.</warning>');
}
}
$linkConstraint = $link->getConstraint();
if ($linkConstraint && !($linkConstraint instanceof EmptyConstraint)) {
if (!array_key_exists($require, $this->nameConstraints)) {
$this->nameConstraints[$require] = new MultiConstraint(array($linkConstraint), false);
} elseif ($this->nameConstraints[$require]) {
// TODO addConstraint function?
$this->nameConstraints[$require] = new MultiConstraint(array_merge(array($linkConstraint), $this->nameConstraints[$require]->getConstraints()), false);
}
// else it is null and should stay null
} else {
$this->nameConstraints[$require] = null;
}
}
// if we're doing a partial update with deps we also need to unfix packages which are being replaced in case they
// are currently locked and thus prevent this updateable package from being installable/updateable
if ($propagateUpdate && $request->getUpdateAllowTransitiveDependencies()) {
foreach ($package->getReplaces() as $link) {
$replace = $link->getTarget();
if (isset($this->loadedNames[$replace]) && isset($this->skippedLoad[$replace])) {
if ($request->getUpdateAllowTransitiveRootDependencies() || !$this->isRootRequire($request, $this->skippedLoad[$replace])) {
$this->unfixPackage($request, $replace);
$loadNames[$replace] = null;
// TODO should we try to merge constraints here?
$this->nameConstraints[$replace] = null;
} elseif (!$request->getUpdateAllowTransitiveRootDependencies() && $this->isRootRequire($request, $replace) && !isset($this->updateAllowWarned[$require])) {
$this->updateAllowWarned[$replace] = true;
$this->io->writeError('<warning>Dependency "'.$require.'" is also a root requirement. Package has not been listed as an update argument, so keeping locked at old version. Use --with-all-dependencies to include root dependencies.</warning>');
}
}
}
}
return $loadNames;
}
/**
* Checks if a particular name is required directly in the request
*
* @return bool
*/
private function isRootRequire(Request $request, $name)
{
$rootRequires = $request->getRequires();
return isset($rootRequires[$name]);
}
/**
* Checks whether the update allow list allows this package in the lock file to be updated
* @return bool
*/
private function isUpdateAllowed(PackageInterface $package)
{
foreach ($this->updateAllowList as $pattern => $void) {
$patternRegexp = BasePackage::packageNameToRegexp($pattern);
if (preg_match($patternRegexp, $package->getName())) {
return true;
}
}
return false;
}
private function warnAboutNonMatchingUpdateAllowList(Request $request)
{
foreach ($this->updateAllowList as $pattern => $void) {
$patternRegexp = BasePackage::packageNameToRegexp($pattern);
// update pattern matches a locked package? => all good
foreach ($request->getLockedRepository()->getPackages() as $package) {
if (preg_match($patternRegexp, $package->getName())) {
continue 2;
}
}
// update pattern matches a root require? => all good, probably a new package
foreach ($request->getRequires() as $packageName => $constraint) {
if (preg_match($patternRegexp, $packageName)) {
continue 2;
}
}
if (strpos($pattern, '*') !== false) {
$this->io->writeError('<warning>Pattern "' . $pattern . '" listed for update does not match any locked packages.</warning>');
} else {
$this->io->writeError('<warning>Package "' . $pattern . '" listed for update is not locked.</warning>');
}
}
}
/**
* Reverts the decision to use a fixed package from lock file if a partial update with transitive dependencies
* found that this package actually needs to be updated
*/
private function unfixPackage(Request $request, $name)
{
// remove locked package by this name which was already initialized
foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) {
if (!($lockedPackage instanceof AliasPackage) && $lockedPackage->getName() === $name) {
if (false !== $index = array_search($lockedPackage, $this->packages, true)) {
$request->unfixPackage($lockedPackage);
unset($this->packages[$index]);
if (isset($this->aliasMap[spl_object_hash($lockedPackage)])) {
foreach ($this->aliasMap[spl_object_hash($lockedPackage)] as $aliasIndex => $aliasPackage) {
$request->unfixPackage($aliasPackage);
unset($this->packages[$aliasIndex]);
}
unset($this->aliasMap[spl_object_hash($lockedPackage)]);
}
}
}
}
// if we unfixed a replaced package name, we also need to unfix the replacer itself
if ($this->skippedLoad[$name] !== $name) {
$this->unfixPackage($request, $this->skippedLoad[$name]);
}
unset($this->skippedLoad[$name]);
unset($this->loadedNames[$name]);
}
}

View File

@ -13,6 +13,8 @@
namespace Composer\DependencyResolver;
use Composer\Package\CompletePackageInterface;
use Composer\Repository\RepositorySet;
use Composer\Semver\Constraint\Constraint;
/**
* Represents a problem detected while solving dependencies
@ -28,20 +30,13 @@ class Problem
protected $reasonSeen;
/**
* A set of reasons for the problem, each is a rule or a job and a rule
* A set of reasons for the problem, each is a rule or a root require and a rule
* @var array
*/
protected $reasons = array();
protected $section = 0;
protected $pool;
public function __construct(Pool $pool)
{
$this->pool = $pool;
}
/**
* Add a rule as a reason
*
@ -49,10 +44,7 @@ class Problem
*/
public function addRule(Rule $rule)
{
$this->addReason(spl_object_hash($rule), array(
'rule' => $rule,
'job' => $rule->getJob(),
));
$this->addReason(spl_object_hash($rule), $rule);
}
/**
@ -68,124 +60,67 @@ class Problem
/**
* A human readable textual representation of the problem's reasons
*
* @param array $installedMap A map of all installed packages
* @param array $installedMap A map of all present packages
* @return string
*/
public function getPrettyString(array $installedMap = array())
public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array())
{
// TODO doesn't this entirely defeat the purpose of the problem sections? what's the point of sections?
$reasons = call_user_func_array('array_merge', array_reverse($this->reasons));
if (count($reasons) === 1) {
reset($reasons);
$reason = current($reasons);
$rule = current($reasons);
$job = $reason['job'];
if (!in_array($rule->getReason(), array(Rule::RULE_ROOT_REQUIRE, Rule::RULE_FIXED), true)) {
throw new \LogicException("Single reason problems must contain a request rule.");
}
$packageName = $job['packageName'];
$constraint = $job['constraint'];
$reasonData = $rule->getReasonData();
$packageName = $reasonData['packageName'];
$constraint = $reasonData['constraint'];
if (isset($constraint)) {
$packages = $this->pool->whatProvides($packageName, $constraint);
$packages = $pool->whatProvides($packageName, $constraint);
} else {
$packages = array();
}
if ($job && $job['cmd'] === 'install' && empty($packages)) {
// handle php/hhvm
if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') {
$version = phpversion();
$available = $this->pool->whatProvides($packageName);
if (count($available)) {
$firstAvailable = reset($available);
$version = $firstAvailable->getPrettyVersion();
$extra = $firstAvailable->getExtra();
if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) {
$version .= '; ' . $firstAvailable->getDescription();
}
}
$msg = "\n - This package requires ".$packageName.$this->constraintToText($constraint).' but ';
if (defined('HHVM_VERSION') || (count($available) && $packageName === 'hhvm')) {
return $msg . 'your HHVM version does not satisfy that requirement.';
}
if ($packageName === 'hhvm') {
return $msg . 'you are running this with PHP and not HHVM.';
}
return $msg . 'your PHP version ('. $version .') does not satisfy that requirement.';
}
// handle php extensions
if (0 === stripos($packageName, 'ext-')) {
if (false !== strpos($packageName, ' ')) {
return "\n - The requested PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.';
}
$ext = substr($packageName, 4);
$error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system';
return "\n - The requested PHP extension ".$packageName.$this->constraintToText($constraint).' '.$error.'. Install or enable PHP\'s '.$ext.' extension.';
}
// handle linked libs
if (0 === stripos($packageName, 'lib-')) {
if (strtolower($packageName) === 'lib-icu') {
$error = extension_loaded('intl') ? 'has the wrong version installed, try upgrading the intl extension.' : 'is missing from your system, make sure the intl extension is loaded.';
return "\n - The requested linked library ".$packageName.$this->constraintToText($constraint).' '.$error;
}
return "\n - The requested linked library ".$packageName.$this->constraintToText($constraint).' has the wrong version installed or is missing from your system, make sure to load the extension providing it.';
}
if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) {
$illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName);
return "\n - The requested package ".$packageName.' could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.';
}
if ($providers = $this->pool->whatProvides($packageName, $constraint, true, true)) {
return "\n - The requested package ".$packageName.$this->constraintToText($constraint).' is satisfiable by '.$this->getPackageList($providers).' but these conflict with your requirements or minimum-stability.';
}
if ($providers = $this->pool->whatProvides($packageName, null, true, true)) {
return "\n - The requested package ".$packageName.$this->constraintToText($constraint).' exists as '.$this->getPackageList($providers).' but these are rejected by your constraint.';
}
return "\n - The requested package ".$packageName.' could not be found in any version, there may be a typo in the package name.';
if (empty($packages)) {
return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $packageName, $constraint));
}
}
$messages = array();
foreach ($reasons as $reason) {
$rule = $reason['rule'];
$job = $reason['job'];
if ($job) {
$messages[] = $this->jobToText($job);
} elseif ($rule) {
if ($rule instanceof Rule) {
$messages[] = $rule->getPrettyString($this->pool, $installedMap);
}
}
foreach ($reasons as $rule) {
$messages[] = $rule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool);
}
return "\n - ".implode("\n - ", $messages);
}
public function isCausedByLock()
{
foreach ($this->reasons as $sectionRules) {
foreach ($sectionRules as $rule) {
if ($rule->isCausedByLock()) {
return true;
}
}
}
}
/**
* Store a reason descriptor but ignore duplicates
*
* @param string $id A canonical identifier for the reason
* @param string $reason The reason descriptor
* @param Rule $reason The reason descriptor
*/
protected function addReason($id, $reason)
protected function addReason($id, Rule $reason)
{
// TODO: if a rule is part of a problem description in two sections, isn't this going to remove a message
// that is important to understand the issue?
if (!isset($this->reasonSeen[$id])) {
$this->reasonSeen[$id] = true;
$this->reasons[$this->section][] = $reason;
@ -198,39 +133,150 @@ class Problem
}
/**
* Turns a job into a human readable description
*
* @param array $job
* @return string
* @internal
*/
protected function jobToText($job)
public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $packageName, $constraint = null)
{
$packageName = $job['packageName'];
$constraint = $job['constraint'];
switch ($job['cmd']) {
case 'install':
$packages = $this->pool->whatProvides($packageName, $constraint);
if (!$packages) {
return 'No package found to satisfy install request for '.$packageName.$this->constraintToText($constraint);
// handle php/hhvm
if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') {
$version = phpversion();
$available = $pool->whatProvides($packageName);
if (count($available)) {
$firstAvailable = reset($available);
$version = $firstAvailable->getPrettyVersion();
$extra = $firstAvailable->getExtra();
if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) {
$version .= '; ' . str_replace('Package ', '', $firstAvailable->getDescription());
}
}
$msg = "- Root composer.json requires ".$packageName.self::constraintToText($constraint).' but ';
if (defined('HHVM_VERSION') || (count($available) && $packageName === 'hhvm')) {
return array($msg, 'your HHVM version does not satisfy that requirement.');
}
if ($packageName === 'hhvm') {
return array($msg, 'you are running this with PHP and not HHVM.');
}
return array($msg, 'your '.$packageName.' version ('. $version .') does not satisfy that requirement.');
}
// handle php extensions
if (0 === stripos($packageName, 'ext-')) {
if (false !== strpos($packageName, ' ')) {
return array('- ', "PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.');
}
$ext = substr($packageName, 4);
$error = extension_loaded($ext) ? 'it has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'it is missing from your system';
return array("- Root composer.json requires PHP extension ".$packageName.self::constraintToText($constraint).' but ', $error.'. Install or enable PHP\'s '.$ext.' extension.');
}
// handle linked libs
if (0 === stripos($packageName, 'lib-')) {
if (strtolower($packageName) === 'lib-icu') {
$error = extension_loaded('intl') ? 'it has the wrong version installed, try upgrading the intl extension.' : 'it is missing from your system, make sure the intl extension is loaded.';
return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', $error);
}
return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', 'it has the wrong version installed or is missing from your system, make sure to load the extension providing it.');
}
$fixedPackage = null;
foreach ($request->getFixedPackages() as $package) {
if ($package->getName() === $packageName) {
$fixedPackage = $package;
if ($pool->isUnacceptableFixedPackage($package)) {
return array("- ", $package->getPrettyName().' is fixed to '.$package->getPrettyVersion().' (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you list it as an argument for the update command.');
}
break;
}
}
// first check if the actual requested package is found in normal conditions
// if so it must mean it is rejected by another constraint than the one given here
if ($packages = $repositorySet->findPackages($packageName, $constraint)) {
$rootReqs = $repositorySet->getRootRequires();
if (isset($rootReqs[$packageName])) {
$filtered = array_filter($packages, function ($p) use ($rootReqs, $packageName) {
return $rootReqs[$packageName]->matches(new Constraint('==', $p->getVersion()));
});
if (0 === count($filtered)) {
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with your root composer.json require ('.$rootReqs[$packageName]->getPrettyString().').');
}
}
if ($fixedPackage) {
$fixedConstraint = new Constraint('==', $fixedPackage->getVersion());
$filtered = array_filter($packages, function ($p) use ($fixedConstraint) {
return $fixedConstraint->matches(new Constraint('==', $p->getVersion()));
});
if (0 === count($filtered)) {
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but the package is fixed to '.$fixedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.');
}
}
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with another require.');
}
// check if the package is found when bypassing stability checks
if ($packages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) {
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your minimum-stability.');
}
// check if the package is found when bypassing the constraint check
if ($packages = $repositorySet->findPackages($packageName, null)) {
// we must first verify if a valid package would be found in a lower priority repository
if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) {
$higherRepoPackages = $repositorySet->findPackages($packageName, null);
$nextRepoPackages = array();
$nextRepo = null;
foreach ($allReposPackages as $package) {
if ($nextRepo === null || $nextRepo === $package->getRepository()) {
$nextRepoPackages[] = $package;
$nextRepo = $package->getRepository();
} else {
break;
}
}
return 'Installation request for '.$packageName.$this->constraintToText($constraint).' -> satisfiable by '.$this->getPackageList($packages).'.';
case 'update':
return 'Update request for '.$packageName.$this->constraintToText($constraint).'.';
case 'remove':
return 'Removal request for '.$packageName.$this->constraintToText($constraint).'';
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable.');
}
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your constraint.');
}
if (isset($constraint)) {
$packages = $this->pool->whatProvides($packageName, $constraint);
} else {
$packages = array();
if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) {
$illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName);
return array("- Root composer.json requires $packageName, it ", 'could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.');
}
return 'Job(cmd='.$job['cmd'].', target='.$packageName.', packages=['.$this->getPackageList($packages).'])';
if ($providers = $repositorySet->getProviders($packageName)) {
$maxProviders = 20;
$providersStr = implode(array_map(function ($p) {
$description = $p['description'] ? ' '.substr($p['description'], 0, 100) : '';
return " - ${p['name']}".$description."\n";
}, count($providers) > $maxProviders+1 ? array_slice($providers, 0, $maxProviders) : $providers));
if (count($providers) > $maxProviders+1) {
$providersStr .= ' ... and '.(count($providers)-$maxProviders).' more.'."\n";
}
return array("- Root composer.json requires $packageName".self::constraintToText($constraint).", it ", "could not be found in any version, but the following packages provide it:\n".$providersStr." Consider requiring one of these to satisfy the $packageName requirement.");
}
return array("- Root composer.json requires $packageName, it ", "could not be found in any version, there may be a typo in the package name.");
}
protected function getPackageList($packages)
/**
* @internal
*/
public static function getPackageList(array $packages)
{
$prepared = array();
foreach ($packages as $package) {
@ -238,19 +284,37 @@ class Problem
$prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion();
}
foreach ($prepared as $name => $package) {
// remove the implicit dev-master alias to avoid cruft in the display
if (isset($package['versions']['9999999-dev']) && isset($package['versions']['dev-master'])) {
unset($package['versions']['9999999-dev']);
}
$prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']';
}
return implode(', ', $prepared);
}
private static function hasMultipleNames(array $packages)
{
$name = null;
foreach ($packages as $package) {
if ($name === null || $name === $package->getName()) {
$name = $package->getName();
} else {
return true;
}
}
return false;
}
/**
* Turns a constraint into text usable in a sentence describing a job
* Turns a constraint into text usable in a sentence describing a request
*
* @param \Composer\Semver\Constraint\ConstraintInterface $constraint
* @return string
*/
protected function constraintToText($constraint)
protected static function constraintToText($constraint)
{
return $constraint ? ' '.$constraint->getPrettyString() : '';
}

View File

@ -12,6 +12,10 @@
namespace Composer\DependencyResolver;
use Composer\Package\Package;
use Composer\Package\PackageInterface;
use Composer\Package\RootAliasPackage;
use Composer\Repository\LockArrayRepository;
use Composer\Semver\Constraint\ConstraintInterface;
/**
@ -19,60 +23,129 @@ use Composer\Semver\Constraint\ConstraintInterface;
*/
class Request
{
protected $jobs;
/**
* Identifies a partial update for listed packages only, all dependencies will remain at locked versions
*/
const UPDATE_ONLY_LISTED = 0;
public function __construct()
/**
* Identifies a partial update for listed packages and recursively all their dependencies, however dependencies
* also directly required by the root composer.json and their dependencies will remain at the locked version.
*/
const UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE = 1;
/**
* Identifies a partial update for listed packages and recursively all their dependencies, even dependencies
* also directly required by the root composer.json will be updated.
*/
const UPDATE_LISTED_WITH_TRANSITIVE_DEPS = 2;
protected $lockedRepository;
protected $requires = array();
protected $fixedPackages = array();
protected $unlockables = array();
protected $updateAllowList = array();
protected $updateAllowTransitiveDependencies = false;
public function __construct(LockArrayRepository $lockedRepository = null)
{
$this->jobs = array();
$this->lockedRepository = $lockedRepository;
}
public function install($packageName, ConstraintInterface $constraint = null)
public function requireName($packageName, ConstraintInterface $constraint = null)
{
$this->addJob($packageName, 'install', $constraint);
}
public function update($packageName, ConstraintInterface $constraint = null)
{
$this->addJob($packageName, 'update', $constraint);
}
public function remove($packageName, ConstraintInterface $constraint = null)
{
$this->addJob($packageName, 'remove', $constraint);
$packageName = strtolower($packageName);
$this->requires[$packageName] = $constraint;
}
/**
* Mark an existing package as being installed and having to remain installed
*
* These jobs will not be tempered with by the solver
*
* @param string $packageName
* @param ConstraintInterface|null $constraint
* @param bool $lockable if set to false, the package will not be written to the lock file
*/
public function fix($packageName, ConstraintInterface $constraint = null)
public function fixPackage(PackageInterface $package, $lockable = true)
{
$this->addJob($packageName, 'install', $constraint, true);
$this->fixedPackages[spl_object_hash($package)] = $package;
if (!$lockable) {
$this->unlockables[spl_object_hash($package)] = $package;
}
}
protected function addJob($packageName, $cmd, ConstraintInterface $constraint = null, $fixed = false)
public function unfixPackage(PackageInterface $package)
{
$packageName = strtolower($packageName);
$this->jobs[] = array(
'cmd' => $cmd,
'packageName' => $packageName,
'constraint' => $constraint,
'fixed' => $fixed,
);
unset($this->fixedPackages[spl_object_hash($package)]);
unset($this->unlockables[spl_object_hash($package)]);
}
public function updateAll()
public function setUpdateAllowList($updateAllowList, $updateAllowTransitiveDependencies)
{
$this->jobs[] = array('cmd' => 'update-all');
$this->updateAllowList = $updateAllowList;
$this->updateAllowTransitiveDependencies = $updateAllowTransitiveDependencies;
}
public function getJobs()
public function getUpdateAllowList()
{
return $this->jobs;
return $this->updateAllowList;
}
public function getUpdateAllowTransitiveDependencies()
{
return $this->updateAllowTransitiveDependencies !== self::UPDATE_ONLY_LISTED;
}
public function getUpdateAllowTransitiveRootDependencies()
{
return $this->updateAllowTransitiveDependencies === self::UPDATE_LISTED_WITH_TRANSITIVE_DEPS;
}
public function getRequires()
{
return $this->requires;
}
public function getFixedPackages()
{
return $this->fixedPackages;
}
public function isFixedPackage(PackageInterface $package)
{
return isset($this->fixedPackages[spl_object_hash($package)]);
}
// TODO look into removing the packageIds option, the only place true is used is for the installed map in the solver problems
// some locked packages may not be in the pool so they have a package->id of -1
public function getPresentMap($packageIds = false)
{
$presentMap = array();
if ($this->lockedRepository) {
foreach ($this->lockedRepository->getPackages() as $package) {
$presentMap[$packageIds ? $package->id : spl_object_hash($package)] = $package;
}
}
foreach ($this->fixedPackages as $package) {
$presentMap[$packageIds ? $package->id : spl_object_hash($package)] = $package;
}
return $presentMap;
}
public function getUnlockableMap()
{
$unlockableMap = array();
foreach ($this->unlockables as $package) {
$unlockableMap[$package->id] = $package;
}
return $unlockableMap;
}
public function getLockedRepository()
{
return $this->lockedRepository;
}
}

View File

@ -15,6 +15,7 @@ namespace Composer\DependencyResolver;
use Composer\Package\CompletePackage;
use Composer\Package\Link;
use Composer\Package\PackageInterface;
use Composer\Repository\RepositorySet;
/**
* @author Nils Adermann <naderman@naderman.de>
@ -24,8 +25,8 @@ abstract class Rule
{
// reason constants
const RULE_INTERNAL_ALLOW_UPDATE = 1;
const RULE_JOB_INSTALL = 2;
const RULE_JOB_REMOVE = 3;
const RULE_ROOT_REQUIRE = 2;
const RULE_FIXED = 3;
const RULE_PACKAGE_CONFLICT = 6;
const RULE_PACKAGE_REQUIRES = 7;
const RULE_PACKAGE_OBSOLETES = 8;
@ -41,22 +42,17 @@ abstract class Rule
const BITFIELD_DISABLED = 16;
protected $bitfield;
protected $job;
protected $request;
protected $reasonData;
/**
* @param int $reason A RULE_* constant describing the reason for generating this rule
* @param Link|PackageInterface $reasonData
* @param array $job The job this rule was created from
*/
public function __construct($reason, $reasonData, $job = null)
public function __construct($reason, $reasonData)
{
$this->reasonData = $reasonData;
if ($job) {
$this->job = $job;
}
$this->bitfield = (0 << self::BITFIELD_DISABLED) |
($reason << self::BITFIELD_REASON) |
(255 << self::BITFIELD_TYPE);
@ -66,11 +62,6 @@ abstract class Rule
abstract public function getHash();
public function getJob()
{
return $this->job;
}
abstract public function equals(Rule $rule);
public function getReason()
@ -85,11 +76,17 @@ abstract class Rule
public function getRequiredPackage()
{
if ($this->getReason() === self::RULE_JOB_INSTALL) {
return $this->reasonData;
$reason = $this->getReason();
if ($reason === self::RULE_ROOT_REQUIRE) {
return $this->reasonData['packageName'];
}
if ($this->getReason() === self::RULE_PACKAGE_REQUIRES) {
if ($reason === self::RULE_FIXED) {
return $this->reasonData['package']->getName();
}
if ($reason === self::RULE_PACKAGE_REQUIRES) {
return $this->reasonData->getTarget();
}
}
@ -126,7 +123,12 @@ abstract class Rule
abstract public function isAssertion();
public function getPrettyString(Pool $pool, array $installedMap = array())
public function isCausedByLock()
{
return $this->getReason() === self::RULE_FIXED && $this->reasonData['lockable'];
}
public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array())
{
$literals = $this->getLiterals();
@ -142,17 +144,30 @@ abstract class Rule
case self::RULE_INTERNAL_ALLOW_UPDATE:
return $ruleText;
case self::RULE_JOB_INSTALL:
return "Install command rule ($ruleText)";
case self::RULE_ROOT_REQUIRE:
$packageName = $this->reasonData['packageName'];
$constraint = $this->reasonData['constraint'];
case self::RULE_JOB_REMOVE:
return "Remove command rule ($ruleText)";
$packages = $pool->whatProvides($packageName, $constraint);
if (!$packages) {
return 'No package found to satisfy root composer.json require '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '');
}
return 'Root composer.json requires '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '').' -> satisfiable by '.$this->formatPackagesUnique($pool, $packages).'.';
case self::RULE_FIXED:
$package = $this->reasonData['package'];
if ($this->reasonData['lockable']) {
return $package->getPrettyName().' is locked to version '.$package->getPrettyVersion().' and an update of this package was not requested.';
}
return $package->getPrettyName().' is present at version '.$package->getPrettyVersion() . ' and cannot be modified by Composer';
case self::RULE_PACKAGE_CONFLICT:
$package1 = $pool->literalToPackage($literals[0]);
$package2 = $pool->literalToPackage($literals[1]);
return $package1->getPrettyString().' conflicts with '.$this->formatPackagesUnique($pool, array($package2)).'.';
return $package2->getPrettyString().' conflicts with '.$package1->getPrettyString().'.';
case self::RULE_PACKAGE_REQUIRES:
$sourceLiteral = array_shift($literals);
@ -169,73 +184,103 @@ abstract class Rule
} else {
$targetName = $this->reasonData->getTarget();
if ($targetName === 'php' || $targetName === 'php-64bit' || $targetName === 'hhvm') {
// handle php/hhvm
if (defined('HHVM_VERSION')) {
return $text . ' -> your HHVM version does not satisfy that requirement.';
}
$reason = Problem::getMissingPackageReason($repositorySet, $request, $pool, $targetName, $this->reasonData->getConstraint());
$packages = $pool->whatProvides($targetName);
$package = count($packages) ? current($packages) : phpversion();
if ($targetName === 'hhvm') {
if ($package instanceof CompletePackage) {
return $text . ' -> your HHVM version ('.$package->getPrettyVersion().') does not satisfy that requirement.';
} else {
return $text . ' -> you are running this with PHP and not HHVM.';
}
}
if (!($package instanceof CompletePackage)) {
return $text . ' -> your PHP version ('.phpversion().') does not satisfy that requirement.';
}
$extra = $package->getExtra();
if (!empty($extra['config.platform'])) {
$text .= ' -> your PHP version ('.phpversion().') overridden by "config.platform.php" version ('.$package->getPrettyVersion().') does not satisfy that requirement.';
} else {
$text .= ' -> your PHP version ('.$package->getPrettyVersion().') does not satisfy that requirement.';
}
return $text;
}
if (0 === strpos($targetName, 'ext-')) {
// handle php extensions
$ext = substr($targetName, 4);
$error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system';
return $text . ' -> the requested PHP extension '.$ext.' '.$error.'.';
}
if (0 === strpos($targetName, 'lib-')) {
// handle linked libs
$lib = substr($targetName, 4);
return $text . ' -> the requested linked library '.$lib.' has the wrong version installed or is missing from your system, make sure to have the extension providing it.';
}
if ($providers = $pool->whatProvides($targetName, $this->reasonData->getConstraint(), true, true)) {
return $text . ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $providers) .' but these conflict with your requirements or minimum-stability.';
}
return $text . ' -> no matching package found.';
return $text . ' -> ' . $reason[1];
}
return $text;
case self::RULE_PACKAGE_OBSOLETES:
if (count($literals) === 2 && $literals[0] < 0 && $literals[1] < 0) {
$package1 = $pool->literalToPackage($literals[0]);
$package2 = $pool->literalToPackage($literals[1]);
$replaces1 = $this->getReplacedNames($package1);
$replaces2 = $this->getReplacedNames($package2);
$reason = null;
if ($conflictingNames = array_values(array_intersect($replaces1, $replaces2))) {
$reason = 'They both replace '.(count($conflictingNames) > 1 ? '['.implode(', ', $conflictingNames).']' : $conflictingNames[0]).' and thus cannot coexist.';
} elseif (in_array($package1->getName(), $replaces2, true)) {
$reason = $package2->getName().' replaces '.$package1->getName().' and thus cannot coexist with it.';
} elseif (in_array($package2->getName(), $replaces1, true)) {
$reason = $package1->getName().' replaces '.$package2->getName().' and thus cannot coexist with it.';
}
if ($reason) {
if (isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) {
// swap vars so the if below passes
$tmp = $package2;
$package2 = $package1;
$package1 = $tmp;
}
if (!isset($installedMap[$package1->id]) && isset($installedMap[$package2->id])) {
return $package1->getPrettyString().' cannot be installed as that would require removing '.$package2->getPrettyString().'. '.$reason;
}
if (!isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) {
return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'. '.$reason;
}
}
return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'.';
}
return $ruleText;
case self::RULE_INSTALLED_PACKAGE_OBSOLETES:
return $ruleText;
case self::RULE_PACKAGE_SAME_NAME:
return 'Can only install one of: ' . $this->formatPackagesUnique($pool, $literals) . '.';
$replacedNames = null;
$packageNames = array();
foreach ($literals as $literal) {
$package = $pool->literalToPackage($literal);
$pkgReplaces = $this->getReplacedNames($package);
if ($pkgReplaces) {
if ($replacedNames === null) {
$replacedNames = $this->getReplacedNames($package);
} else {
$replacedNames = array_intersect($replacedNames, $this->getReplacedNames($package));
}
}
$packageNames[$package->getName()] = true;
}
if ($replacedNames) {
$replacedNames = array_values(array_intersect(array_keys($packageNames), $replacedNames));
}
if ($replacedNames && count($packageNames) > 1) {
$replacer = null;
foreach ($literals as $literal) {
$package = $pool->literalToPackage($literal);
if (array_intersect($replacedNames, $this->getReplacedNames($package))) {
$replacer = $package;
break;
}
}
$replacedNames = count($replacedNames) > 1 ? '['.implode(', ', $replacedNames).']' : $replacedNames[0];
if ($replacer) {
return 'Only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '. '.$replacer->getName().' replaces '.$replacedNames.' and thus cannot coexist with it.';
}
}
return 'You can only install one version of a package, so only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '.';
case self::RULE_PACKAGE_IMPLICIT_OBSOLETES:
return $ruleText;
case self::RULE_LEARNED:
return 'Conclusion: '.$ruleText;
if (isset($learnedPool[$this->reasonData])) {
$learnedString = ', learned rules:'."\n - ";
$reasons = array();
foreach ($learnedPool[$this->reasonData] as $learnedRule) {
$reasons[] = $learnedRule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool);
}
$learnedString .= implode("\n - ", array_unique($reasons));
} else {
$learnedString = ' (reasoning unavailable)';
}
return 'Conclusion: '.$ruleText.$learnedString;
case self::RULE_PACKAGE_ALIAS:
return $ruleText;
default:
@ -252,17 +297,22 @@ abstract class Rule
protected function formatPackagesUnique($pool, array $packages)
{
$prepared = array();
foreach ($packages as $package) {
foreach ($packages as $index => $package) {
if (!is_object($package)) {
$package = $pool->literalToPackage($package);
$packages[$index] = $pool->literalToPackage($package);
}
$prepared[$package->getName()]['name'] = $package->getPrettyName();
$prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion();
}
foreach ($prepared as $name => $package) {
$prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']';
}
return implode(', ', $prepared);
return Problem::getPackageList($packages);
}
private function getReplacedNames(PackageInterface $package)
{
$names = array();
foreach ($package->getReplaces() as $link) {
$names[] = $link->getTarget();
}
return $names;
}
}

View File

@ -28,11 +28,10 @@ class Rule2Literals extends Rule
* @param int $literal2
* @param int $reason A RULE_* constant describing the reason for generating this rule
* @param Link|PackageInterface $reasonData
* @param array $job The job this rule was created from
*/
public function __construct($literal1, $literal2, $reason, $reasonData, $job = null)
public function __construct($literal1, $literal2, $reason, $reasonData)
{
parent::__construct($reason, $reasonData, $job);
parent::__construct($reason, $reasonData);
if ($literal1 < $literal2) {
$this->literal1 = $literal1;

View File

@ -12,6 +12,8 @@
namespace Composer\DependencyResolver;
use Composer\Repository\RepositorySet;
/**
* @author Nils Adermann <naderman@naderman.de>
*/
@ -19,7 +21,7 @@ class RuleSet implements \IteratorAggregate, \Countable
{
// highest priority => lowest number
const TYPE_PACKAGE = 0;
const TYPE_JOB = 1;
const TYPE_REQUEST = 1;
const TYPE_LEARNED = 4;
/**
@ -32,7 +34,7 @@ class RuleSet implements \IteratorAggregate, \Countable
protected static $types = array(
255 => 'UNKNOWN',
self::TYPE_PACKAGE => 'PACKAGE',
self::TYPE_JOB => 'JOB',
self::TYPE_REQUEST => 'REQUEST',
self::TYPE_LEARNED => 'LEARNED',
);
@ -155,13 +157,13 @@ class RuleSet implements \IteratorAggregate, \Countable
return array_keys($types);
}
public function getPrettyString(Pool $pool = null)
public function getPrettyString(RepositorySet $repositorySet = null, Request $request = null, Pool $pool = null)
{
$string = "\n";
foreach ($this->rules as $type => $rules) {
$string .= str_pad(self::$types[$type], 8, ' ') . ": ";
foreach ($rules as $rule) {
$string .= ($pool ? $rule->getPrettyString($pool) : $rule)."\n";
$string .= ($repositorySet && $request && $pool ? $rule->getPrettyString($repositorySet, $request, $pool) : $rule)."\n";
}
$string .= "\n\n";
}
@ -171,6 +173,6 @@ class RuleSet implements \IteratorAggregate, \Countable
public function __toString()
{
return $this->getPrettyString(null);
return $this->getPrettyString(null, null, null);
}
}

View File

@ -12,9 +12,11 @@
namespace Composer\DependencyResolver;
use Composer\Package\LinkConstraint\VersionConstraint;
use Composer\Package\PackageInterface;
use Composer\Package\AliasPackage;
use Composer\Repository\PlatformRepository;
use Composer\Semver\Constraint\Constraint;
/**
* @author Nils Adermann <naderman@naderman.de>
@ -24,13 +26,11 @@ class RuleSetGenerator
protected $policy;
protected $pool;
protected $rules;
protected $jobs;
protected $installedMap;
protected $whitelistedMap;
protected $addedMap;
protected $conflictAddedMap;
protected $addedPackages;
protected $addedPackagesByNames;
protected $conflictsForName;
public function __construct(PolicyInterface $policy, Pool $pool)
{
@ -76,33 +76,17 @@ class RuleSetGenerator
* @param array $packages The set of packages to choose from
* @param int $reason A RULE_* constant describing the reason for
* generating this rule
* @param array $job The job this rule was created from
* @param array $reasonData Additional data like the root require or fix request info
* @return Rule The generated rule
*/
protected function createInstallOneOfRule(array $packages, $reason, $job)
protected function createInstallOneOfRule(array $packages, $reason, $reasonData)
{
$literals = array();
foreach ($packages as $package) {
$literals[] = $package->id;
}
return new GenericRule($literals, $reason, $job['packageName'], $job);
}
/**
* Creates a rule to remove a package
*
* The rule for a package A is (-A).
*
* @param PackageInterface $package The package to be removed
* @param int $reason A RULE_* constant describing the
* reason for generating this rule
* @param array $job The job this rule was created from
* @return Rule The generated rule
*/
protected function createRemoveRule(PackageInterface $package, $reason, $job)
{
return new GenericRule(array(-$package->id), $reason, $job['packageName'], $job);
return new GenericRule($literals, $reason, $reasonData);
}
/**
@ -129,6 +113,20 @@ class RuleSetGenerator
return new Rule2Literals(-$issuer->id, -$provider->id, $reason, $reasonData);
}
protected function createMultiConflictRule(array $packages, $reason, $reasonData = null)
{
$literals = array();
foreach ($packages as $package) {
$literals[] = -$package->id;
}
if (count($literals) == 2) {
return new Rule2Literals($literals[0], $literals[1], $reason, $reasonData);
}
return new MultiConflictRule($literals, $reason, $reasonData);
}
/**
* Adds a rule unless it duplicates an existing one of any type
*
@ -147,41 +145,6 @@ class RuleSetGenerator
$this->rules->add($newRule, $type);
}
protected function whitelistFromPackage(PackageInterface $package)
{
$workQueue = new \SplQueue;
$workQueue->enqueue($package);
while (!$workQueue->isEmpty()) {
$package = $workQueue->dequeue();
if (isset($this->whitelistedMap[$package->id])) {
continue;
}
$this->whitelistedMap[$package->id] = true;
foreach ($package->getRequires() as $link) {
$possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint(), true);
foreach ($possibleRequires as $require) {
$workQueue->enqueue($require);
}
}
$obsoleteProviders = $this->pool->whatProvides($package->getName(), null, true);
foreach ($obsoleteProviders as $provider) {
if ($provider === $package) {
continue;
}
if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
$workQueue->enqueue($provider);
}
}
}
}
protected function addRulesForPackage(PackageInterface $package, $ignorePlatformReqs)
{
$workQueue = new \SplQueue;
@ -225,9 +188,16 @@ class RuleSetGenerator
if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
$this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, $package));
} elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) {
$reason = ($packageName == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES;
$this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $provider, $reason, $package));
} else {
if (!isset($this->conflictsForName[$packageName])) {
$this->conflictsForName[$packageName] = array();
}
if (!$package instanceof AliasPackage) {
$this->conflictsForName[$packageName][$package->id] = $package;
}
if (!$provider instanceof AliasPackage) {
$this->conflictsForName[$packageName][$provider->id] = $provider;
}
}
}
}
@ -248,7 +218,7 @@ class RuleSetGenerator
/** @var PackageInterface $possibleConflict */
foreach ($this->addedPackagesByNames[$link->getTarget()] as $possibleConflict) {
$conflictMatch = $this->pool->match($possibleConflict, $link->getTarget(), $link->getConstraint(), true);
$conflictMatch = $this->pool->match($possibleConflict, $link->getTarget(), $link->getConstraint());
if ($conflictMatch === Pool::MATCH || $conflictMatch === Pool::MATCH_REPLACE) {
$this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $possibleConflict, Rule::RULE_PACKAGE_CONFLICT, $link));
@ -258,8 +228,6 @@ class RuleSetGenerator
}
// check obsoletes and implicit obsoletes of a package
$isInstalled = isset($this->installedMap[$package->id]);
foreach ($package->getReplaces() as $link) {
if (!isset($this->addedPackagesByNames[$link->getTarget()])) {
continue;
@ -272,12 +240,19 @@ class RuleSetGenerator
}
if (!$this->obsoleteImpossibleForAlias($package, $provider)) {
$reason = $isInstalled ? Rule::RULE_INSTALLED_PACKAGE_OBSOLETES : Rule::RULE_PACKAGE_OBSOLETES;
$reason = Rule::RULE_PACKAGE_OBSOLETES;
$this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $provider, $reason, $link));
}
}
}
}
foreach ($this->conflictsForName as $name => $packages) {
if (count($packages) > 1) {
$reason = Rule::RULE_PACKAGE_SAME_NAME;
$this->addRule(RuleSet::TYPE_PACKAGE, $this->createMultiConflictRule($packages, $reason, null));
}
}
}
protected function obsoleteImpossibleForAlias($package, $provider)
@ -294,77 +269,61 @@ class RuleSetGenerator
return $impossible;
}
protected function whitelistFromJobs()
protected function addRulesForRequest(Request $request, $ignorePlatformReqs)
{
foreach ($this->jobs as $job) {
switch ($job['cmd']) {
case 'install':
$packages = $this->pool->whatProvides($job['packageName'], $job['constraint'], true);
foreach ($packages as $package) {
$this->whitelistFromPackage($package);
}
break;
$unlockableMap = $request->getUnlockableMap();
foreach ($request->getFixedPackages() as $package) {
if ($package->id == -1) {
// fixed package was not added to the pool as it did not pass the stability requirements, this is fine
if ($this->pool->isUnacceptableFixedPackage($package)) {
continue;
}
// otherwise, looks like a bug
throw new \LogicException("Fixed package ".$package->getName()." ".$package->getVersion().($package instanceof AliasPackage ? " (alias)" : "")." was not added to solver pool.");
}
$this->addRulesForPackage($package, $ignorePlatformReqs);
$rule = $this->createInstallOneOfRule(array($package), Rule::RULE_FIXED, array(
'package' => $package,
'lockable' => !isset($unlockableMap[$package->id]),
));
$this->addRule(RuleSet::TYPE_REQUEST, $rule);
}
foreach ($request->getRequires() as $packageName => $constraint) {
if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $packageName)) {
continue;
}
$packages = $this->pool->whatProvides($packageName, $constraint);
if ($packages) {
foreach ($packages as $package) {
$this->addRulesForPackage($package, $ignorePlatformReqs);
}
$rule = $this->createInstallOneOfRule($packages, Rule::RULE_ROOT_REQUIRE, array(
'packageName' => $packageName,
'constraint' => $constraint,
));
$this->addRule(RuleSet::TYPE_REQUEST, $rule);
}
}
}
protected function addRulesForJobs($ignorePlatformReqs)
public function getRulesFor(Request $request, $ignorePlatformReqs = false)
{
foreach ($this->jobs as $job) {
switch ($job['cmd']) {
case 'install':
if (!$job['fixed'] && $ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) {
break;
}
$packages = $this->pool->whatProvides($job['packageName'], $job['constraint']);
if ($packages) {
foreach ($packages as $package) {
if (!isset($this->installedMap[$package->id])) {
$this->addRulesForPackage($package, $ignorePlatformReqs);
}
}
$rule = $this->createInstallOneOfRule($packages, Rule::RULE_JOB_INSTALL, $job);
$this->addRule(RuleSet::TYPE_JOB, $rule);
}
break;
case 'remove':
// remove all packages with this name including uninstalled
// ones to make sure none of them are picked as replacements
$packages = $this->pool->whatProvides($job['packageName'], $job['constraint']);
foreach ($packages as $package) {
$rule = $this->createRemoveRule($package, Rule::RULE_JOB_REMOVE, $job);
$this->addRule(RuleSet::TYPE_JOB, $rule);
}
break;
}
}
}
public function getRulesFor($jobs, $installedMap, $ignorePlatformReqs = false)
{
$this->jobs = $jobs;
$this->rules = new RuleSet;
$this->installedMap = $installedMap;
$this->whitelistedMap = array();
foreach ($this->installedMap as $package) {
$this->whitelistFromPackage($package);
}
$this->whitelistFromJobs();
$this->pool->setWhitelist($this->whitelistedMap);
$this->addedMap = array();
$this->conflictAddedMap = array();
$this->addedPackages = array();
$this->addedPackagesByNames = array();
foreach ($this->installedMap as $package) {
$this->addRulesForPackage($package, $ignorePlatformReqs);
}
$this->conflictsForName = array();
$this->addRulesForJobs($ignorePlatformReqs);
$this->addRulesForRequest($request, $ignorePlatformReqs);
$this->addConflictRules($ignorePlatformReqs);

View File

@ -44,13 +44,24 @@ class RuleWatchGraph
return;
}
foreach (array($node->watch1, $node->watch2) as $literal) {
if (!isset($this->watchChains[$literal])) {
$this->watchChains[$literal] = new RuleWatchChain;
}
if (!$node->getRule() instanceof MultiConflictRule) {
foreach (array($node->watch1, $node->watch2) as $literal) {
if (!isset($this->watchChains[$literal])) {
$this->watchChains[$literal] = new RuleWatchChain;
}
$this->watchChains[$literal]->unshift($node);
$this->watchChains[$literal]->unshift($node);
}
} else {
foreach ($node->getRule()->getLiterals() as $literal) {
if (!isset($this->watchChains[$literal])) {
$this->watchChains[$literal] = new RuleWatchChain;
}
$this->watchChains[$literal]->unshift($node);
}
}
}
/**
@ -92,28 +103,40 @@ class RuleWatchGraph
$chain->rewind();
while ($chain->valid()) {
$node = $chain->current();
$otherWatch = $node->getOtherWatch($literal);
if (!$node->getRule() instanceof MultiConflictRule) {
$otherWatch = $node->getOtherWatch($literal);
if (!$node->getRule()->isDisabled() && !$decisions->satisfy($otherWatch)) {
$ruleLiterals = $node->getRule()->getLiterals();
if (!$node->getRule()->isDisabled() && !$decisions->satisfy($otherWatch)) {
$ruleLiterals = $node->getRule()->getLiterals();
$alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $decisions) {
return $literal !== $ruleLiteral &&
$otherWatch !== $ruleLiteral &&
!$decisions->conflict($ruleLiteral);
});
$alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $decisions) {
return $literal !== $ruleLiteral &&
$otherWatch !== $ruleLiteral &&
!$decisions->conflict($ruleLiteral);
});
if ($alternativeLiterals) {
reset($alternativeLiterals);
$this->moveWatch($literal, current($alternativeLiterals), $node);
continue;
if ($alternativeLiterals) {
reset($alternativeLiterals);
$this->moveWatch($literal, current($alternativeLiterals), $node);
continue;
}
if ($decisions->conflict($otherWatch)) {
return $node->getRule();
}
$decisions->decide($otherWatch, $level, $node->getRule());
}
} else {
foreach ($node->getRule()->getLiterals() as $otherLiteral) {
if ($literal !== $otherLiteral && !$decisions->satisfy($otherLiteral)) {
if ($decisions->conflict($otherLiteral)) {
return $node->getRule();
}
if ($decisions->conflict($otherWatch)) {
return $node->getRule();
$decisions->decide($otherLiteral, $level, $node->getRule());
}
}
$decisions->decide($otherWatch, $level, $node->getRule());
}
$chain->next();

View File

@ -55,7 +55,7 @@ class RuleWatchNode
$literals = $this->rule->getLiterals();
// if there are only 2 elements, both are being watched anyway
if (count($literals) < 3) {
if (count($literals) < 3 || $this->rule instanceof MultiConflictRule) {
return;
}

View File

@ -13,7 +13,7 @@
namespace Composer\DependencyResolver;
use Composer\IO\IOInterface;
use Composer\Repository\RepositoryInterface;
use Composer\Package\PackageInterface;
use Composer\Repository\PlatformRepository;
/**
@ -28,23 +28,18 @@ class Solver
protected $policy;
/** @var Pool */
protected $pool;
/** @var RepositoryInterface */
protected $installed;
/** @var RuleSet */
protected $rules;
/** @var RuleSetGenerator */
protected $ruleSetGenerator;
/** @var array */
protected $jobs;
/** @var int[] */
protected $updateMap = array();
/** @var RuleWatchGraph */
protected $watchGraph;
/** @var Decisions */
protected $decisions;
/** @var int[] */
protected $installedMap;
/** @var PackageInterface[] */
protected $fixedMap;
/** @var int */
protected $propagateIndex;
@ -66,16 +61,13 @@ class Solver
/**
* @param PolicyInterface $policy
* @param Pool $pool
* @param RepositoryInterface $installed
* @param IOInterface $io
*/
public function __construct(PolicyInterface $policy, Pool $pool, RepositoryInterface $installed, IOInterface $io)
public function __construct(PolicyInterface $policy, Pool $pool, IOInterface $io)
{
$this->io = $io;
$this->policy = $policy;
$this->pool = $pool;
$this->installed = $installed;
$this->ruleSetGenerator = new RuleSetGenerator($policy, $pool);
}
/**
@ -86,6 +78,11 @@ class Solver
return count($this->rules);
}
public function getPool()
{
return $this->pool;
}
// aka solver_makeruledecisions
private function makeAssertionRuleDecisions()
@ -121,23 +118,23 @@ class Solver
$conflict = $this->decisions->decisionRule($literal);
if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) {
$problem = new Problem($this->pool);
$problem = new Problem();
$problem->addRule($rule);
$problem->addRule($conflict);
$this->disableProblem($rule);
$rule->disable();
$this->problems[] = $problem;
continue;
}
// conflict with another job
$problem = new Problem($this->pool);
// conflict with another root require/fixed package
$problem = new Problem();
$problem->addRule($rule);
$problem->addRule($conflict);
// push all of our rules (can only be job rules)
// push all of our rules (can only be root require/fixed package rules)
// asserting this literal on the problem stack
foreach ($this->rules->getIteratorFor(RuleSet::TYPE_JOB) as $assertRule) {
foreach ($this->rules->getIteratorFor(RuleSet::TYPE_REQUEST) as $assertRule) {
if ($assertRule->isDisabled() || !$assertRule->isAssertion()) {
continue;
}
@ -148,9 +145,8 @@ class Solver
if (abs($literal) !== abs($assertRuleLiteral)) {
continue;
}
$problem->addRule($assertRule);
$this->disableProblem($assertRule);
$assertRule->disable();
}
$this->problems[] = $problem;
@ -159,47 +155,29 @@ class Solver
}
}
protected function setupInstalledMap()
protected function setupFixedMap(Request $request)
{
$this->installedMap = array();
foreach ($this->installed->getPackages() as $package) {
$this->installedMap[$package->id] = $package;
$this->fixedMap = array();
foreach ($request->getFixedPackages() as $package) {
$this->fixedMap[$package->id] = $package;
}
}
/**
* @param Request $request
* @param bool $ignorePlatformReqs
*/
protected function checkForRootRequireProblems($ignorePlatformReqs)
protected function checkForRootRequireProblems($request, $ignorePlatformReqs)
{
foreach ($this->jobs as $job) {
switch ($job['cmd']) {
case 'update':
$packages = $this->pool->whatProvides($job['packageName'], $job['constraint']);
foreach ($packages as $package) {
if (isset($this->installedMap[$package->id])) {
$this->updateMap[$package->id] = true;
}
}
break;
foreach ($request->getRequires() as $packageName => $constraint) {
if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $packageName)) {
continue;
}
case 'update-all':
foreach ($this->installedMap as $package) {
$this->updateMap[$package->id] = true;
}
break;
case 'install':
if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) {
break;
}
if (!$this->pool->whatProvides($job['packageName'], $job['constraint'])) {
$problem = new Problem($this->pool);
$problem->addRule(new GenericRule(array(), null, null, $job));
$this->problems[] = $problem;
}
break;
if (!$this->pool->whatProvides($packageName, $constraint)) {
$problem = new Problem();
$problem->addRule(new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, array('packageName' => $packageName, 'constraint' => $constraint)));
$this->problems[] = $problem;
}
}
}
@ -207,15 +185,16 @@ class Solver
/**
* @param Request $request
* @param bool $ignorePlatformReqs
* @return array
* @return LockTransaction
*/
public function solve(Request $request, $ignorePlatformReqs = false)
{
$this->jobs = $request->getJobs();
$this->setupFixedMap($request);
$this->setupInstalledMap();
$this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap, $ignorePlatformReqs);
$this->checkForRootRequireProblems($ignorePlatformReqs);
$this->io->writeError('Generating rules', true, IOInterface::DEBUG);
$this->ruleSetGenerator = new RuleSetGenerator($this->policy, $this->pool);
$this->rules = $this->ruleSetGenerator->getRulesFor($request, $ignorePlatformReqs);
$this->checkForRootRequireProblems($request, $ignorePlatformReqs);
$this->decisions = new Decisions($this->pool);
$this->watchGraph = new RuleWatchGraph;
@ -223,29 +202,20 @@ class Solver
$this->watchGraph->insert(new RuleWatchNode($rule));
}
/* make decisions based on job/update assertions */
/* make decisions based on root require/fix assertions */
$this->makeAssertionRuleDecisions();
$this->io->writeError('Resolving dependencies through SAT', true, IOInterface::DEBUG);
$before = microtime(true);
$this->runSat(true);
$this->runSat();
$this->io->writeError('', true, IOInterface::DEBUG);
$this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE);
// decide to remove everything that's installed and undecided
foreach ($this->installedMap as $packageId => $void) {
if ($this->decisions->undecided($packageId)) {
$this->decisions->decide(-$packageId, 1, null);
}
}
if ($this->problems) {
throw new SolverProblemsException($this->problems, $this->installedMap);
throw new SolverProblemsException($this->problems, $this->learnedPool);
}
$transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions);
return $transaction->getOperations();
return new LockTransaction($this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions);
}
/**
@ -322,11 +292,10 @@ class Solver
*
* @param int $level
* @param string|int $literal
* @param bool $disableRules
* @param Rule $rule
* @return int
*/
private function setPropagateLearn($level, $literal, $disableRules, Rule $rule)
private function setPropagateLearn($level, $literal, Rule $rule)
{
$level++;
@ -340,7 +309,7 @@ class Solver
}
if ($level == 1) {
return $this->analyzeUnsolvable($rule, $disableRules);
return $this->analyzeUnsolvable($rule);
}
// conflict
@ -377,14 +346,13 @@ class Solver
/**
* @param int $level
* @param array $decisionQueue
* @param bool $disableRules
* @param Rule $rule
* @return int
*/
private function selectAndInstall($level, array $decisionQueue, $disableRules, Rule $rule)
private function selectAndInstall($level, array $decisionQueue, Rule $rule)
{
// choose best package to install from decisionQueue
$literals = $this->policy->selectPreferredPackages($this->pool, $this->installedMap, $decisionQueue, $rule->getRequiredPackage());
$literals = $this->policy->selectPreferredPackages($this->pool, $decisionQueue, $rule->getRequiredPackage());
$selectedLiteral = array_shift($literals);
@ -393,7 +361,7 @@ class Solver
$this->branches[] = array($literals, $level);
}
return $this->setPropagateLearn($level, $selectedLiteral, $disableRules, $rule);
return $this->setPropagateLearn($level, $selectedLiteral, $rule);
}
/**
@ -539,12 +507,11 @@ class Solver
/**
* @param Rule $conflictRule
* @param bool $disableRules
* @return int
*/
private function analyzeUnsolvable(Rule $conflictRule, $disableRules)
private function analyzeUnsolvable(Rule $conflictRule)
{
$problem = new Problem($this->pool);
$problem = new Problem();
$problem->addRule($conflictRule);
$this->analyzeUnsolvableRule($problem, $conflictRule);
@ -586,41 +553,9 @@ class Solver
}
}
if ($disableRules) {
foreach ($this->problems[count($this->problems) - 1] as $reason) {
$this->disableProblem($reason['rule']);
}
$this->resetSolver();
return 1;
}
return 0;
}
/**
* @param Rule $why
*/
private function disableProblem(Rule $why)
{
$job = $why->getJob();
if (!$job) {
$why->disable();
return;
}
// disable all rules of this job
foreach ($this->rules as $rule) {
/** @var Rule $rule */
if ($job === $rule->getJob()) {
$rule->disable();
}
}
}
private function resetSolver()
{
$this->decisions->reset();
@ -661,17 +596,14 @@ class Solver
}
}
/**
* @param bool $disableRules
*/
private function runSat($disableRules = true)
private function runSat()
{
$this->propagateIndex = 0;
/*
* here's the main loop:
* 1) propagate new decisions (only needed once)
* 2) fulfill jobs
* 2) fulfill root requires/fixed packages
* 3) fulfill all unresolved rules
* 4) minimalize solution if we had choices
* if we encounter a problem, we rewind to a safe level and restart
@ -679,10 +611,7 @@ class Solver
*/
$decisionQueue = array();
/**
* @todo this makes $disableRules always false; determine the rationale and possibly remove dead code?
*/
$disableRules = array();
$decisionSupplementQueue = array();
$level = 1;
$systemLevel = $level + 1;
@ -691,7 +620,7 @@ class Solver
if (1 === $level) {
$conflictRule = $this->propagate($level);
if (null !== $conflictRule) {
if ($this->analyzeUnsolvable($conflictRule, $disableRules)) {
if ($this->analyzeUnsolvable($conflictRule)) {
continue;
}
@ -699,9 +628,9 @@ class Solver
}
}
// handle job rules
// handle root require/fixed package rules
if ($level < $systemLevel) {
$iterator = $this->rules->getIteratorFor(RuleSet::TYPE_JOB);
$iterator = $this->rules->getIteratorFor(RuleSet::TYPE_REQUEST);
foreach ($iterator as $rule) {
if ($rule->isEnabled()) {
$decisionQueue = array();
@ -718,26 +647,21 @@ class Solver
}
if ($noneSatisfied && count($decisionQueue)) {
// prune all update packages until installed version
// except for requested updates
if (count($this->installed) != count($this->updateMap)) {
$prunedQueue = array();
foreach ($decisionQueue as $literal) {
if (isset($this->installedMap[abs($literal)])) {
$prunedQueue[] = $literal;
if (isset($this->updateMap[abs($literal)])) {
$prunedQueue = $decisionQueue;
break;
}
}
// if any of the options in the decision queue are fixed, only use those
$prunedQueue = array();
foreach ($decisionQueue as $literal) {
if (isset($this->fixedMap[abs($literal)])) {
$prunedQueue[] = $literal;
}
}
if (!empty($prunedQueue)) {
$decisionQueue = $prunedQueue;
}
}
if ($noneSatisfied && count($decisionQueue)) {
$oLevel = $level;
$level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule);
$level = $this->selectAndInstall($level, $decisionQueue, $rule);
if (0 === $level) {
return;
@ -751,7 +675,7 @@ class Solver
$systemLevel = $level + 1;
// jobs left
// root requires/fixed packages left
$iterator->next();
if ($iterator->valid()) {
continue;
@ -813,7 +737,7 @@ class Solver
continue;
}
$level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule);
$level = $this->selectAndInstall($level, $decisionQueue, $rule);
if (0 === $level) {
return;
@ -856,7 +780,7 @@ class Solver
$why = $this->decisions->lastReason();
$level = $this->setPropagateLearn($level, $lastLiteral, $disableRules, $why);
$level = $this->setPropagateLearn($level, $lastLiteral, $why);
if ($level == 0) {
return;

View File

@ -13,6 +13,7 @@
namespace Composer\DependencyResolver;
use Composer\Util\IniHelper;
use Composer\Repository\RepositorySet;
/**
* @author Nils Adermann <naderman@naderman.de>
@ -20,29 +21,33 @@ use Composer\Util\IniHelper;
class SolverProblemsException extends \RuntimeException
{
protected $problems;
protected $installedMap;
protected $learnedPool;
public function __construct(array $problems, array $installedMap)
public function __construct(array $problems, array $learnedPool)
{
$this->problems = $problems;
$this->installedMap = $installedMap;
$this->learnedPool = $learnedPool;
parent::__construct($this->createMessage(), 2);
parent::__construct('Failed resolving dependencies with '.count($problems).' problems, call getPrettyString to get formatted details', 2);
}
protected function createMessage()
public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isDevExtraction = false)
{
$installedMap = $request->getPresentMap(true);
$text = "\n";
$hasExtensionProblems = false;
$isCausedByLock = false;
foreach ($this->problems as $i => $problem) {
$text .= " Problem ".($i + 1).$problem->getPrettyString($this->installedMap)."\n";
$text .= " Problem ".($i + 1).$problem->getPrettyString($repositorySet, $request, $pool, $installedMap, $this->learnedPool)."\n";
if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) {
$hasExtensionProblems = true;
}
$isCausedByLock |= $problem->isCausedByLock();
}
if (strpos($text, 'could not be found') || strpos($text, 'no matching package found')) {
if (!$isDevExtraction && (strpos($text, 'could not be found') || strpos($text, 'no matching package found'))) {
$text .= "\nPotential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details.\n - It's a private package and you forgot to add a custom repository to find it\n\nRead <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.";
}
@ -50,6 +55,10 @@ class SolverProblemsException extends \RuntimeException
$text .= $this->createExtensionHint();
}
if ($isCausedByLock && !$isDevExtraction) {
$text .= "\nUse the option --with-all-dependencies to allow updates and removals for packages currently locked to specific versions.";
}
return $text;
}
@ -76,8 +85,8 @@ class SolverProblemsException extends \RuntimeException
private function hasExtensionProblems(array $reasonSets)
{
foreach ($reasonSets as $reasonSet) {
foreach ($reasonSet as $reason) {
if (isset($reason["rule"]) && 0 === strpos($reason["rule"]->getRequiredPackage(), 'ext-')) {
foreach ($reasonSet as $rule) {
if (0 === strpos($rule->getRequiredPackage(), 'ext-')) {
return true;
}
}

View File

@ -13,161 +13,205 @@
namespace Composer\DependencyResolver;
use Composer\Package\AliasPackage;
use Composer\Package\Link;
use Composer\Package\PackageInterface;
use Composer\Repository\PlatformRepository;
/**
* @author Nils Adermann <naderman@naderman.de>
*/
class Transaction
{
protected $policy;
protected $pool;
protected $installedMap;
protected $decisions;
protected $transaction;
/**
* @var array
*/
protected $operations;
public function __construct($policy, $pool, $installedMap, $decisions)
/**
* Packages present at the beginning of the transaction
* @var array
*/
protected $presentPackages;
/**
* Package set resulting from this transaction
* @var array
*/
protected $resultPackageMap;
/**
* @var array
*/
protected $resultPackagesByName = array();
public function __construct($presentPackages, $resultPackages)
{
$this->policy = $policy;
$this->pool = $pool;
$this->installedMap = $installedMap;
$this->decisions = $decisions;
$this->transaction = array();
$this->presentPackages = $presentPackages;
$this->setResultPackageMaps($resultPackages);
$this->operations = $this->calculateOperations();
}
public function getOperations()
{
$installMeansUpdateMap = $this->findUpdates();
return $this->operations;
}
$updateMap = array();
$installMap = array();
$uninstallMap = array();
private function setResultPackageMaps($resultPackages)
{
$packageSort = function (PackageInterface $a, PackageInterface $b) {
// sort alias packages by the same name behind their non alias version
if ($a->getName() == $b->getName() && $a instanceof AliasPackage != $b instanceof AliasPackage) {
return $a instanceof AliasPackage ? -1 : 1;
}
return strcmp($b->getName(), $a->getName());
};
foreach ($this->decisions as $i => $decision) {
$literal = $decision[Decisions::DECISION_LITERAL];
$reason = $decision[Decisions::DECISION_REASON];
$this->resultPackageMap = array();
foreach ($resultPackages as $package) {
$this->resultPackageMap[spl_object_hash($package)] = $package;
foreach ($package->getNames() as $name) {
$this->resultPackagesByName[$name][] = $package;
}
}
$package = $this->pool->literalToPackage($literal);
uasort($this->resultPackageMap, $packageSort);
foreach ($this->resultPackagesByName as $name => $packages) {
uasort($this->resultPackagesByName[$name], $packageSort);
}
}
// wanted & installed || !wanted & !installed
if (($literal > 0) == isset($this->installedMap[$package->id])) {
protected function calculateOperations()
{
$operations = array();
$presentPackageMap = array();
$removeMap = array();
$presentAliasMap = array();
$removeAliasMap = array();
foreach ($this->presentPackages as $package) {
if ($package instanceof AliasPackage) {
$presentAliasMap[$package->getName().'::'.$package->getVersion()] = $package;
$removeAliasMap[$package->getName().'::'.$package->getVersion()] = $package;
} else {
$presentPackageMap[$package->getName()] = $package;
$removeMap[$package->getName()] = $package;
}
}
$stack = $this->getRootPackages();
$visited = array();
$processed = array();
while (!empty($stack)) {
$package = array_pop($stack);
if (isset($processed[spl_object_hash($package)])) {
continue;
}
if ($literal > 0) {
if (isset($installMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) {
$source = $installMeansUpdateMap[abs($literal)];
$updateMap[$package->id] = array(
'package' => $package,
'source' => $source,
'reason' => $reason,
);
// avoid updates to one package from multiple origins
unset($installMeansUpdateMap[abs($literal)]);
$ignoreRemove[$source->id] = true;
} else {
$installMap[$package->id] = array(
'package' => $package,
'reason' => $reason,
);
}
}
}
foreach ($this->decisions as $i => $decision) {
$literal = $decision[Decisions::DECISION_LITERAL];
$reason = $decision[Decisions::DECISION_REASON];
$package = $this->pool->literalToPackage($literal);
if ($literal <= 0 &&
isset($this->installedMap[$package->id]) &&
!isset($ignoreRemove[$package->id])) {
$uninstallMap[$package->id] = array(
'package' => $package,
'reason' => $reason,
);
}
}
$this->transactionFromMaps($installMap, $updateMap, $uninstallMap);
return $this->transaction;
}
protected function transactionFromMaps($installMap, $updateMap, $uninstallMap)
{
$queue = array_map(
function ($operation) {
return $operation['package'];
},
$this->findRootPackages($installMap, $updateMap)
);
$visited = array();
while (!empty($queue)) {
$package = array_pop($queue);
$packageId = $package->id;
if (!isset($visited[$packageId])) {
$queue[] = $package;
if (!isset($visited[spl_object_hash($package)])) {
$visited[spl_object_hash($package)] = true;
$stack[] = $package;
if ($package instanceof AliasPackage) {
$queue[] = $package->getAliasOf();
$stack[] = $package->getAliasOf();
} else {
foreach ($package->getRequires() as $link) {
$possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
$possibleRequires = $this->getProvidersInResult($link);
foreach ($possibleRequires as $require) {
$queue[] = $require;
$stack[] = $require;
}
}
}
} elseif (!isset($processed[spl_object_hash($package)])) {
$processed[spl_object_hash($package)] = true;
$visited[$package->id] = true;
} else {
if (isset($installMap[$packageId])) {
$this->install(
$installMap[$packageId]['package'],
$installMap[$packageId]['reason']
);
unset($installMap[$packageId]);
}
if (isset($updateMap[$packageId])) {
$this->update(
$updateMap[$packageId]['source'],
$updateMap[$packageId]['package'],
$updateMap[$packageId]['reason']
);
unset($updateMap[$packageId]);
if ($package instanceof AliasPackage) {
$aliasKey = $package->getName().'::'.$package->getVersion();
if (isset($presentAliasMap[$aliasKey])) {
unset($removeAliasMap[$aliasKey]);
} else {
$operations[] = new Operation\MarkAliasInstalledOperation($package);
}
} else {
if (isset($presentPackageMap[$package->getName()])) {
$source = $presentPackageMap[$package->getName()];
// do we need to update?
// TODO different for lock?
if ($package->getVersion() != $presentPackageMap[$package->getName()]->getVersion() ||
$package->getDistReference() !== $presentPackageMap[$package->getName()]->getDistReference() ||
$package->getSourceReference() !== $presentPackageMap[$package->getName()]->getSourceReference()
) {
$operations[] = new Operation\UpdateOperation($source, $package);
}
unset($removeMap[$package->getName()]);
} else {
$operations[] = new Operation\InstallOperation($package);
unset($removeMap[$package->getName()]);
}
}
}
}
foreach ($uninstallMap as $uninstall) {
$this->uninstall($uninstall['package'], $uninstall['reason']);
foreach ($removeMap as $name => $package) {
array_unshift($operations, new Operation\UninstallOperation($package, null));
}
foreach ($removeAliasMap as $nameVersion => $package) {
$operations[] = new Operation\MarkAliasUninstalledOperation($package, null);
}
$operations = $this->movePluginsToFront($operations);
// TODO fix this:
// we have to do this again here even though the above stack code did it because moving plugins moves them before uninstalls
$operations = $this->moveUninstallsToFront($operations);
// TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place?
/*
if ('update' === $opType) {
$targetPackage = $operation->getTargetPackage();
if ($targetPackage->isDev()) {
$initialPackage = $operation->getInitialPackage();
if ($targetPackage->getVersion() === $initialPackage->getVersion()
&& (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference())
&& (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference())
) {
$this->io->writeError(' - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG);
$this->io->writeError('', true, IOInterface::DEBUG);
continue;
}
}
}*/
return $this->operations = $operations;
}
protected function findRootPackages($installMap, $updateMap)
/**
* Determine which packages in the result are not required by any other packages in it.
*
* These serve as a starting point to enumerate packages in a topological order despite potential cycles.
* If there are packages with a cycle on the top level the package with the lowest name gets picked
*
* @return array
*/
protected function getRootPackages()
{
$packages = $installMap + $updateMap;
$roots = $packages;
$roots = $this->resultPackageMap;
foreach ($packages as $packageId => $operation) {
$package = $operation['package'];
if (!isset($roots[$packageId])) {
foreach ($this->resultPackageMap as $packageHash => $package) {
if (!isset($roots[$packageHash])) {
continue;
}
foreach ($package->getRequires() as $link) {
$possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
$possibleRequires = $this->getProvidersInResult($link);
foreach ($possibleRequires as $require) {
if ($require !== $package) {
unset($roots[$require->id]);
unset($roots[spl_object_hash($require)]);
}
}
}
@ -176,69 +220,87 @@ class Transaction
return $roots;
}
protected function findUpdates()
protected function getProvidersInResult(Link $link)
{
$installMeansUpdateMap = array();
if (!isset($this->resultPackagesByName[$link->getTarget()])) {
return array();
}
return $this->resultPackagesByName[$link->getTarget()];
}
foreach ($this->decisions as $i => $decision) {
$literal = $decision[Decisions::DECISION_LITERAL];
$package = $this->pool->literalToPackage($literal);
/**
* Workaround: if your packages depend on plugins, we must be sure
* that those are installed / updated first; else it would lead to packages
* being installed multiple times in different folders, when running Composer
* twice.
*
* While this does not fix the root-causes of https://github.com/composer/composer/issues/1147,
* it at least fixes the symptoms and makes usage of composer possible (again)
* in such scenarios.
*
* @param Operation\OperationInterface[] $operations
* @return Operation\OperationInterface[] reordered operation list
*/
private function movePluginsToFront(array $operations)
{
$pluginsNoDeps = array();
$pluginsWithDeps = array();
$pluginRequires = array();
if ($package instanceof AliasPackage) {
foreach (array_reverse($operations, true) as $idx => $op) {
if ($op instanceof Operation\InstallOperation) {
$package = $op->getPackage();
} elseif ($op instanceof Operation\UpdateOperation) {
$package = $op->getTargetPackage();
} else {
continue;
}
// !wanted & installed
if ($literal <= 0 && isset($this->installedMap[$package->id])) {
$updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package);
// is this package a plugin?
$isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer';
$literals = array($package->id);
// is this a plugin or a dependency of a plugin?
if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) {
// get the package's requires, but filter out any platform requirements or 'composer-plugin-api'
$requires = array_filter(array_keys($package->getRequires()), function ($req) {
return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req);
});
foreach ($updates as $update) {
$literals[] = $update->id;
// is this a plugin with no meaningful dependencies?
if ($isPlugin && !count($requires)) {
// plugins with no dependencies go to the very front
array_unshift($pluginsNoDeps, $op);
} else {
// capture the requirements for this package so those packages will be moved up as well
$pluginRequires = array_merge($pluginRequires, $requires);
// move the operation to the front
array_unshift($pluginsWithDeps, $op);
}
foreach ($literals as $updateLiteral) {
if ($updateLiteral !== $literal) {
$installMeansUpdateMap[abs($updateLiteral)] = $package;
}
}
unset($operations[$idx]);
}
}
return $installMeansUpdateMap;
return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations);
}
protected function install($package, $reason)
/**
* Removals of packages should be executed before installations in
* case two packages resolve to the same path (due to custom installers)
*
* @param Operation\OperationInterface[] $operations
* @return Operation\OperationInterface[] reordered operation list
*/
private function moveUninstallsToFront(array $operations)
{
if ($package instanceof AliasPackage) {
return $this->markAliasInstalled($package, $reason);
$uninstOps = array();
foreach ($operations as $idx => $op) {
if ($op instanceof Operation\UninstallOperation) {
$uninstOps[] = $op;
unset($operations[$idx]);
}
}
$this->transaction[] = new Operation\InstallOperation($package, $reason);
}
protected function update($from, $to, $reason)
{
$this->transaction[] = new Operation\UpdateOperation($from, $to, $reason);
}
protected function uninstall($package, $reason)
{
if ($package instanceof AliasPackage) {
return $this->markAliasUninstalled($package, $reason);
}
$this->transaction[] = new Operation\UninstallOperation($package, $reason);
}
protected function markAliasInstalled($package, $reason)
{
$this->transaction[] = new Operation\MarkAliasInstalledOperation($package, $reason);
}
protected function markAliasUninstalled($package, $reason)
{
$this->transaction[] = new Operation\MarkAliasUninstalledOperation($package, $reason);
return array_merge($uninstOps, $operations);
}
}

View File

@ -30,33 +30,56 @@ abstract class ArchiveDownloader extends FileDownloader
* @throws \RuntimeException
* @throws \UnexpectedValueException
*/
public function download(PackageInterface $package, $path, $output = true)
public function install(PackageInterface $package, $path, $output = true)
{
$temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8);
$retries = 3;
while ($retries--) {
$fileName = parent::download($package, $path, $output);
if ($output) {
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): Extracting archive");
} else {
$this->io->writeError('Extracting archive', false);
}
if ($output) {
$this->io->writeError(' Extracting archive', false, IOInterface::VERBOSE);
$this->filesystem->ensureDirectoryExists($path);
if (!$this->filesystem->isDirEmpty($path)) {
throw new \RuntimeException('Expected empty path to extract '.$package.' into but directory exists: '.$path);
}
do {
$temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8);
} while (is_dir($temporaryDir));
$fileName = $this->getFileName($package, $path);
try {
$this->filesystem->ensureDirectoryExists($temporaryDir);
try {
$this->extract($package, $fileName, $temporaryDir);
} catch (\Exception $e) {
// remove cache if the file was corrupted
parent::clearLastCacheWrite($package);
throw $e;
}
try {
$this->filesystem->ensureDirectoryExists($temporaryDir);
try {
$this->extract($fileName, $temporaryDir);
} catch (\Exception $e) {
// remove cache if the file was corrupted
parent::clearLastCacheWrite($package);
throw $e;
$this->filesystem->unlink($fileName);
$renameAsOne = false;
if (!file_exists($path) || ($this->filesystem->isDirEmpty($path) && $this->filesystem->removeDirectory($path))) {
$renameAsOne = true;
}
$contentDir = $this->getFolderContent($temporaryDir);
$singleDirAtTopLevel = 1 === count($contentDir) && is_dir(reset($contentDir));
if ($renameAsOne) {
// if the target $path is clear, we can rename the whole package in one go instead of looping over the contents
if ($singleDirAtTopLevel) {
$extractedDir = (string) reset($contentDir);
} else {
$extractedDir = $temporaryDir;
}
$this->filesystem->unlink($fileName);
$contentDir = $this->getFolderContent($temporaryDir);
$this->filesystem->rename($extractedDir, $path);
} else {
// only one dir in the archive, extract its contents out of it
if (1 === count($contentDir) && is_dir(reset($contentDir))) {
if ($singleDirAtTopLevel) {
$contentDir = $this->getFolderContent((string) reset($contentDir));
}
@ -65,44 +88,16 @@ abstract class ArchiveDownloader extends FileDownloader
$file = (string) $file;
$this->filesystem->rename($file, $path . '/' . basename($file));
}
$this->filesystem->removeDirectory($temporaryDir);
if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) {
$this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/');
}
if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) {
$this->filesystem->removeDirectory($this->config->get('vendor-dir'));
}
} catch (\Exception $e) {
// clean up
$this->filesystem->removeDirectory($path);
$this->filesystem->removeDirectory($temporaryDir);
// retry downloading if we have an invalid zip file
if ($retries && $e instanceof \UnexpectedValueException && class_exists('ZipArchive') && $e->getCode() === \ZipArchive::ER_NOZIP) {
$this->io->writeError('');
if ($this->io->isDebug()) {
$this->io->writeError(' Invalid zip file ('.$e->getMessage().'), retrying...');
} else {
$this->io->writeError(' Invalid zip file, retrying...');
}
usleep(500000);
continue;
}
throw $e;
}
break;
}
}
$this->filesystem->removeDirectory($temporaryDir);
} catch (\Exception $e) {
// clean up
$this->filesystem->removeDirectory($path);
$this->filesystem->removeDirectory($temporaryDir);
/**
* {@inheritdoc}
*/
protected function getFileName(PackageInterface $package, $path)
{
return rtrim($path.'/'.md5($path.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.');
throw $e;
}
}
/**
@ -113,7 +108,7 @@ abstract class ArchiveDownloader extends FileDownloader
*
* @throws \UnexpectedValueException If can not extract downloaded file to path
*/
abstract protected function extract($file, $path);
abstract protected function extract(PackageInterface $package, $file, $path);
/**
* Returns the folder content, excluding dotfiles

View File

@ -15,6 +15,7 @@ namespace Composer\Downloader;
use Composer\Package\PackageInterface;
use Composer\IO\IOInterface;
use Composer\Util\Filesystem;
use React\Promise\PromiseInterface;
/**
* Downloaders manager.
@ -24,6 +25,7 @@ use Composer\Util\Filesystem;
class DownloadManager
{
private $io;
private $httpDownloader;
private $preferDist = false;
private $preferSource = false;
private $packagePreferences = array();
@ -33,9 +35,9 @@ class DownloadManager
/**
* Initializes download manager.
*
* @param IOInterface $io The Input Output Interface
* @param bool $preferSource prefer downloading from source
* @param Filesystem|null $filesystem custom Filesystem object
* @param IOInterface $io The Input Output Interface
* @param bool $preferSource prefer downloading from source
* @param Filesystem|null $filesystem custom Filesystem object
*/
public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null)
{
@ -83,22 +85,6 @@ class DownloadManager
return $this;
}
/**
* Sets whether to output download progress information for all registered
* downloaders
*
* @param bool $outputProgress
* @return DownloadManager
*/
public function setOutputProgress($outputProgress)
{
foreach ($this->downloaders as $downloader) {
$downloader->setOutputProgress($outputProgress);
}
return $this;
}
/**
* Sets installer downloader for a specific installation type.
*
@ -140,7 +126,7 @@ class DownloadManager
* wrong type
* @return DownloaderInterface|null
*/
public function getDownloaderForInstalledPackage(PackageInterface $package)
public function getDownloaderForPackage(PackageInterface $package)
{
$installationSource = $package->getInstallationSource();
@ -154,7 +140,7 @@ class DownloadManager
$downloader = $this->getDownloader($package->getSourceType());
} else {
throw new \InvalidArgumentException(
'Package '.$package.' seems not been installed properly'
'Package '.$package.' does not have an installation source set'
);
}
@ -171,63 +157,117 @@ class DownloadManager
return $downloader;
}
public function getDownloaderType(DownloaderInterface $downloader)
{
return array_search($downloader, $this->downloaders);
}
/**
* Downloads package into target dir.
*
* @param PackageInterface $package package instance
* @param string $targetDir target dir
* @param bool $preferSource prefer installation from source
* @param PackageInterface $package package instance
* @param string $targetDir target dir
* @param PackageInterface|null $prevPackage previous package instance in case of updates
*
* @return PromiseInterface
* @throws \InvalidArgumentException if package have no urls to download from
* @throws \RuntimeException
*/
public function download(PackageInterface $package, $targetDir, $preferSource = null)
public function download(PackageInterface $package, $targetDir, PackageInterface $prevPackage = null)
{
$preferSource = null !== $preferSource ? $preferSource : $this->preferSource;
$sourceType = $package->getSourceType();
$distType = $package->getDistType();
$targetDir = $this->normalizeTargetDir($targetDir);
$this->filesystem->ensureDirectoryExists(dirname($targetDir));
$sources = array();
if ($sourceType) {
$sources[] = 'source';
}
if ($distType) {
$sources[] = 'dist';
}
$sources = $this->getAvailableSources($package, $prevPackage);
if (empty($sources)) {
throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified');
}
$io = $this->io;
$self = $this;
if (!$preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) {
$sources = array_reverse($sources);
}
$this->filesystem->ensureDirectoryExists($targetDir);
foreach ($sources as $i => $source) {
if (isset($e)) {
$this->io->writeError(' <warning>Now trying to download from ' . $source . '</warning>');
$download = function ($retry = false) use (&$sources, $io, $package, $self, $targetDir, &$download, $prevPackage) {
$source = array_shift($sources);
if ($retry) {
$io->writeError(' <warning>Now trying to download from ' . $source . '</warning>');
}
$package->setInstallationSource($source);
try {
$downloader = $this->getDownloaderForInstalledPackage($package);
if ($downloader) {
$downloader->download($package, $targetDir);
}
break;
} catch (\RuntimeException $e) {
if ($i === count($sources) - 1) {
throw $e;
$downloader = $self->getDownloaderForPackage($package);
if (!$downloader) {
return \React\Promise\resolve();
}
$handleError = function ($e) use ($sources, $source, $package, $io, $download) {
if ($e instanceof \RuntimeException) {
if (!$sources) {
throw $e;
}
$io->writeError(
' <warning>Failed to download '.
$package->getPrettyName().
' from ' . $source . ': '.
$e->getMessage().'</warning>'
);
return $download(true);
}
$this->io->writeError(
' <warning>Failed to download '.
$package->getPrettyName().
' from ' . $source . ': '.
$e->getMessage().'</warning>'
);
throw $e;
};
try {
$result = $downloader->download($package, $targetDir, $prevPackage);
} catch (\Exception $e) {
return $handleError($e);
}
if (!$result instanceof PromiseInterface) {
return \React\Promise\resolve($result);
}
$res = $result->then(function ($res) {
return $res;
}, $handleError);
return $res;
};
return $download();
}
/**
* Prepares an operation execution
*
* @param string $type one of install/update/uninstall
* @param PackageInterface $package package instance
* @param string $targetDir target dir
* @param PackageInterface|null $prevPackage previous package instance in case of updates
*
* @return PromiseInterface|null
*/
public function prepare($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null)
{
$targetDir = $this->normalizeTargetDir($targetDir);
$downloader = $this->getDownloaderForPackage($package);
if ($downloader) {
return $downloader->prepare($type, $package, $targetDir, $prevPackage);
}
}
/**
* Installs package into target dir.
*
* @param PackageInterface $package package instance
* @param string $targetDir target dir
*
* @return PromiseInterface|null
* @throws \InvalidArgumentException if package have no urls to download from
* @throws \RuntimeException
*/
public function install(PackageInterface $package, $targetDir)
{
$targetDir = $this->normalizeTargetDir($targetDir);
$downloader = $this->getDownloaderForPackage($package);
if ($downloader) {
return $downloader->install($package, $targetDir);
}
}
@ -238,39 +278,30 @@ class DownloadManager
* @param PackageInterface $target target package version
* @param string $targetDir target dir
*
* @return PromiseInterface|null
* @throws \InvalidArgumentException if initial package is not installed
*/
public function update(PackageInterface $initial, PackageInterface $target, $targetDir)
{
$downloader = $this->getDownloaderForInstalledPackage($initial);
$targetDir = $this->normalizeTargetDir($targetDir);
$downloader = $this->getDownloaderForPackage($target);
$initialDownloader = $this->getDownloaderForPackage($initial);
// no downloaders present means update from metapackage to metapackage, nothing to do
if (!$initialDownloader && !$downloader) {
return;
}
// if we have a downloader present before, but not after, the package became a metapackage and its files should be removed
if (!$downloader) {
return;
}
$installationSource = $initial->getInstallationSource();
if ('dist' === $installationSource) {
$initialType = $initial->getDistType();
$targetType = $target->getDistType();
} else {
$initialType = $initial->getSourceType();
$targetType = $target->getSourceType();
}
// upgrading from a dist stable package to a dev package, force source reinstall
if ($target->isDev() && 'dist' === $installationSource) {
$downloader->remove($initial, $targetDir);
$this->download($target, $targetDir);
return;
return $initialDownloader->remove($initial, $targetDir);
}
$initialType = $this->getDownloaderType($initialDownloader);
$targetType = $this->getDownloaderType($downloader);
if ($initialType === $targetType) {
$target->setInstallationSource($installationSource);
try {
$downloader->update($initial, $target, $targetDir);
return;
return $downloader->update($initial, $target, $targetDir);
} catch (\RuntimeException $e) {
if (!$this->io->isInteractive()) {
throw $e;
@ -282,8 +313,17 @@ class DownloadManager
}
}
$downloader->remove($initial, $targetDir);
$this->download($target, $targetDir, 'source' === $installationSource);
// if downloader type changed, or update failed and user asks for reinstall,
// we wipe the dir and do a new install instead of updating it
$promise = $initialDownloader->remove($initial, $targetDir);
if ($promise) {
$self = $this;
return $promise->then(function ($res) use ($self, $target, $targetDir) {
return $self->install($target, $targetDir);
});
}
return $this->install($target, $targetDir);
}
/**
@ -291,12 +331,34 @@ class DownloadManager
*
* @param PackageInterface $package package instance
* @param string $targetDir target dir
*
* @return PromiseInterface|null
*/
public function remove(PackageInterface $package, $targetDir)
{
$downloader = $this->getDownloaderForInstalledPackage($package);
$targetDir = $this->normalizeTargetDir($targetDir);
$downloader = $this->getDownloaderForPackage($package);
if ($downloader) {
$downloader->remove($package, $targetDir);
return $downloader->remove($package, $targetDir);
}
}
/**
* Cleans up a failed operation
*
* @param string $type one of install/update/uninstall
* @param PackageInterface $package package instance
* @param string $targetDir target dir
* @param PackageInterface|null $prevPackage previous package instance in case of updates
*
* @return PromiseInterface|null
*/
public function cleanup($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null)
{
$targetDir = $this->normalizeTargetDir($targetDir);
$downloader = $this->getDownloaderForPackage($package);
if ($downloader) {
return $downloader->cleanup($type, $package, $targetDir, $prevPackage);
}
}
@ -322,4 +384,64 @@ class DownloadManager
return $package->isDev() ? 'source' : 'dist';
}
/**
* @return string[]
*/
private function getAvailableSources(PackageInterface $package, PackageInterface $prevPackage = null)
{
$sourceType = $package->getSourceType();
$distType = $package->getDistType();
// add source before dist by default
$sources = array();
if ($sourceType) {
$sources[] = 'source';
}
if ($distType) {
$sources[] = 'dist';
}
if (empty($sources)) {
throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified');
}
if (
$prevPackage
// if we are updating, we want to keep the same source as the previously installed package (if available in the new one)
&& in_array($prevPackage->getInstallationSource(), $sources, true)
// unless the previous package was stable dist (by default) and the new package is dev, then we allow the new default to take over
&& !(!$prevPackage->isDev() && $prevPackage->getInstallationSource() === 'dist' && $package->isDev())
) {
$prevSource = $prevPackage->getInstallationSource();
usort($sources, function ($a, $b) use ($prevSource) {
return $a === $prevSource ? -1 : 1;
});
return $sources;
}
// reverse sources in case dist is the preferred source for this package
if (!$this->preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) {
$sources = array_reverse($sources);
}
return $sources;
}
/**
* Downloaders expect a /path/to/dir without trailing slash
*
* If any Installer provides a path with a trailing slash, this can cause bugs so make sure we remove them
*
* @return string
*/
private function normalizeTargetDir($dir)
{
if ($dir === '\\' || $dir === '/') {
return $dir;
}
return rtrim($dir, '\\/');
}
}

View File

@ -13,6 +13,7 @@
namespace Composer\Downloader;
use Composer\Package\PackageInterface;
use React\Promise\PromiseInterface;
/**
* Downloader interface.
@ -30,12 +31,35 @@ interface DownloaderInterface
public function getInstallationSource();
/**
* Downloads specific package into specific folder.
* This should do any network-related tasks to prepare for an upcoming install/update
*
* @return PromiseInterface|null
*/
public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null);
/**
* Do anything that needs to be done between all downloads have been completed and the actual operation is executed
*
* All packages get first downloaded, then all together prepared, then all together installed/updated/uninstalled. Therefore
* for error recovery it is important to avoid failing during install/update/uninstall as much as possible, and risky things or
* user prompts should happen in the prepare step rather. In case of failure, cleanup() will be called so that changes can
* be undone as much as possible.
*
* @param string $type one of install/update/uninstall
* @param PackageInterface $package package instance
* @param string $path download path
* @param PackageInterface $prevPackage previous package instance in case of an update
* @return PromiseInterface|null
*/
public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null);
/**
* Installs specific package into specific folder.
*
* @param PackageInterface $package package instance
* @param string $path download path
*/
public function download(PackageInterface $package, $path);
public function install(PackageInterface $package, $path);
/**
* Updates specific package in specific folder from initial to target version.
@ -55,10 +79,17 @@ interface DownloaderInterface
public function remove(PackageInterface $package, $path);
/**
* Sets whether to output download progress information or not
* Do anything to cleanup changes applied in the prepare or install/update/uninstall steps
*
* @param bool $outputProgress
* @return DownloaderInterface
* Note that cleanup will be called for all packages regardless if they failed an operation or not, to give
* all installers a change to cleanup things they did previously, so you need to keep track of changes
* applied in the installer/downloader themselves.
*
* @param string $type one of install/update/uninstall
* @param PackageInterface $package package instance
* @param string $path download path
* @param PackageInterface $prevPackage previous package instance in case of an update
* @return PromiseInterface|null
*/
public function setOutputProgress($outputProgress);
public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null);
}

View File

@ -24,8 +24,9 @@ use Composer\Plugin\PluginEvents;
use Composer\Plugin\PreFileDownloadEvent;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\Filesystem;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\Util\Url as UrlUtil;
use Composer\Downloader\TransportException;
/**
* Base downloader for files
@ -39,11 +40,13 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
{
protected $io;
protected $config;
protected $rfs;
protected $httpDownloader;
protected $filesystem;
protected $cache;
protected $outputProgress = true;
private $lastCacheWrites = array();
/**
* @private this is only public for php 5.3 support in closures
*/
public $lastCacheWrites = array();
private $eventDispatcher;
/**
@ -51,17 +54,17 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
*
* @param IOInterface $io The IO instance
* @param Config $config The config
* @param HttpDownloader $httpDownloader The remote filesystem
* @param EventDispatcher $eventDispatcher The event dispatcher
* @param Cache $cache Optional cache instance
* @param RemoteFilesystem $rfs The remote filesystem
* @param Cache $cache Cache instance
* @param Filesystem $filesystem The filesystem
*/
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, RemoteFilesystem $rfs = null, Filesystem $filesystem = null)
public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $filesystem = null)
{
$this->io = $io;
$this->config = $config;
$this->eventDispatcher = $eventDispatcher;
$this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $config);
$this->httpDownloader = $httpDownloader;
$this->filesystem = $filesystem ?: new Filesystem();
$this->cache = $cache;
@ -81,127 +84,191 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
/**
* {@inheritDoc}
*/
public function download(PackageInterface $package, $path, $output = true)
public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
{
if (!$package->getDistUrl()) {
throw new \InvalidArgumentException('The given package is missing url information');
}
if ($output) {
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): ", false);
}
$retries = 3;
$urls = $package->getDistUrls();
while ($url = array_shift($urls)) {
try {
$fileName = $this->doDownload($package, $path, $url);
break;
} catch (\Exception $e) {
if ($this->io->isDebug()) {
$this->io->writeError('');
$this->io->writeError('Failed: ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage());
} elseif (count($urls)) {
$this->io->writeError('');
$this->io->writeError(' Failed, trying the next URL ('.$e->getCode().': '.$e->getMessage().')', false);
}
if (!count($urls)) {
throw $e;
}
}
foreach ($urls as $index => $url) {
$processedUrl = $this->processUrl($package, $url);
$urls[$index] = array(
'base' => $url,
'processed' => $processedUrl,
'cacheKey' => $this->getCacheKey($package, $processedUrl)
);
}
if ($output) {
$this->io->writeError('');
}
return $fileName;
}
protected function doDownload(PackageInterface $package, $path, $url)
{
$this->filesystem->emptyDirectory($path);
$fileName = $this->getFileName($package, $path);
$this->filesystem->ensureDirectoryExists($path);
$this->filesystem->ensureDirectoryExists(dirname($fileName));
$processedUrl = $this->processUrl($package, $url);
$origin = RemoteFilesystem::getOrigin($processedUrl);
$io = $this->io;
$cache = $this->cache;
$httpDownloader = $this->httpDownloader;
$eventDispatcher = $this->eventDispatcher;
$filesystem = $this->filesystem;
$self = $this;
$preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $processedUrl);
if ($this->eventDispatcher) {
$this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
}
$rfs = $preFileDownloadEvent->getRemoteFilesystem();
$accept = null;
$reject = null;
$download = function () use ($io, $output, $httpDownloader, $cache, $eventDispatcher, $package, $fileName, &$urls, &$accept, &$reject) {
$url = reset($urls);
if ($eventDispatcher) {
$preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed']);
$eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
}
try {
$checksum = $package->getDistSha1Checksum();
$cacheKey = $this->getCacheKey($package, $processedUrl);
$cacheKey = $url['cacheKey'];
// use from cache if it is present and has a valid checksum or we have no checksum to check against
if ($this->cache && (!$checksum || $checksum === $this->cache->sha1($cacheKey)) && $this->cache->copyTo($cacheKey, $fileName)) {
$this->io->writeError('Loading from cache', false);
if ($cache && (!$checksum || $checksum === $cache->sha1($cacheKey)) && $cache->copyTo($cacheKey, $fileName)) {
if ($output) {
$io->writeError(" - Loading <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>) from cache", true, IOInterface::VERY_VERBOSE);
}
$result = \React\Promise\resolve($fileName);
} else {
// download if cache restore failed
if (!$this->outputProgress) {
$this->io->writeError('Downloading', false);
if ($output) {
$io->writeError(" - Downloading <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
}
// try to download 3 times then fail hard
$retries = 3;
while ($retries--) {
try {
$rfs->copy($origin, $processedUrl, $fileName, $this->outputProgress, $package->getTransportOptions());
break;
} catch (TransportException $e) {
// if we got an http response with a proper code, then requesting again will probably not help, abort
if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) {
throw $e;
}
$this->io->writeError('');
$this->io->writeError(' Download failed, retrying...', true, IOInterface::VERBOSE);
usleep(500000);
}
}
if (!$this->outputProgress) {
$this->io->writeError(' (<comment>100%</comment>)', false);
}
if ($this->cache) {
$this->lastCacheWrites[$package->getName()] = $cacheKey;
$this->cache->copyFrom($cacheKey, $fileName);
}
$result = $httpDownloader->addCopy($url['processed'], $fileName, $package->getTransportOptions())
->then($accept, $reject);
}
if (!file_exists($fileName)) {
throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the'
.' directory is writable and you have internet connectivity');
return $result->then(function ($result) use ($fileName, $checksum, $url) {
// in case of retry, the first call's Promise chain finally calls this twice at the end,
// once with $result being the returned $fileName from $accept, and then once for every
// failed request with a null result, which can be skipped.
if (null === $result) {
return $fileName;
}
if (!file_exists($fileName)) {
throw new \UnexpectedValueException($url['base'].' could not be saved to '.$fileName.', make sure the'
.' directory is writable and you have internet connectivity');
}
if ($checksum && hash_file('sha1', $fileName) !== $checksum) {
throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url['base'].')');
}
return $fileName;
});
};
$accept = function ($response) use ($cache, $package, $fileName, $self, &$urls) {
$url = reset($urls);
$cacheKey = $url['cacheKey'];
if ($cache) {
$self->lastCacheWrites[$package->getName()] = $cacheKey;
$cache->copyFrom($cacheKey, $fileName);
}
if ($checksum && hash_file('sha1', $fileName) !== $checksum) {
throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url.')');
}
} catch (\Exception $e) {
$response->collect();
return $fileName;
};
$reject = function ($e) use ($io, &$urls, $download, $fileName, $package, &$retries, $filesystem, $self) {
// clean up
$this->filesystem->removeDirectory($path);
$this->clearLastCacheWrite($package);
throw $e;
}
if (file_exists($fileName)) {
$filesystem->unlink($fileName);
}
$self->clearLastCacheWrite($package);
return $fileName;
if ($e instanceof TransportException) {
// if we got an http response with a proper code, then requesting again will probably not help, abort
if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) {
$retries = 0;
}
}
// special error code returned when network is being artificially disabled
if ($e instanceof TransportException && $e->getStatusCode() === 499) {
$retries = 0;
$urls = array();
}
if ($retries) {
usleep(500000);
$retries--;
return $download();
}
array_shift($urls);
if ($urls) {
if ($io->isDebug()) {
$io->writeError(' Failed downloading '.$package->getName().': ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage());
$io->writeError(' Trying the next URL for '.$package->getName());
} elseif (count($urls)) {
$io->writeError(' Failed downloading '.$package->getName().', trying the next URL ('.$e->getCode().': '.$e->getMessage().')');
}
$retries = 3;
usleep(100000);
return $download();
}
throw $e;
};
return $download();
}
/**
* {@inheritDoc}
*/
public function setOutputProgress($outputProgress)
public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null)
{
$this->outputProgress = $outputProgress;
return $this;
}
protected function clearLastCacheWrite(PackageInterface $package)
/**
* {@inheritDoc}
*/
public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null)
{
$fileName = $this->getFileName($package, $path);
if (file_exists($fileName)) {
$this->filesystem->unlink($fileName);
}
if (is_dir($path) && $this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) {
$this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/');
}
if (is_dir($path) && $this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) {
$this->filesystem->removeDirectory($this->config->get('vendor-dir'));
}
if (is_dir($path) && $this->filesystem->isDirEmpty($path)) {
$this->filesystem->removeDirectory($path);
}
}
/**
* {@inheritDoc}
*/
public function install(PackageInterface $package, $path, $output = true)
{
if ($output) {
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
}
$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));
}
/**
* TODO mark private in v3
* @protected This is public due to PHP 5.3
*/
public function clearLastCacheWrite(PackageInterface $package)
{
if ($this->cache && isset($this->lastCacheWrites[$package->getName()])) {
$this->cache->remove($this->lastCacheWrites[$package->getName()]);
@ -218,11 +285,11 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
$from = $initial->getFullPrettyVersion();
$to = $target->getFullPrettyVersion();
$actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Updating' : 'Downgrading';
$actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading';
$this->io->writeError(" - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
$this->remove($initial, $path, false);
$this->download($target, $path, false);
$this->install($target, $path, false);
$this->io->writeError('');
}
@ -249,7 +316,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
*/
protected function getFileName(PackageInterface $package, $path)
{
return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
return rtrim($this->config->get('vendor-dir').'/composer/'.md5($package.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.');
}
/**
@ -291,15 +358,15 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
public function getLocalChanges(PackageInterface $package, $targetDir)
{
$prevIO = $this->io;
$prevProgress = $this->outputProgress;
$this->io = new NullIO;
$this->io->loadConfiguration($this->config);
$this->outputProgress = false;
$e = null;
try {
$this->download($package, $targetDir.'_compare', false);
$res = $this->download($package, $targetDir.'_compare', null, false);
$this->httpDownloader->wait();
$res = $this->install($package, $targetDir.'_compare', false);
$comparer = new Comparer();
$comparer->setSource($targetDir.'_compare');
@ -311,7 +378,6 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
}
$this->io = $prevIO;
$this->outputProgress = $prevProgress;
if ($e) {
throw $e;

View File

@ -23,7 +23,15 @@ class FossilDownloader extends VcsDownloader
/**
* {@inheritDoc}
*/
public function doDownload(PackageInterface $package, $path, $url)
protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null)
{
}
/**
* {@inheritDoc}
*/
protected function doInstall(PackageInterface $package, $path, $url)
{
// Ensure we are allowed to use this URL by config
$this->config->prohibitUrlByConfig($url, $this->io);
@ -49,7 +57,7 @@ class FossilDownloader extends VcsDownloader
/**
* {@inheritDoc}
*/
public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
{
// Ensure we are allowed to use this URL by config
$this->config->prohibitUrlByConfig($url, $this->io);

View File

@ -17,6 +17,7 @@ use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
use Composer\Util\Filesystem;
use Composer\Util\Git as GitUtil;
use Composer\Util\Url;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Cache;
@ -29,6 +30,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
private $hasStashedChanges = false;
private $hasDiscardedChanges = false;
private $gitUtil;
private $cachedPackages = array();
public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, Filesystem $fs = null)
{
@ -39,7 +41,28 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
/**
* {@inheritDoc}
*/
public function doDownload(PackageInterface $package, $path, $url)
protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null)
{
GitUtil::cleanEnv();
$cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/';
$gitVersion = $this->gitUtil->getVersion();
// --dissociate option is only available since git 2.3.0-rc0
if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) {
$this->io->writeError(" - Syncing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>) into cache");
$this->io->writeError(sprintf(' Cloning to cache at %s', ProcessExecutor::escape($cachePath)), true, IOInterface::DEBUG);
$ref = $package->getSourceReference();
if ($this->gitUtil->fetchRefOrSyncMirror($url, $cachePath, $ref) && is_dir($cachePath)) {
$this->cachedPackages[$package->getId()][$ref] = true;
}
}
}
/**
* {@inheritDoc}
*/
protected function doInstall(PackageInterface $package, $path, $url)
{
GitUtil::cleanEnv();
$path = $this->normalizePath($path);
@ -47,31 +70,20 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
$ref = $package->getSourceReference();
$flag = Platform::isWindows() ? '/D ' : '';
// --dissociate option is only available since git 2.3.0-rc0
$gitVersion = $this->gitUtil->getVersion();
$msg = "Cloning ".$this->getShortHash($ref);
$command = 'git clone --no-checkout %url% %path% && cd '.$flag.'%path% && git remote add composer %url% && git fetch composer && git remote set-url origin %sanitizedUrl% && git remote set-url composer %sanitizedUrl%';
if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) {
$this->io->writeError('', true, IOInterface::DEBUG);
$this->io->writeError(sprintf(' Cloning to cache at %s', ProcessExecutor::escape($cachePath)), true, IOInterface::DEBUG);
try {
if (!$this->gitUtil->fetchRefOrSyncMirror($url, $cachePath, $ref)) {
$this->io->writeError('<error>Failed to update '.$url.' in cache, package installation for '.$package->getPrettyName().' might fail.</error>');
}
if (is_dir($cachePath)) {
$command =
'git clone --no-checkout %cachePath% %path% --dissociate --reference %cachePath% '
. '&& cd '.$flag.'%path% '
. '&& git remote set-url origin %sanitizedUrl% && git remote add composer %sanitizedUrl%';
$msg = "Cloning ".$this->getShortHash($ref).' from cache';
}
} catch (\RuntimeException $e) {
if (0 === strpos(get_class($e), 'PHPUnit')) {
throw $e;
}
if (!empty($this->cachedPackages[$package->getId()][$ref])) {
$msg = "Cloning ".$this->getShortHash($ref).' from cache';
$command =
'git clone --no-checkout %cachePath% %path% --dissociate --reference %cachePath% '
. '&& cd '.$flag.'%path% '
. '&& git remote set-url origin %sanitizedUrl% && git remote add composer %sanitizedUrl%';
} else {
$msg = "Cloning ".$this->getShortHash($ref);
$command = 'git clone --no-checkout %url% %path% && cd '.$flag.'%path% && git remote add composer %url% && git fetch composer && git remote set-url origin %sanitizedUrl% && git remote set-url composer %sanitizedUrl%';
if (getenv('COMPOSER_DISABLE_NETWORK')) {
throw new \RuntimeException('The required git reference for '.$package->getName().' is not in cache and network is disabled, aborting');
}
}
$this->io->writeError($msg);
$commandCallable = function ($url) use ($path, $command, $cachePath) {
@ -105,13 +117,52 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
/**
* {@inheritDoc}
*/
public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
{
GitUtil::cleanEnv();
$path = $this->normalizePath($path);
if (!$this->hasMetadataRepository($path)) {
throw new \RuntimeException('The .git directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information');
}
$cachePath = $this->config->get('cache-vcs-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $url).'/';
$ref = $target->getSourceReference();
$flag = Platform::isWindows() ? '/D ' : '';
if (!empty($this->cachedPackages[$target->getId()][$ref])) {
$msg = "Checking out ".$this->getShortHash($ref).' from cache';
$command = 'git rev-parse --quiet --verify %ref% || (git remote set-url composer %cachePath% && git fetch composer && git fetch --tags composer); git remote set-url composer %sanitizedUrl%';
} else {
$msg = "Checking out ".$this->getShortHash($ref);
$command = 'git remote set-url composer %url% && git rev-parse --quiet --verify %ref% || (git fetch composer && git fetch --tags composer); git remote set-url composer %sanitizedUrl%';
if (getenv('COMPOSER_DISABLE_NETWORK')) {
throw new \RuntimeException('The required git reference for '.$target->getName().' is not in cache and network is disabled, aborting');
}
}
$this->io->writeError($msg);
$commandCallable = function ($url) use ($ref, $command, $cachePath) {
return str_replace(
array('%url%', '%ref%', '%cachePath%', '%sanitizedUrl%'),
array(
ProcessExecutor::escape($url),
ProcessExecutor::escape($ref.'^{commit}'),
ProcessExecutor::escape($cachePath),
ProcessExecutor::escape(preg_replace('{://([^@]+?):(.+?)@}', '://', $url)),
),
$command
);
};
$this->gitUtil->runCommand($commandCallable, $url, $path);
if ($newRef = $this->updateToCommit($path, $ref, $target->getPrettyVersion(), $target->getReleaseDate())) {
if ($target->getDistReference() === $target->getSourceReference()) {
$target->setDistReference($newRef);
}
$target->setSourceReference($newRef);
}
$updateOriginUrl = false;
if (
0 === $this->process->execute('git remote -v', $output, $path)
@ -122,28 +173,6 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
$updateOriginUrl = true;
}
}
$ref = $target->getSourceReference();
$this->io->writeError(" Checking out ".$this->getShortHash($ref));
$command = '(git remote set-url composer %s && git rev-parse --quiet --verify %s || (git fetch composer && git fetch --tags composer)) && git remote set-url composer %s';
$commandCallable = function ($url) use ($command, $ref) {
return sprintf(
$command,
ProcessExecutor::escape($url),
ProcessExecutor::escape($ref.'^{commit}'),
ProcessExecutor::escape(preg_replace('{://([^@]+?):(.+?)@}', '://', $url))
);
};
$this->gitUtil->runCommand($commandCallable, $url, $path);
if ($newRef = $this->updateToCommit($path, $ref, $target->getPrettyVersion(), $target->getReleaseDate())) {
if ($target->getDistReference() === $target->getSourceReference()) {
$target->setDistReference($newRef);
}
$target->setSourceReference($newRef);
}
if ($updateOriginUrl) {
$this->updateOriginUrl($path, $target->getSourceUrl());
}
@ -272,7 +301,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
$changes = array_map(function ($elem) {
return ' '.$elem;
}, preg_split('{\s*\r?\n\s*}', $changes));
$this->io->writeError(' <error>The package has modified files:</error>');
$this->io->writeError(' <error>'.$package->getPrettyName().' has modified files:</error>');
$this->io->writeError(array_slice($changes, 0, 10));
if (count($changes) > 10) {
$this->io->writeError(' <info>' . (count($changes) - 10) . ' more files modified, choose "v" to view the full list</info>');
@ -373,7 +402,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
) {
$command = sprintf('git checkout '.$force.'-B %s %s -- && git reset --hard %2$s --', ProcessExecutor::escape($branch), ProcessExecutor::escape('composer/'.$reference));
if (0 === $this->process->execute($command, $output, $path)) {
return;
return null;
}
}
@ -391,14 +420,14 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
) {
$command = sprintf('git reset --hard %s --', ProcessExecutor::escape($reference));
if (0 === $this->process->execute($command, $output, $path)) {
return;
return null;
}
}
}
$command = sprintf($template, ProcessExecutor::escape($gitRef));
if (0 === $this->process->execute($command, $output, $path)) {
return;
return null;
}
// reference was not found (prints "fatal: reference is not a tree: $ref")
@ -406,7 +435,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
$this->io->writeError(' <warning>'.$reference.' is gone (history was rewritten?)</warning>');
}
throw new \RuntimeException(GitUtil::sanitizeUrl('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()));
throw new \RuntimeException(Url::sanitize('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()));
}
protected function updateOriginUrl($path, $url)

View File

@ -18,7 +18,7 @@ use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface;
/**
@ -28,17 +28,19 @@ use Composer\IO\IOInterface;
*/
class GzipDownloader extends ArchiveDownloader
{
/** @var ProcessExecutor */
protected $process;
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
}
protected function extract($file, $path)
protected function extract(PackageInterface $package, $file, $path)
{
$targetFilepath = $path . DIRECTORY_SEPARATOR . basename(substr($file, 0, -3));
$filename = pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_FILENAME);
$targetFilepath = $path . DIRECTORY_SEPARATOR . $filename;
// Try to use gunzip on *nix
if (!Platform::isWindows()) {
@ -63,14 +65,6 @@ class GzipDownloader extends ArchiveDownloader
$this->extractUsingExt($file, $targetFilepath);
}
/**
* {@inheritdoc}
*/
protected function getFileName(PackageInterface $package, $path)
{
return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
}
private function extractUsingExt($file, $targetFilepath)
{
$archiveFile = gzopen($file, 'rb');

View File

@ -24,7 +24,15 @@ class HgDownloader extends VcsDownloader
/**
* {@inheritDoc}
*/
public function doDownload(PackageInterface $package, $path, $url)
protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null)
{
}
/**
* {@inheritDoc}
*/
protected function doInstall(PackageInterface $package, $path, $url)
{
$hgUtils = new HgUtils($this->io, $this->config, $this->process);
@ -44,7 +52,7 @@ class HgDownloader extends VcsDownloader
/**
* {@inheritDoc}
*/
public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
{
$hgUtils = new HgUtils($this->io, $this->config, $this->process);

View File

@ -37,7 +37,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
/**
* {@inheritdoc}
*/
public function download(PackageInterface $package, $path, $output = true)
public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
{
$url = $package->getDistUrl();
$realUrl = realpath($url);
@ -50,14 +50,6 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
}
if (realpath($path) === $realUrl) {
if ($output) {
$this->io->writeError(sprintf(
' - Installing <info>%s</info> (<comment>%s</comment>): Source already present',
$package->getName(),
$package->getFullPrettyVersion()
));
}
return;
}
@ -73,6 +65,29 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
$realUrl
));
}
}
/**
* {@inheritdoc}
*/
public function install(PackageInterface $package, $path, $output = true)
{
$url = $package->getDistUrl();
$realUrl = realpath($url);
if (realpath($path) === $realUrl) {
if ($output) {
$this->io->writeError(sprintf(
' - Installing <info>%s</info> (<comment>%s</comment>): Source already present',
$package->getName(),
$package->getFullPrettyVersion()
));
} else {
$this->io->writeError('Source already present', false);
}
return;
}
// Get the transport options with default values
$transportOptions = $package->getTransportOptions() + array('symlink' => null, 'relative' => true);
@ -154,7 +169,9 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
$fileSystem->mirror($realUrl, $path, $iterator);
}
$this->io->writeError('');
if ($output) {
$this->io->writeError('');
}
}
/**
@ -164,7 +181,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
{
$realUrl = realpath($package->getDistUrl());
if (realpath($path) === $realUrl) {
if ($path === $realUrl) {
if ($output) {
$this->io->writeError(" - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>), source is still present in $path");
}

View File

@ -27,7 +27,15 @@ class PerforceDownloader extends VcsDownloader
/**
* {@inheritDoc}
*/
public function doDownload(PackageInterface $package, $path, $url)
protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null)
{
}
/**
* {@inheritDoc}
*/
public function doInstall(PackageInterface $package, $path, $url)
{
$ref = $package->getSourceReference();
$label = $this->getLabelFromSourceReference($ref);
@ -76,9 +84,9 @@ class PerforceDownloader extends VcsDownloader
/**
* {@inheritDoc}
*/
public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
{
$this->doDownload($target, $path, $url);
$this->doInstall($target, $path, $url);
}
/**

View File

@ -12,6 +12,8 @@
namespace Composer\Downloader;
use Composer\Package\PackageInterface;
/**
* Downloader for phar files
*
@ -22,7 +24,7 @@ class PharDownloader extends ArchiveDownloader
/**
* {@inheritDoc}
*/
protected function extract($file, $path)
protected function extract(PackageInterface $package, $file, $path)
{
// Can throw an UnexpectedValueException
$archive = new \Phar($file);

View File

@ -18,8 +18,9 @@ use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\IniHelper;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
use RarArchive;
/**
@ -31,15 +32,16 @@ use RarArchive;
*/
class RarDownloader extends ArchiveDownloader
{
/** @var ProcessExecutor */
protected $process;
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
}
protected function extract($file, $path)
protected function extract(PackageInterface $package, $file, $path)
{
$processError = null;

View File

@ -28,7 +28,15 @@ class SvnDownloader extends VcsDownloader
/**
* {@inheritDoc}
*/
public function doDownload(PackageInterface $package, $path, $url)
protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null)
{
}
/**
* {@inheritDoc}
*/
protected function doInstall(PackageInterface $package, $path, $url)
{
SvnUtil::cleanEnv();
$ref = $package->getSourceReference();
@ -42,13 +50,13 @@ class SvnDownloader extends VcsDownloader
}
$this->io->writeError(" Checking out ".$package->getSourceReference());
$this->execute($url, "svn co", sprintf("%s/%s", $url, $ref), null, $path);
$this->execute($package, $url, "svn co", sprintf("%s/%s", $url, $ref), null, $path);
}
/**
* {@inheritDoc}
*/
public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
{
SvnUtil::cleanEnv();
$ref = $target->getSourceReference();
@ -64,7 +72,7 @@ class SvnDownloader extends VcsDownloader
}
$this->io->writeError(" Checking out " . $ref);
$this->execute($url, "svn switch" . $flags, sprintf("%s/%s", $url, $ref), $path);
$this->execute($target, $url, "svn switch" . $flags, sprintf("%s/%s", $url, $ref), $path);
}
/**
@ -93,7 +101,7 @@ class SvnDownloader extends VcsDownloader
* @throws \RuntimeException
* @return string
*/
protected function execute($baseUrl, $command, $url, $cwd = null, $path = null)
protected function execute(PackageInterface $package, $baseUrl, $command, $url, $cwd = null, $path = null)
{
$util = new SvnUtil($baseUrl, $this->io, $this->config);
$util->setCacheCredentials($this->cacheCredentials);
@ -101,7 +109,7 @@ class SvnDownloader extends VcsDownloader
return $util->execute($command, $url, $cwd, $path, $this->io->isVerbose());
} catch (\RuntimeException $e) {
throw new \RuntimeException(
'Package could not be downloaded, '.$e->getMessage()
$package->getPrettyName().' could not be downloaded, '.$e->getMessage()
);
}
}
@ -127,7 +135,7 @@ class SvnDownloader extends VcsDownloader
return ' '.$elem;
}, preg_split('{\s*\r?\n\s*}', $changes));
$countChanges = count($changes);
$this->io->writeError(sprintf(' <error>The package has modified file%s:</error>', $countChanges === 1 ? '' : 's'));
$this->io->writeError(sprintf(' <error>'.$package->getPrettyName().' has modified file%s:</error>', $countChanges === 1 ? '' : 's'));
$this->io->writeError(array_slice($changes, 0, 10));
if ($countChanges > 10) {
$remainingChanges = $countChanges - 10;

View File

@ -12,6 +12,8 @@
namespace Composer\Downloader;
use Composer\Package\PackageInterface;
/**
* Downloader for tar files: tar, tar.gz or tar.bz2
*
@ -22,7 +24,7 @@ class TarDownloader extends ArchiveDownloader
/**
* {@inheritDoc}
*/
protected function extract($file, $path)
protected function extract(PackageInterface $package, $file, $path)
{
// Can throw an UnexpectedValueException
$archive = new \PharData($file);

View File

@ -20,6 +20,7 @@ use Composer\Package\Version\VersionParser;
use Composer\Util\ProcessExecutor;
use Composer\IO\IOInterface;
use Composer\Util\Filesystem;
use React\Promise\PromiseInterface;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
@ -54,44 +55,78 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
/**
* {@inheritDoc}
*/
public function download(PackageInterface $package, $path)
public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null)
{
if (!$package->getSourceReference()) {
throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information');
}
$urls = $this->prepareUrls($package->getSourceUrls());
while ($url = array_shift($urls)) {
try {
return $this->doDownload($package, $path, $url, $prevPackage);
} catch (\Exception $e) {
// rethrow phpunit exceptions to avoid hard to debug bug failures
if ($e instanceof \PHPUnit\Framework\Exception) {
throw $e;
}
if ($this->io->isDebug()) {
$this->io->writeError('Failed: ['.get_class($e).'] '.$e->getMessage());
} elseif (count($urls)) {
$this->io->writeError(' Failed, trying the next URL');
}
if (!count($urls)) {
throw $e;
}
}
}
}
/**
* {@inheritDoc}
*/
public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null)
{
if ($type === 'update') {
$this->cleanChanges($prevPackage, $path, true);
} elseif ($type === 'install') {
$this->filesystem->emptyDirectory($path);
} elseif ($type === 'uninstall') {
$this->cleanChanges($package, $path, false);
}
}
/**
* {@inheritDoc}
*/
public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null)
{
if ($type === 'update') {
// TODO keep track of whether prepare was called for this package
$this->reapplyChanges($path);
}
}
/**
* {@inheritDoc}
*/
public function install(PackageInterface $package, $path)
{
if (!$package->getSourceReference()) {
throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information');
}
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): ", false);
$this->filesystem->emptyDirectory($path);
$urls = $package->getSourceUrls();
$urls = $this->prepareUrls($package->getSourceUrls());
while ($url = array_shift($urls)) {
try {
if (Filesystem::isLocalPath($url)) {
// realpath() below will not understand
// url that starts with "file://"
$needle = 'file://';
$isFileProtocol = false;
if (0 === strpos($url, $needle)) {
$url = substr($url, strlen($needle));
$isFileProtocol = true;
}
// realpath() below will not understand %20 spaces etc.
if (false !== strpos($url, '%')) {
$url = rawurldecode($url);
}
$url = realpath($url);
if ($isFileProtocol) {
$url = $needle . $url;
}
}
$this->doDownload($package, $path, $url);
$this->doInstall($package, $path, $url);
break;
} catch (\Exception $e) {
// rethrow phpunit exceptions to avoid hard to debug bug failures
if ($e instanceof \PHPUnit_Framework_Exception) {
if ($e instanceof \PHPUnit\Framework\Exception) {
throw $e;
}
if ($this->io->isDebug()) {
@ -130,25 +165,21 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
$to = $target->getFullPrettyVersion();
}
$actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Updating' : 'Downgrading';
$actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading';
$this->io->writeError(" - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
$this->cleanChanges($initial, $path, true);
$urls = $target->getSourceUrls();
$urls = $this->prepareUrls($target->getSourceUrls());
$exception = null;
while ($url = array_shift($urls)) {
try {
if (Filesystem::isLocalPath($url)) {
$url = realpath($url);
}
$this->doUpdate($initial, $target, $path, $url);
$exception = null;
break;
} catch (\Exception $exception) {
// rethrow phpunit exceptions to avoid hard to debug bug failures
if ($exception instanceof \PHPUnit_Framework_Exception) {
if ($exception instanceof \PHPUnit\Framework\Exception) {
throw $exception;
}
if ($this->io->isDebug()) {
@ -159,8 +190,6 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
}
}
$this->reapplyChanges($path);
// print the commit logs if in verbose mode and VCS metadata is present
// because in case of missing metadata code would trigger another exception
if (!$exception && $this->io->isVerbose() && $this->hasMetadataRepository($path)) {
@ -196,21 +225,11 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
public function remove(PackageInterface $package, $path)
{
$this->io->writeError(" - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getPrettyVersion() . "</comment>)");
$this->cleanChanges($package, $path, false);
if (!$this->filesystem->removeDirectory($path)) {
throw new \RuntimeException('Could not completely delete '.$path.', aborting.');
}
}
/**
* Download progress information is not available for all VCS downloaders.
* {@inheritDoc}
*/
public function setOutputProgress($outputProgress)
{
return $this;
}
/**
* {@inheritDoc}
*/
@ -244,7 +263,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
}
/**
* Guarantee that no changes have been made to the local copy
* Reapply previously stashes changes if applicable, only called after an update (regardless if successful or not)
*
* @param string $path
* @throws \RuntimeException in case the operation must be aborted or the patch does not apply cleanly
@ -253,14 +272,28 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
{
}
/**
* Downloads data needed to run an install/update later
*
* @param PackageInterface $package package instance
* @param string $path download path
* @param string $url package url
* @param PackageInterface|null $prevPackage previous package (in case of an update)
*
* @return PromiseInterface|null
*/
abstract protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null);
/**
* Downloads specific package into specific folder.
*
* @param PackageInterface $package package instance
* @param string $path download path
* @param string $url package url
*
* @return PromiseInterface|null
*/
abstract protected function doDownload(PackageInterface $package, $path, $url);
abstract protected function doInstall(PackageInterface $package, $path, $url);
/**
* Updates specific package in specific folder from initial to target version.
@ -269,6 +302,8 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
* @param PackageInterface $target updated package
* @param string $path download path
* @param string $url package url
*
* @return PromiseInterface|null
*/
abstract protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url);
@ -290,4 +325,33 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
* @return bool
*/
abstract protected function hasMetadataRepository($path);
private function prepareUrls(array $urls)
{
foreach ($urls as $index => $url) {
if (Filesystem::isLocalPath($url)) {
// realpath() below will not understand
// url that starts with "file://"
$fileProtocol = 'file://';
$isFileProtocol = false;
if (0 === strpos($url, $fileProtocol)) {
$url = substr($url, strlen($fileProtocol));
$isFileProtocol = true;
}
// realpath() below will not understand %20 spaces etc.
if (false !== strpos($url, '%')) {
$url = rawurldecode($url);
}
$urls[$index] = realpath($url);
if ($isFileProtocol) {
$urls[$index] = $fileProtocol . $urls[$index];
}
}
}
return $urls;
}
}

View File

@ -17,7 +17,7 @@ use Composer\Cache;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface;
/**
@ -28,16 +28,17 @@ use Composer\IO\IOInterface;
*/
class XzDownloader extends ArchiveDownloader
{
/** @var ProcessExecutor */
protected $process;
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
}
protected function extract($file, $path)
protected function extract(PackageInterface $package, $file, $path)
{
$command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path);
@ -49,12 +50,4 @@ class XzDownloader extends ArchiveDownloader
throw new \RuntimeException($processError);
}
/**
* {@inheritdoc}
*/
protected function getFileName(PackageInterface $package, $path)
{
return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
}
}

View File

@ -19,7 +19,7 @@ use Composer\Package\PackageInterface;
use Composer\Util\IniHelper;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface;
use Symfony\Component\Process\ExecutableFinder;
use ZipArchive;
@ -33,19 +33,21 @@ class ZipDownloader extends ArchiveDownloader
private static $hasZipArchive;
private static $isWindows;
/** @var ProcessExecutor */
protected $process;
/** @var ZipArchive|null */
private $zipArchiveObject;
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
}
/**
* {@inheritDoc}
*/
public function download(PackageInterface $package, $path, $output = true)
public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
{
if (null === self::$hasSystemUnzip) {
$finder = new ExecutableFinder;
@ -74,7 +76,7 @@ class ZipDownloader extends ArchiveDownloader
}
}
return parent::download($package, $path, $output);
return parent::download($package, $path, $prevPackage, $output);
}
/**
@ -185,7 +187,7 @@ class ZipDownloader extends ArchiveDownloader
* @param string $file File to extract
* @param string $path Path where to extract file
*/
public function extract($file, $path)
public function extract(PackageInterface $package, $file, $path)
{
// Each extract calls its alternative if not available or fails
if (self::$isWindows) {

View File

@ -13,13 +13,16 @@
namespace Composer\EventDispatcher;
use Composer\DependencyResolver\PolicyInterface;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Request;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Transaction;
use Composer\Installer\InstallerEvent;
use Composer\IO\IOInterface;
use Composer\Composer;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\Repository\CompositeRepository;
use Composer\Repository\RepositoryInterface;
use Composer\Repository\RepositorySet;
use Composer\Script;
use Composer\Installer\PackageEvent;
use Composer\Installer\BinaryInstaller;
@ -46,7 +49,7 @@ class EventDispatcher
protected $io;
protected $loader;
protected $process;
protected $listeners;
protected $listeners = array();
private $eventStack;
/**
@ -99,40 +102,34 @@ class EventDispatcher
/**
* Dispatch a package event.
*
* @param string $eventName The constant in PackageEvents
* @param bool $devMode Whether or not we are in dev mode
* @param PolicyInterface $policy The policy
* @param Pool $pool The pool
* @param CompositeRepository $installedRepo The installed repository
* @param Request $request The request
* @param array $operations The list of operations
* @param OperationInterface $operation The package being installed/updated/removed
* @param string $eventName The constant in PackageEvents
* @param bool $devMode Whether or not we are in dev mode
* @param RepositoryInterface $localRepo The installed repository
* @param array $operations The list of operations
* @param OperationInterface $operation The package being installed/updated/removed
*
* @return int return code of the executed script if any, for php scripts a false return
* value is changed to 1, anything else to 0
*/
public function dispatchPackageEvent($eventName, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations, OperationInterface $operation)
public function dispatchPackageEvent($eventName, $devMode, RepositoryInterface $localRepo, array $operations, OperationInterface $operation)
{
return $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $policy, $pool, $installedRepo, $request, $operations, $operation));
return $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $localRepo, $operations, $operation));
}
/**
* Dispatch a installer event.
*
* @param string $eventName The constant in InstallerEvents
* @param bool $devMode Whether or not we are in dev mode
* @param PolicyInterface $policy The policy
* @param Pool $pool The pool
* @param CompositeRepository $installedRepo The installed repository
* @param Request $request The request
* @param array $operations The list of operations
* @param string $eventName The constant in InstallerEvents
* @param bool $devMode Whether or not we are in dev mode
* @param bool $executeOperations True if operations will be executed, false in --dry-run
* @param Transaction $transaction The transaction contains the list of operations
*
* @return int return code of the executed script if any, for php scripts a false return
* value is changed to 1, anything else to 0
*/
public function dispatchInstallerEvent($eventName, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations = array())
public function dispatchInstallerEvent($eventName, $devMode, $executeOperations, Transaction $transaction)
{
return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $policy, $pool, $installedRepo, $request, $operations));
return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $executeOperations, $transaction));
}
/**
@ -160,6 +157,9 @@ class EventDispatcher
throw new \RuntimeException('Subscriber '.$className.'::'.$callable[1].' for event '.$event->getName().' is not callable, make sure the function is defined and public');
}
if (is_array($callable) && (is_string($callable[0]) || is_object($callable[0])) && is_string($callable[1])) {
$this->io->writeError(sprintf('> %s: %s', $event->getName(), (is_object($callable[0]) ? get_class($callable[0]) : $callable[0]).'->'.$callable[1] ), true, IOInterface::VERBOSE);
}
$event = $this->checkListenerExpectedEvent($callable, $event);
$return = false === call_user_func($callable, $event) ? 1 : 0;
} elseif ($this->isComposerScript($callable)) {
@ -172,8 +172,8 @@ class EventDispatcher
$args = array_merge($script, $event->getArguments());
$flags = $event->getFlags();
if (substr($callable, 0, 10) === '@composer ') {
$exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . substr($callable, 9);
if (0 !== ($exitCode = $this->process->execute($exec))) {
$exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . ' ' . implode(' ', $args);
if (0 !== ($exitCode = $this->executeTty($exec))) {
$this->io->writeError(sprintf('<error>Script %s handling the %s event returned with error code '.$exitCode.'</error>', $callable, $event->getName()), true, IOInterface::QUIET);
throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
@ -184,6 +184,7 @@ class EventDispatcher
}
try {
/** @var InstallerEvent $event */
$scriptEvent = new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags);
$scriptEvent->setOriginatingEvent($event);
$return = $this->dispatch($scriptName, $scriptEvent);
@ -247,7 +248,7 @@ class EventDispatcher
}
}
if (0 !== ($exitCode = $this->process->execute($exec))) {
if (0 !== ($exitCode = $this->executeTty($exec))) {
$this->io->writeError(sprintf('<error>Script %s handling the %s event returned with error code '.$exitCode.'</error>', $callable, $event->getName()), true, IOInterface::QUIET);
throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
@ -264,6 +265,15 @@ class EventDispatcher
return $return;
}
protected function executeTty($exec)
{
if ($this->io->isInteractive()) {
return $this->process->executeTty($exec);
}
return $this->process->execute($exec);
}
protected function getPhpExecCommand()
{
$finder = new PhpExecutableFinder();
@ -327,44 +337,6 @@ class EventDispatcher
$expected = $typehint->getName();
// BC support
if (!$event instanceof $expected && $expected === 'Composer\Script\CommandEvent') {
trigger_error('The callback '.$this->serializeCallback($target).' declared at '.$reflected->getDeclaringFunction()->getFileName().' accepts a '.$expected.' but '.$event->getName().' events use a '.get_class($event).' instance. Please adjust your type hint accordingly, see https://getcomposer.org/doc/articles/scripts.md#event-classes', E_USER_DEPRECATED);
$event = new \Composer\Script\CommandEvent(
$event->getName(),
$event->getComposer(),
$event->getIO(),
$event->isDevMode(),
$event->getArguments()
);
}
if (!$event instanceof $expected && $expected === 'Composer\Script\PackageEvent') {
trigger_error('The callback '.$this->serializeCallback($target).' declared at '.$reflected->getDeclaringFunction()->getFileName().' accepts a '.$expected.' but '.$event->getName().' events use a '.get_class($event).' instance. Please adjust your type hint accordingly, see https://getcomposer.org/doc/articles/scripts.md#event-classes', E_USER_DEPRECATED);
$event = new \Composer\Script\PackageEvent(
$event->getName(),
$event->getComposer(),
$event->getIO(),
$event->isDevMode(),
$event->getPolicy(),
$event->getPool(),
$event->getInstalledRepo(),
$event->getRequest(),
$event->getOperations(),
$event->getOperation()
);
}
if (!$event instanceof $expected && $expected === 'Composer\Script\Event') {
trigger_error('The callback '.$this->serializeCallback($target).' declared at '.$reflected->getDeclaringFunction()->getFileName().' accepts a '.$expected.' but '.$event->getName().' events use a '.get_class($event).' instance. Please adjust your type hint accordingly, see https://getcomposer.org/doc/articles/scripts.md#event-classes', E_USER_DEPRECATED);
$event = new \Composer\Script\Event(
$event->getName(),
$event->getComposer(),
$event->getIO(),
$event->isDevMode(),
$event->getArguments(),
$event->getFlags()
);
}
return $event;
}
@ -397,6 +369,22 @@ class EventDispatcher
$this->listeners[$eventName][$priority][] = $listener;
}
/**
* @param callable|object $listener A callable or an object instance for which all listeners should be removed
*/
public function removeListener($listener)
{
foreach ($this->listeners as $eventName => $priorities) {
foreach ($priorities as $priority => $listeners) {
foreach ($listeners as $index => $candidate) {
if ($listener === $candidate || (is_array($candidate) && is_object($listener) && $candidate[0] === $listener)) {
unset($this->listeners[$eventName][$priority][$index]);
}
}
}
}
}
/**
* Adds object methods as listeners for the events in getSubscribedEvents
*
@ -513,7 +501,7 @@ class EventDispatcher
*
* @param Event $event
* @throws \RuntimeException
* @return number
* @return int
*/
protected function pushEvent(Event $event)
{

View File

@ -23,7 +23,8 @@ use Composer\Repository\WritableRepositoryInterface;
use Composer\Util\Filesystem;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\Util\Loop;
use Composer\Util\Silencer;
use Composer\Plugin\PluginEvents;
use Composer\EventDispatcher\Event;
@ -222,6 +223,13 @@ class Factory
return trim(getenv('COMPOSER')) ?: './composer.json';
}
public static function getLockFile($composerFile)
{
return "json" === pathinfo($composerFile, PATHINFO_EXTENSION)
? substr($composerFile, 0, -4).'lock'
: $composerFile . '.lock';
}
public static function createAdditionalStyles()
{
return array(
@ -325,14 +333,15 @@ class Factory
$io->loadConfiguration($config);
}
$rfs = self::createRemoteFilesystem($io, $config);
$httpDownloader = self::createHttpDownloader($io, $config);
$loop = new Loop($httpDownloader);
// initialize event dispatcher
$dispatcher = new EventDispatcher($composer, $io);
$composer->setEventDispatcher($dispatcher);
// initialize repository manager
$rm = RepositoryFactory::manager($io, $config, $dispatcher, $rfs);
$rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher);
$composer->setRepositoryManager($rm);
// load local repository
@ -352,12 +361,12 @@ class Factory
$composer->setPackage($package);
// initialize installation manager
$im = $this->createInstallationManager();
$im = $this->createInstallationManager($loop, $io, $dispatcher);
$composer->setInstallationManager($im);
if ($fullLoad) {
// initialize download manager
$dm = $this->createDownloadManager($io, $config, $dispatcher, $rfs);
$dm = $this->createDownloadManager($io, $config, $httpDownloader, $dispatcher);
$composer->setDownloadManager($dm);
// initialize autoload generator
@ -365,7 +374,7 @@ class Factory
$composer->setAutoloadGenerator($generator);
// initialize archive manager
$am = $this->createArchiveManager($config, $dm);
$am = $this->createArchiveManager($config, $dm, $loop);
$composer->setArchiveManager($am);
}
@ -386,11 +395,9 @@ class Factory
// init locker if possible
if ($fullLoad && isset($composerFile)) {
$lockFile = "json" === pathinfo($composerFile, PATHINFO_EXTENSION)
? substr($composerFile, 0, -4).'lock'
: $composerFile . '.lock';
$lockFile = self::getLockFile($composerFile);
$locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $rm, $im, file_get_contents($composerFile));
$locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $im, file_get_contents($composerFile));
$composer->setLocker($locker);
}
@ -411,7 +418,7 @@ class Factory
/**
* @param IOInterface $io IO instance
* @param bool $disablePlugins Whether plugins should not be loaded
* @return Composer
* @return Composer|null
*/
public static function createGlobal(IOInterface $io, $disablePlugins = false)
{
@ -451,7 +458,7 @@ class Factory
* @param EventDispatcher $eventDispatcher
* @return Downloader\DownloadManager
*/
public function createDownloadManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null)
public function createDownloadManager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null)
{
$cache = null;
if ($config->get('cache-files-ttl') > 0) {
@ -484,14 +491,14 @@ class Factory
$dm->setDownloader('fossil', new Downloader\FossilDownloader($io, $config, $executor, $fs));
$dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs));
$dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config));
$dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
$dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
$dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache, $rfs));
$dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
$dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
$dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache, $rfs));
$dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache, $rfs));
$dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $eventDispatcher, $cache, $rfs));
$dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
$dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
$dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
$dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
return $dm;
}
@ -501,15 +508,9 @@ class Factory
* @param Downloader\DownloadManager $dm Manager use to download sources
* @return Archiver\ArchiveManager
*/
public function createArchiveManager(Config $config, Downloader\DownloadManager $dm = null)
public function createArchiveManager(Config $config, Downloader\DownloadManager $dm, Loop $loop)
{
if (null === $dm) {
$io = new IO\NullIO();
$io->loadConfiguration($config);
$dm = $this->createDownloadManager($io, $config);
}
$am = new Archiver\ArchiveManager($dm);
$am = new Archiver\ArchiveManager($dm, $loop);
$am->addArchiver(new Archiver\ZipArchiver);
$am->addArchiver(new Archiver\PharArchiver);
@ -531,9 +532,9 @@ class Factory
/**
* @return Installer\InstallationManager
*/
protected function createInstallationManager()
public function createInstallationManager(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null)
{
return new Installer\InstallationManager();
return new Installer\InstallationManager($loop, $io, $eventDispatcher);
}
/**
@ -579,10 +580,10 @@ class Factory
/**
* @param IOInterface $io IO instance
* @param Config $config Config instance
* @param array $options Array of options passed directly to RemoteFilesystem constructor
* @return RemoteFilesystem
* @param array $options Array of options passed directly to HttpDownloader constructor
* @return HttpDownloader
*/
public static function createRemoteFilesystem(IOInterface $io, Config $config = null, $options = array())
public static function createHttpDownloader(IOInterface $io, Config $config = null, $options = array())
{
static $warned = false;
$disableTls = false;
@ -596,18 +597,18 @@ class Factory
throw new Exception\NoSslException('The openssl extension is required for SSL/TLS protection but is not available. '
. 'If you can not enable the openssl extension, you can disable this error, at your own risk, by setting the \'disable-tls\' option to true.');
}
$remoteFilesystemOptions = array();
$httpDownloaderOptions = array();
if ($disableTls === false) {
if ($config && $config->get('cafile')) {
$remoteFilesystemOptions['ssl']['cafile'] = $config->get('cafile');
$httpDownloaderOptions['ssl']['cafile'] = $config->get('cafile');
}
if ($config && $config->get('capath')) {
$remoteFilesystemOptions['ssl']['capath'] = $config->get('capath');
$httpDownloaderOptions['ssl']['capath'] = $config->get('capath');
}
$remoteFilesystemOptions = array_replace_recursive($remoteFilesystemOptions, $options);
$httpDownloaderOptions = array_replace_recursive($httpDownloaderOptions, $options);
}
try {
$remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls);
$httpDownloader = new HttpDownloader($io, $config, $httpDownloaderOptions, $disableTls);
} catch (TransportException $e) {
if (false !== strpos($e->getMessage(), 'cafile')) {
$io->write('<error>Unable to locate a valid CA certificate file. You must set a valid \'cafile\' option.</error>');
@ -620,7 +621,7 @@ class Factory
throw $e;
}
return $remoteFilesystem;
return $httpDownloader;
}
/**

View File

@ -14,10 +14,9 @@ namespace Composer\IO;
use Composer\Config;
use Composer\Util\ProcessExecutor;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
abstract class BaseIO implements IOInterface, LoggerInterface
abstract class BaseIO implements IOInterface
{
protected $authentications = array();

View File

@ -13,13 +13,14 @@
namespace Composer\IO;
use Composer\Config;
use Psr\Log\LoggerInterface;
/**
* The Input/Output helper interface.
*
* @author François Pluchino <francois.pluchino@opendisplay.com>
*/
interface IOInterface
interface IOInterface extends LoggerInterface
{
const QUIET = 1;
const NORMAL = 2;
@ -80,6 +81,24 @@ interface IOInterface
*/
public function writeError($messages, $newline = true, $verbosity = self::NORMAL);
/**
* Writes a message to the output, without formatting it.
*
* @param string|array $messages The message as an array of lines or a single string
* @param bool $newline Whether to add a newline or not
* @param int $verbosity Verbosity level from the VERBOSITY_* constants
*/
public function writeRaw($messages, $newline = true, $verbosity = self::NORMAL);
/**
* Writes a message to the error output, without formatting it.
*
* @param string|array $messages The message as an array of lines or a single string
* @param bool $newline Whether to add a newline or not
* @param int $verbosity Verbosity level from the VERBOSITY_* constants
*/
public function writeErrorRaw($messages, $newline = true, $verbosity = self::NORMAL);
/**
* Overwrites a previous message to the output.
*
@ -107,7 +126,7 @@ interface IOInterface
* @param string $default The default answer if none is given by the user
*
* @throws \RuntimeException If there is no data to read in the input stream
* @return string The user answer
* @return string|null The user answer
*/
public function ask($question, $default = null);
@ -145,7 +164,7 @@ interface IOInterface
*
* @param string $question The question to ask
*
* @return string The answer
* @return string|null The answer
*/
public function askAndHideAnswer($question);
@ -160,7 +179,7 @@ interface IOInterface
* @param bool $multiselect Select more than one value separated by comma
*
* @throws \InvalidArgumentException
* @return int|string|array The selected value or values (the key of the choices array)
* @return int|string|array|bool The selected value or values (the key of the choices array)
*/
public function select($question, $choices, $default, $attempts = false, $errorMessage = 'Value "%s" is invalid', $multiselect = false);

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,9 @@ use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation;
use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\StreamContextFactory;
use Composer\Util\Loop;
/**
* Package operation manager.
@ -37,6 +39,16 @@ class InstallationManager
private $installers = array();
private $cache = array();
private $notifiablePackages = array();
private $loop;
private $io;
private $eventDispatcher;
public function __construct(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null)
{
$this->loop = $loop;
$this->io = $io;
$this->eventDispatcher = $eventDispatcher;
}
public function reset()
{
@ -151,13 +163,105 @@ class InstallationManager
/**
* Executes solver operation.
*
* @param RepositoryInterface $repo repository in which to check
* @param OperationInterface $operation operation instance
* @param RepositoryInterface $repo repository in which to add/remove/update packages
* @param OperationInterface[] $operations operations to execute
* @param bool $devMode whether the install is being run in dev mode
* @param bool $operation whether to dispatch script events
*/
public function execute(RepositoryInterface $repo, OperationInterface $operation)
public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true)
{
$method = $operation->getJobType();
$this->$method($repo, $operation);
$promises = array();
foreach ($operations as $operation) {
$opType = $operation->getOperationType();
$promise = null;
if ($opType === 'install') {
$package = $operation->getPackage();
$installer = $this->getInstaller($package->getType());
$promise = $installer->download($package);
} elseif ($opType === 'update') {
$target = $operation->getTargetPackage();
$targetType = $target->getType();
$installer = $this->getInstaller($targetType);
$promise = $installer->download($target, $operation->getInitialPackage());
}
if ($promise) {
$promises[] = $promise;
}
}
if (!empty($promises)) {
$this->loop->wait($promises);
}
foreach ($operations as $operation) {
$opType = $operation->getOperationType();
// ignoring alias ops as they don't need to execute anything
if (!in_array($opType, array('update', 'install', 'uninstall'))) {
// output alias ops in debug verbosity as they have no output otherwise
if ($this->io->isDebug()) {
$this->io->writeError(' - ' . $operation->show(false));
}
$this->$opType($repo, $operation);
continue;
}
if ($opType === 'install' || $opType === 'uninstall') {
$package = $operation->getPackage();
$initialPackage = null;
} elseif ($opType === 'update') {
$package = $operation->getTargetPackage();
$initialPackage = $operation->getInitialPackage();
}
$installer = $this->getInstaller($package->getType());
$event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($opType);
if (defined($event) && $runScripts && $this->eventDispatcher) {
$this->eventDispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation);
}
$dispatcher = $this->eventDispatcher;
$installManager = $this;
$loop = $this->loop;
$io = $this->io;
$promise = $installer->prepare($opType, $package, $initialPackage);
if (null === $promise) {
$promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); });
}
$promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) {
return $installManager->$opType($repo, $operation);
})->then(function () use ($opType, $installer, $package, $initialPackage) {
return $installer->cleanup($opType, $package, $initialPackage);
})->then(function () use ($opType, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) {
$repo->write($devMode, $installManager);
$event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($opType);
if (defined($event) && $runScripts && $dispatcher) {
$dispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation);
}
}, function ($e) use ($opType, $installer, $package, $initialPackage, $loop, $io) {
$io->writeError(' <error>' . ucfirst($opType) .' of '.$package->getPrettyName().' failed</error>');
$promise = $installer->cleanup($opType, $package, $initialPackage);
if ($promise) {
$loop->wait(array($promise));
}
throw $e;
});
$promises[] = $promise;
}
if (!empty($promises)) {
$this->loop->wait($promises);
}
}
/**
@ -170,8 +274,10 @@ class InstallationManager
{
$package = $operation->getPackage();
$installer = $this->getInstaller($package->getType());
$installer->install($repo, $package);
$promise = $installer->install($repo, $package);
$this->markForNotification($package);
return $promise;
}
/**
@ -190,12 +296,15 @@ class InstallationManager
if ($initialType === $targetType) {
$installer = $this->getInstaller($initialType);
$installer->update($repo, $initial, $target);
$promise = $installer->update($repo, $initial, $target);
$this->markForNotification($target);
} else {
$this->getInstaller($initialType)->uninstall($repo, $initial);
$this->getInstaller($targetType)->install($repo, $target);
$installer = $this->getInstaller($targetType);
$promise = $installer->install($repo, $target);
}
return $promise;
}
/**
@ -208,7 +317,8 @@ class InstallationManager
{
$package = $operation->getPackage();
$installer = $this->getInstaller($package->getType());
$installer->uninstall($repo, $package);
return $installer->uninstall($repo, $package);
}
/**

View File

@ -14,18 +14,13 @@ namespace Composer\Installer;
use Composer\Composer;
use Composer\DependencyResolver\PolicyInterface;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Request;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Transaction;
use Composer\EventDispatcher\Event;
use Composer\IO\IOInterface;
use Composer\Repository\CompositeRepository;
use Composer\Repository\RepositorySet;
/**
* An event for all installer.
*
* @author François Pluchino <francois.pluchino@gmail.com>
*/
class InstallerEvent extends Event
{
/**
@ -44,29 +39,14 @@ class InstallerEvent extends Event
private $devMode;
/**
* @var PolicyInterface
* @var bool
*/
private $policy;
private $executeOperations;
/**
* @var Pool
* @var Transaction
*/
private $pool;
/**
* @var CompositeRepository
*/
private $installedRepo;
/**
* @var Request
*/
private $request;
/**
* @var OperationInterface[]
*/
private $operations;
private $transaction;
/**
* Constructor.
@ -75,24 +55,18 @@ class InstallerEvent extends Event
* @param Composer $composer
* @param IOInterface $io
* @param bool $devMode
* @param PolicyInterface $policy
* @param Pool $pool
* @param CompositeRepository $installedRepo
* @param Request $request
* @param OperationInterface[] $operations
* @param bool $executeOperations
* @param Transaction $transaction
*/
public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations = array())
public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, $executeOperations, Transaction $transaction)
{
parent::__construct($eventName);
$this->composer = $composer;
$this->io = $io;
$this->devMode = $devMode;
$this->policy = $policy;
$this->pool = $pool;
$this->installedRepo = $installedRepo;
$this->request = $request;
$this->operations = $operations;
$this->executeOperations = $executeOperations;
$this->transaction = $transaction;
}
/**
@ -120,42 +94,18 @@ class InstallerEvent extends Event
}
/**
* @return PolicyInterface
* @return bool
*/
public function getPolicy()
public function isExecutingOperations()
{
return $this->policy;
return $this->executeOperations;
}
/**
* @return Pool
* @return Transaction|null
*/
public function getPool()
public function getTransaction()
{
return $this->pool;
}
/**
* @return CompositeRepository
*/
public function getInstalledRepo()
{
return $this->installedRepo;
}
/**
* @return Request
*/
public function getRequest()
{
return $this->request;
}
/**
* @return OperationInterface[]
*/
public function getOperations()
{
return $this->operations;
return $this->transaction;
}
}

View File

@ -12,32 +12,15 @@
namespace Composer\Installer;
/**
* The Installer Events.
*
* @author François Pluchino <francois.pluchino@gmail.com>
*/
class InstallerEvents
{
/**
* The PRE_DEPENDENCIES_SOLVING event occurs as a installer begins
* resolve operations.
* The PRE_OPERATIONS_EXEC event occurs before the lock file gets
* installed and operations are executed.
*
* The event listener method receives a
* Composer\Installer\InstallerEvent instance.
* The event listener method receives an Composer\Installer\InstallerEvent instance.
*
* @var string
*/
const PRE_DEPENDENCIES_SOLVING = 'pre-dependencies-solving';
/**
* The POST_DEPENDENCIES_SOLVING event occurs as a installer after
* resolve operations.
*
* The event listener method receives a
* Composer\Installer\InstallerEvent instance.
*
* @var string
*/
const POST_DEPENDENCIES_SOLVING = 'post-dependencies-solving';
const PRE_OPERATIONS_EXEC = 'pre-operations-exec';
}

View File

@ -15,6 +15,7 @@ namespace Composer\Installer;
use Composer\Package\PackageInterface;
use Composer\Repository\InstalledRepositoryInterface;
use InvalidArgumentException;
use React\Promise\PromiseInterface;
/**
* Interface for the package installation manager.
@ -42,20 +43,46 @@ interface InstallerInterface
*/
public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package);
/**
* Downloads the files needed to later install the given package.
*
* @param PackageInterface $package package instance
* @param PackageInterface $prevPackage previous package instance in case of an update
* @return PromiseInterface|null
*/
public function download(PackageInterface $package, PackageInterface $prevPackage = null);
/**
* Do anything that needs to be done between all downloads have been completed and the actual operation is executed
*
* All packages get first downloaded, then all together prepared, then all together installed/updated/uninstalled. Therefore
* for error recovery it is important to avoid failing during install/update/uninstall as much as possible, and risky things or
* user prompts should happen in the prepare step rather. In case of failure, cleanup() will be called so that changes can
* be undone as much as possible.
*
* @param string $type one of install/update/uninstall
* @param PackageInterface $package package instance
* @param PackageInterface $prevPackage previous package instance in case of an update
* @return PromiseInterface|null
*/
public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null);
/**
* Installs specific package.
*
* @param InstalledRepositoryInterface $repo repository in which to check
* @param PackageInterface $package package instance
* @param InstalledRepositoryInterface $repo repository in which to check
* @param PackageInterface $package package instance
* @return PromiseInterface|null
*/
public function install(InstalledRepositoryInterface $repo, PackageInterface $package);
/**
* Updates specific package.
*
* @param InstalledRepositoryInterface $repo repository in which to check
* @param PackageInterface $initial already installed package version
* @param PackageInterface $target updated version
* @param InstalledRepositoryInterface $repo repository in which to check
* @param PackageInterface $initial already installed package version
* @param PackageInterface $target updated version
* @return PromiseInterface|null
*
* @throws InvalidArgumentException if $initial package is not installed
*/
@ -64,11 +91,26 @@ interface InstallerInterface
/**
* Uninstalls specific package.
*
* @param InstalledRepositoryInterface $repo repository in which to check
* @param PackageInterface $package package instance
* @param InstalledRepositoryInterface $repo repository in which to check
* @param PackageInterface $package package instance
* @return PromiseInterface|null
*/
public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package);
/**
* Do anything to cleanup changes applied in the prepare or install/update/uninstall steps
*
* Note that cleanup will be called for all packages regardless if they failed an operation or not, to give
* all installers a change to cleanup things they did previously, so you need to keep track of changes
* applied in the installer/downloader themselves.
*
* @param string $type one of install/update/uninstall
* @param PackageInterface $package package instance
* @param PackageInterface $prevPackage previous package instance in case of an update
* @return PromiseInterface|null
*/
public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null);
/**
* Returns the installation path of a package
*

View File

@ -43,7 +43,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
*
* @param IOInterface $io
* @param Composer $composer
* @param string $type
* @param string|null $type
* @param Filesystem $filesystem
* @param BinaryInstaller $binaryInstaller
*/
@ -85,6 +85,39 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
return (Platform::isWindows() && $this->filesystem->isJunction($installPath)) || is_link($installPath);
}
/**
* {@inheritDoc}
*/
public function download(PackageInterface $package, PackageInterface $prevPackage = null)
{
$this->initializeVendorDir();
$downloadPath = $this->getInstallPath($package);
return $this->downloadManager->download($package, $downloadPath, $prevPackage);
}
/**
* {@inheritDoc}
*/
public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null)
{
$this->initializeVendorDir();
$downloadPath = $this->getInstallPath($package);
return $this->downloadManager->prepare($type, $package, $downloadPath, $prevPackage);
}
/**
* {@inheritDoc}
*/
public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null)
{
$this->initializeVendorDir();
$downloadPath = $this->getInstallPath($package);
return $this->downloadManager->cleanup($type, $package, $downloadPath, $prevPackage);
}
/**
* {@inheritDoc}
*/
@ -194,7 +227,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
protected function installCode(PackageInterface $package)
{
$downloadPath = $this->getInstallPath($package);
$this->downloadManager->download($package, $downloadPath);
$this->downloadManager->install($package, $downloadPath);
}
protected function updateCode(PackageInterface $initial, PackageInterface $target)

View File

@ -47,6 +47,30 @@ class MetapackageInstaller implements InstallerInterface
return $repo->hasPackage($package);
}
/**
* {@inheritDoc}
*/
public function download(PackageInterface $package, PackageInterface $prevPackage = null)
{
// noop
}
/**
* {@inheritDoc}
*/
public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null)
{
// noop
}
/**
* {@inheritDoc}
*/
public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null)
{
// noop
}
/**
* {@inheritDoc}
*/
@ -69,7 +93,7 @@ class MetapackageInstaller implements InstallerInterface
$name = $target->getName();
$from = $initial->getFullPrettyVersion();
$to = $target->getFullPrettyVersion();
$actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Updating' : 'Downgrading';
$actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading';
$this->io->writeError(" - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>)");
$repo->removePackage($initial);

View File

@ -40,6 +40,27 @@ class NoopInstaller implements InstallerInterface
return $repo->hasPackage($package);
}
/**
* {@inheritDoc}
*/
public function download(PackageInterface $package, PackageInterface $prevPackage = null)
{
}
/**
* {@inheritDoc}
*/
public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null)
{
}
/**
* {@inheritDoc}
*/
public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null)
{
}
/**
* {@inheritDoc}
*/

View File

@ -16,19 +16,45 @@ use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\PolicyInterface;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Request;
use Composer\Repository\CompositeRepository;
use Composer\Repository\RepositoryInterface;
use Composer\Repository\RepositorySet;
use Composer\EventDispatcher\Event;
/**
* The Package Event.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class PackageEvent extends InstallerEvent
class PackageEvent extends Event
{
/**
* @var OperationInterface The package instance
* @var Composer
*/
private $composer;
/**
* @var IOInterface
*/
private $io;
/**
* @var bool
*/
private $devMode;
/**
* @var RepositoryInterface
*/
private $localRepo;
/**
* @var OperationInterface[]
*/
private $operations;
/**
* @var OperationInterface The operation instance which is being executed
*/
private $operation;
@ -39,20 +65,63 @@ class PackageEvent extends InstallerEvent
* @param Composer $composer
* @param IOInterface $io
* @param bool $devMode
* @param PolicyInterface $policy
* @param Pool $pool
* @param CompositeRepository $installedRepo
* @param RepositoryInterface $localRepo
* @param Request $request
* @param OperationInterface[] $operations
* @param OperationInterface $operation
*/
public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations, OperationInterface $operation)
public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, RepositoryInterface $localRepo, array $operations = array(), OperationInterface $operation)
{
parent::__construct($eventName, $composer, $io, $devMode, $policy, $pool, $installedRepo, $request, $operations);
parent::__construct($eventName);
$this->composer = $composer;
$this->io = $io;
$this->devMode = $devMode;
$this->localRepo = $localRepo;
$this->operations = $operations;
$this->operation = $operation;
}
/**
* @return Composer
*/
public function getComposer()
{
return $this->composer;
}
/**
* @return IOInterface
*/
public function getIO()
{
return $this->io;
}
/**
* @return bool
*/
public function isDevMode()
{
return $this->devMode;
}
/**
* @return RepositoryInterface
*/
public function getLocalRepo()
{
return $this->localRepo;
}
/**
* @return OperationInterface[]
*/
public function getOperations()
{
return $this->operations;
}
/**
* Returns the package instance.
*

View File

@ -50,19 +50,27 @@ class PluginInstaller extends LibraryInstaller
/**
* {@inheritDoc}
*/
public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
public function download(PackageInterface $package, PackageInterface $prevPackage = null)
{
$extra = $package->getExtra();
if (empty($extra['class'])) {
throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.');
}
return parent::download($package, $prevPackage);
}
/**
* {@inheritDoc}
*/
public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
{
parent::install($repo, $package);
try {
$this->composer->getPluginManager()->registerPackage($package, true);
} catch (\Exception $e) {
// Rollback installation
$this->io->writeError('Plugin installation failed, rolling back');
$this->io->writeError('Plugin initialization failed, uninstalling plugin');
parent::uninstall($repo, $package);
throw $e;
}
@ -73,12 +81,22 @@ class PluginInstaller extends LibraryInstaller
*/
public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target)
{
$extra = $target->getExtra();
if (empty($extra['class'])) {
throw new \UnexpectedValueException('Error while installing '.$target->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.');
}
parent::update($repo, $initial, $target);
$this->composer->getPluginManager()->registerPackage($target, true);
try {
$this->composer->getPluginManager()->deactivatePackage($initial, true);
$this->composer->getPluginManager()->registerPackage($target, true);
} catch (\Exception $e) {
// Rollback installation
$this->io->writeError('Plugin initialization failed, uninstalling plugin');
parent::uninstall($repo, $target);
throw $e;
}
}
public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package)
{
$this->composer->getPluginManager()->uninstallPackage($package, true);
parent::uninstall($repo, $package);
}
}

View File

@ -58,7 +58,7 @@ class ProjectInstaller implements InstallerInterface
/**
* {@inheritDoc}
*/
public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
public function download(PackageInterface $package, PackageInterface $prevPackage = null)
{
$installPath = $this->installPath;
if (file_exists($installPath) && !$this->filesystem->isDirEmpty($installPath)) {
@ -67,7 +67,32 @@ class ProjectInstaller implements InstallerInterface
if (!is_dir($installPath)) {
mkdir($installPath, 0777, true);
}
$this->downloadManager->download($package, $installPath);
return $this->downloadManager->download($package, $installPath, $prevPackage);
}
/**
* {@inheritDoc}
*/
public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null)
{
$this->downloadManager->prepare($type, $package, $this->installPath, $prevPackage);
}
/**
* {@inheritDoc}
*/
public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null)
{
$this->downloadManager->cleanup($type, $package, $this->installPath, $prevPackage);
}
/**
* {@inheritDoc}
*/
public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
{
$this->downloadManager->install($package, $this->installPath);
}
/**

View File

@ -24,6 +24,10 @@ use Symfony\Component\Console\Formatter\OutputFormatter;
*/
class SuggestedPackagesReporter
{
const MODE_LIST = 1;
const MODE_BY_PACKAGE = 2;
const MODE_BY_SUGGESTION = 4;
/**
* @var array
*/
@ -91,38 +95,105 @@ class SuggestedPackagesReporter
/**
* Output suggested packages.
*
* Do not list the ones already installed if installed repository provided.
*
* @param RepositoryInterface $installedRepo Installed packages
* @param int $mode One of the MODE_* constants from this class
* @return SuggestedPackagesReporter
*/
public function output(RepositoryInterface $installedRepo = null)
public function output($mode, RepositoryInterface $installedRepo = null)
{
$suggestedPackages = $this->getFilteredSuggestions($installedRepo);
$suggesters = array();
$suggested = array();
foreach ($suggestedPackages as $suggestion) {
$suggesters[$suggestion['source']][$suggestion['target']] = $suggestion['reason'];
$suggested[$suggestion['target']][$suggestion['source']] = $suggestion['reason'];
}
ksort($suggesters);
ksort($suggested);
// Simple mode
if ($mode & self::MODE_LIST) {
foreach (array_keys($suggested) as $name) {
$this->io->write(sprintf('<info>%s</info>', $name));
}
return 0;
}
// Grouped by package
if ($mode & self::MODE_BY_PACKAGE) {
foreach ($suggesters as $suggester => $suggestions) {
$this->io->write(sprintf('<comment>%s</comment> suggests:', $suggester));
foreach ($suggestions as $suggestion => $reason) {
$this->io->write(sprintf(' - <info>%s</info>' . ($reason ? ': %s' : ''), $suggestion, $this->escapeOutput($reason)));
}
$this->io->write('');
}
}
// Grouped by suggestion
if ($mode & self::MODE_BY_SUGGESTION) {
// Improve readability in full mode
if ($mode & self::MODE_BY_PACKAGE) {
$this->io->write(str_repeat('-', 78));
}
foreach ($suggested as $suggestion => $suggesters) {
$this->io->write(sprintf('<comment>%s</comment> is suggested by:', $suggestion));
foreach ($suggesters as $suggester => $reason) {
$this->io->write(sprintf(' - <info>%s</info>' . ($reason ? ': %s' : ''), $suggester, $this->escapeOutput($reason)));
}
$this->io->write('');
}
}
return $this;
}
/**
* Output number of new suggested packages and a hint to use suggest command.
**
* Do not list the ones already installed if installed repository provided.
*
* @return SuggestedPackagesReporter
*/
public function outputMinimalistic(RepositoryInterface $installedRepo = null)
{
$suggestedPackages = $this->getFilteredSuggestions($installedRepo);
if ($suggestedPackages) {
$this->io->writeError('<info>'.count($suggestedPackages).' package suggestions were added by new dependencies, use `composer suggest` to see details.</info>');
}
return $this;
}
private function getFilteredSuggestions(RepositoryInterface $installedRepo = null)
{
$suggestedPackages = $this->getPackages();
$installedPackages = array();
if (null !== $installedRepo && ! empty($suggestedPackages)) {
$installedNames = array();
if (null !== $installedRepo && !empty($suggestedPackages)) {
foreach ($installedRepo->getPackages() as $package) {
$installedPackages = array_merge(
$installedPackages,
$installedNames = array_merge(
$installedNames,
$package->getNames()
);
}
}
$suggestions = array();
foreach ($suggestedPackages as $suggestion) {
if (in_array($suggestion['target'], $installedPackages)) {
if (in_array($suggestion['target'], $installedNames)) {
continue;
}
$this->io->writeError(sprintf(
'%s suggests installing %s%s',
$suggestion['source'],
$this->escapeOutput($suggestion['target']),
$this->escapeOutput('' !== $suggestion['reason'] ? ' ('.$suggestion['reason'].')' : '')
));
$suggestions[] = $suggestion;
}
return $this;
return $suggestions;
}
/**

View File

@ -15,7 +15,7 @@ namespace Composer\Json;
use JsonSchema\Validator;
use Seld\JsonLint\JsonParser;
use Seld\JsonLint\ParsingException;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface;
use Composer\Downloader\TransportException;
@ -37,25 +37,25 @@ class JsonFile
const COMPOSER_SCHEMA_PATH = '/../../../res/composer-schema.json';
private $path;
private $rfs;
private $httpDownloader;
private $io;
/**
* Initializes json file reader/parser.
*
* @param string $path path to a lockfile
* @param RemoteFilesystem $rfs required for loading http/https json files
* @param string $path path to a lockfile
* @param HttpDownloader $httpDownloader required for loading http/https json files
* @param IOInterface $io
* @throws \InvalidArgumentException
*/
public function __construct($path, RemoteFilesystem $rfs = null, IOInterface $io = null)
public function __construct($path, HttpDownloader $httpDownloader = null, IOInterface $io = null)
{
$this->path = $path;
if (null === $rfs && preg_match('{^https?://}i', $path)) {
throw new \InvalidArgumentException('http urls require a RemoteFilesystem instance to be passed');
if (null === $httpDownloader && preg_match('{^https?://}i', $path)) {
throw new \InvalidArgumentException('http urls require a HttpDownloader instance to be passed');
}
$this->rfs = $rfs;
$this->httpDownloader = $httpDownloader;
$this->io = $io;
}
@ -86,8 +86,8 @@ class JsonFile
public function read()
{
try {
if ($this->rfs) {
$json = $this->rfs->getContents($this->path, $this->path, false);
if ($this->httpDownloader) {
$json = $this->httpDownloader->get($this->path)->getBody();
} else {
if ($this->io && $this->io->isDebug()) {
$this->io->writeError('Reading ' . $this->path);

View File

@ -416,4 +416,9 @@ class AliasPackage extends BasePackage implements CompletePackageInterface
{
return $this->aliasOf->setDistType($type);
}
public function setSourceDistReferences($reference)
{
return $this->aliasOf->setSourceDistReferences($reference);
}
}

View File

@ -16,6 +16,7 @@ use Composer\Downloader\DownloadManager;
use Composer\Package\PackageInterface;
use Composer\Package\RootPackageInterface;
use Composer\Util\Filesystem;
use Composer\Util\Loop;
use Composer\Json\JsonFile;
/**
@ -25,6 +26,7 @@ use Composer\Json\JsonFile;
class ArchiveManager
{
protected $downloadManager;
protected $loop;
protected $archivers = array();
@ -36,9 +38,10 @@ class ArchiveManager
/**
* @param DownloadManager $downloadManager A manager used to download package sources
*/
public function __construct(DownloadManager $downloadManager)
public function __construct(DownloadManager $downloadManager, Loop $loop)
{
$this->downloadManager = $downloadManager;
$this->loop = $loop;
}
/**
@ -149,7 +152,9 @@ class ArchiveManager
try {
// Download sources
$this->downloadManager->download($package, $sourcePath);
$promise = $this->downloadManager->download($package, $sourcePath);
$this->loop->wait(array($promise));
$this->downloadManager->install($package, $sourcePath);
} catch (\Exception $e) {
$filesystem->removeDirectory($sourcePath);
throw $e;

View File

@ -210,18 +210,30 @@ abstract class BasePackage implements PackageInterface
/**
* {@inheritDoc}
*/
public function getFullPrettyVersion($truncate = true)
public function getFullPrettyVersion($truncate = true, $displayMode = PackageInterface::DISPLAY_SOURCE_REF_IF_DEV)
{
if (!$this->isDev() || !in_array($this->getSourceType(), array('hg', 'git'))) {
if ($displayMode === PackageInterface::DISPLAY_SOURCE_REF_IF_DEV &&
(!$this->isDev() || !in_array($this->getSourceType(), array('hg', 'git')))
) {
return $this->getPrettyVersion();
}
// if source reference is a sha1 hash -- truncate
if ($truncate && strlen($this->getSourceReference()) === 40) {
return $this->getPrettyVersion() . ' ' . substr($this->getSourceReference(), 0, 7);
switch ($displayMode) {
case PackageInterface::DISPLAY_SOURCE_REF_IF_DEV:
case PackageInterface::DISPLAY_SOURCE_REF:
$reference = $this->getSourceReference();
break;
case PackageInterface::DISPLAY_DIST_REF:
$reference = $this->getDistReference();
break;
}
return $this->getPrettyVersion() . ' ' . $this->getSourceReference();
// if source reference is a sha1 hash -- truncate
if ($truncate && strlen($reference) === 40) {
return $this->getPrettyVersion() . ' ' . substr($reference, 0, 7);
}
return $this->getPrettyVersion() . ' ' . $reference;
}
public function getStabilityPriority()
@ -238,14 +250,14 @@ abstract class BasePackage implements PackageInterface
/**
* Build a regexp from a package name, expanding * globs as required
*
* @param string $whiteListedPattern
* @param string $allowPattern
* @param string $wrap Wrap the cleaned string by the given string
* @return string
*/
public static function packageNameToRegexp($whiteListedPattern, $wrap = '{^%s$}i')
public static function packageNameToRegexp($allowPattern, $wrap = '{^%s$}i')
{
$cleanedWhiteListedPattern = str_replace('\\*', '.*', preg_quote($whiteListedPattern));
$cleanedAllowPattern = str_replace('\\*', '.*', preg_quote($allowPattern));
return sprintf($wrap, $cleanedWhiteListedPattern);
return sprintf($wrap, $cleanedAllowPattern);
}
}

Some files were not shown because too many files have changed in this diff Show More