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 .travis.yml export-ignore
appveyor.yml export-ignore appveyor.yml export-ignore
phpunit.xml.dist export-ignore phpunit.xml.dist export-ignore
/phpstan/ export-ignore

View File

@ -1,6 +1,6 @@
language: php language: php
dist: trusty dist: bionic
git: git:
depth: 5 depth: 5
@ -9,27 +9,40 @@ cache:
directories: directories:
- $HOME/.composer/cache - $HOME/.composer/cache
addons:
apt:
packages:
- parallel
matrix: matrix:
include: include:
- php: 5.3 - php: 5.3
dist: precise dist: precise
- php: 5.4 - php: 5.4
dist: trusty
- php: 5.5 - php: 5.5
dist: trusty
- php: 5.6 - php: 5.6
dist: xenial
- php: 7.0 - php: 7.0
dist: xenial
- php: 7.1 - php: 7.1
dist: xenial
- php: 7.2 - php: 7.2
dist: xenial
- php: 7.3 - 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 - php: 7.4
env: env:
- deps=high - deps=high
- SYMFONY_PHPUNIT_VERSION=7.5 - 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 fast_finish: true
allow_failures: allow_failures:
- php: nightly - php: nightly
@ -44,9 +57,9 @@ before_install:
install: install:
# flags to pass to 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 # 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 # install dependencies using system provided composer binary
- composer install $flags - composer install $flags
# install dependencies using composer from source # install dependencies using composer from source
@ -58,9 +71,13 @@ before_script:
- git config --global user.email travis@example.com - git config --global user.email travis@example.com
script: script:
- ./vendor/bin/simple-phpunit - if [[ $PHPSTAN == "1" ]]; then
# run test suite directories in parallel using GNU parallel bin/composer require --dev phpstan/phpstan:^0.12 phpunit/phpunit:^7.5 --no-update &&
# - 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);' bin/composer update phpstan/* phpunit/* sebastian/* --with-dependencies &&
vendor/bin/phpstan analyse --configuration=phpstan/config.neon;
else
vendor/bin/simple-phpunit;
fi
before_deploy: before_deploy:
- php -d phar.readonly=0 bin/compile - php -d phar.readonly=0 bin/compile
@ -73,4 +90,4 @@ deploy:
on: on:
tags: true tags: true
repo: composer/composer repo: composer/composer
php: '7.2' php: '7.3'

View File

@ -3,7 +3,7 @@ clone_depth: 5
environment: environment:
# This sets the PHP version (from Chocolatey) # 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_CACHE: C:\tools\phpci
PHPCI_PHP: C:\tools\phpci\php PHPCI_PHP: C:\tools\phpci\php
PHPCI_COMPOSER: C:\tools\phpci\composer PHPCI_COMPOSER: C:\tools\phpci\composer
@ -25,6 +25,15 @@ install:
- IF %PHP%==0 cinst composer -i -y --ia "/DEV=%PHPCI_COMPOSER%" - IF %PHP%==0 cinst composer -i -y --ia "/DEV=%PHPCI_COMPOSER%"
- php -v - php -v
- IF %PHP%==0 (composer --version) ELSE (composer self-update) - 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% - cd %APPVEYOR_BUILD_FOLDER%
- composer install --prefer-dist --no-progress - composer install --prefer-dist --no-progress

View File

@ -24,7 +24,7 @@
"require": { "require": {
"php": "^5.3.2 || ^7.0", "php": "^5.3.2 || ^7.0",
"composer/ca-bundle": "^1.0", "composer/ca-bundle": "^1.0",
"composer/semver": "^1.0", "composer/semver": "^2.0@dev",
"composer/spdx-licenses": "^1.2", "composer/spdx-licenses": "^1.2",
"composer/xdebug-handler": "^1.1", "composer/xdebug-handler": "^1.1",
"justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0", "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",
@ -34,7 +34,8 @@
"symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0", "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0",
"symfony/filesystem": "^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/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": { "conflict": {
"symfony/console": "2.8.38" "symfony/console": "2.8.38"
@ -55,7 +56,7 @@
}, },
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.10-dev" "dev-master": "2.0-dev"
} }
}, },
"autoload": { "autoload": {
@ -65,8 +66,13 @@
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "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": [
"bin/composer" "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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "cc6f9640996dfad00a5b03a8be01a571", "content-hash": "a0a9399315ac0b612d4296b8df745112",
"packages": [ "packages": [
{ {
"name": "composer/ca-bundle", "name": "composer/ca-bundle",
@ -60,20 +60,25 @@
"ssl", "ssl",
"tls" "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" "time": "2020-01-13T10:02:55+00:00"
}, },
{ {
"name": "composer/semver", "name": "composer/semver",
"version": "1.5.1", "version": "2.0.x-dev",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/composer/semver.git", "url": "https://github.com/composer/semver.git",
"reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de" "reference": "4df5ff3249f01018504939d66040d8d2b783d820"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de", "url": "https://api.github.com/repos/composer/semver/zipball/4df5ff3249f01018504939d66040d8d2b783d820",
"reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de", "reference": "4df5ff3249f01018504939d66040d8d2b783d820",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -85,7 +90,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.x-dev" "dev-master": "2.x-dev"
} }
}, },
"autoload": { "autoload": {
@ -121,7 +126,22 @@
"validation", "validation",
"versioning" "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", "name": "composer/spdx-licenses",
@ -181,6 +201,11 @@
"spdx", "spdx",
"validator" "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" "time": "2020-02-14T07:44:31+00:00"
}, },
{ {
@ -225,6 +250,11 @@
"Xdebug", "Xdebug",
"performance" "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": [ "funding": [
{ {
"url": "https://packagist.com", "url": "https://packagist.com",
@ -353,6 +383,44 @@
}, },
"time": "2019-11-01T11:05:21+00:00" "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", "name": "seld/jsonlint",
"version": "1.7.2", "version": "1.7.2",
@ -448,6 +516,10 @@
"keywords": [ "keywords": [
"phar" "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" "time": "2020-02-14T15:25:33+00:00"
}, },
{ {
@ -735,6 +807,9 @@
"polyfill", "polyfill",
"portable" "portable"
], ],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/master"
},
"time": "2020-01-13T11:15:53+00:00" "time": "2020-01-13T11:15:53+00:00"
}, },
{ {
@ -794,6 +869,9 @@
"portable", "portable",
"shim" "shim"
], ],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/master"
},
"time": "2020-01-13T11:15:53+00:00" "time": "2020-01-13T11:15:53+00:00"
}, },
{ {
@ -949,6 +1027,16 @@
"license": [ "license": [
"MIT" "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" "time": "2016-01-25T08:17:30+00:00"
}, },
{ {
@ -1012,6 +1100,10 @@
"spy", "spy",
"stub" "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" "time": "2020-03-05T15:02:03+00:00"
}, },
{ {
@ -1076,6 +1168,10 @@
"compare", "compare",
"equality" "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" "time": "2017-01-29T09:50:25+00:00"
}, },
{ {
@ -1128,6 +1224,10 @@
"keywords": [ "keywords": [
"diff" "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" "time": "2017-05-22T07:24:03+00:00"
}, },
{ {
@ -1195,6 +1295,10 @@
"export", "export",
"exporter" "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" "time": "2016-11-19T08:54:04+00:00"
}, },
{ {
@ -1248,6 +1352,10 @@
], ],
"description": "Provides functionality to recursively process PHP variables", "description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context", "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" "time": "2016-11-19T07:33:16+00:00"
}, },
{ {
@ -1313,6 +1421,9 @@
], ],
"description": "Symfony PHPUnit Bridge", "description": "Symfony PHPUnit Bridge",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/phpunit-bridge/tree/v3.4.38"
},
"funding": [ "funding": [
{ {
"url": "https://symfony.com/sponsor", "url": "https://symfony.com/sponsor",
@ -1332,7 +1443,9 @@
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": [], "stability-flags": {
"composer/semver": 20
},
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
@ -1342,5 +1455,5 @@
"platform-overrides": { "platform-overrides": {
"php": "5.3.9" "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 > if the `composer.lock` has not been updated since changes were made to the
> `composer.json` that might affect dependency resolution. > `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 ```sh
php composer.phar update monolog/monolog [...] php composer.phar update monolog/monolog [...]

View File

@ -106,7 +106,6 @@ resolution.
* **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--no-scripts:** Skips execution of scripts defined in `composer.json`.
* **--no-progress:** Removes the progress display that can mess with some * **--no-progress:** Removes the progress display that can mess with some
terminals or scripts which don't handle backspace characters. 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 * **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster
autoloader. This is recommended especially for production, but can take autoloader. This is recommended especially for production, but can take
a bit of time to run so it is currently not done by default. 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-scripts:** Skips execution of scripts defined in `composer.json`.
* **--no-progress:** Removes the progress display that can mess with some * **--no-progress:** Removes the progress display that can mess with some
terminals or scripts which don't handle backspace characters. terminals or scripts which don't handle backspace characters.
* **--no-suggest:** Skips suggested packages in the output. * **--with-dependencies:** Update also dependencies of packages in the argument list, except those which are root requirements.
* **--with-dependencies:** Add also dependencies of whitelisted packages to the whitelist, except those that are root requirements. * **--with-all-dependencies:** Update also dependencies of packages in the argument list, including those which are root requirements.
* **--with-all-dependencies:** Add also all dependencies of whitelisted packages to the whitelist, including those that are root requirements.
* **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster * **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster
autoloader. This is recommended especially for production, but can take autoloader. This is recommended especially for production, but can take
a bit of time to run so it is currently not done by default. 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 ### Options
* **--dev:** Add packages to `require-dev`. * **--dev:** Add packages to `require-dev`.
* **--dry-run:** Simulate the command without actually doing anything.
* **--prefer-source:** Install packages from `source` when available. * **--prefer-source:** Install packages from `source` when available.
* **--prefer-dist:** Install packages from `dist` when available. * **--prefer-dist:** Install packages from `dist` when available.
* **--no-progress:** Removes the progress display that can mess with some * **--no-progress:** Removes the progress display that can mess with some
terminals or scripts which don't handle backspace characters. 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-update:** Disables the automatic update of the dependencies.
* **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--no-scripts:** Skips execution of scripts defined in `composer.json`.
* **--update-no-dev:** Run the dependency update with the `--no-dev` option. * **--update-no-dev:** Run the dependency update with the `--no-dev` option.
@ -236,6 +234,7 @@ uninstalled.
### Options ### Options
* **--dev:** Remove packages from `require-dev`. * **--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 * **--no-progress:** Removes the progress display that can mess with some
terminals or scripts which don't handle backspace characters. terminals or scripts which don't handle backspace characters.
* **--no-update:** Disables the automatic update of the dependencies. * **--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` optionally pass one or multiple package names in the format of `vendor/package`
to limit output to suggestions made by those packages only. 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. the package offering the suggestions or the suggested packages respectively.
Use the `--verbose (-v)` flag to display the suggesting package and the suggestion reason. If you only want a list of suggested package names, use `--list`.
This implies `--by-package --by-suggestion`, showing both lists.
### Options ### Options
* **--by-package:** Groups output by suggesting package. * **--by-package:** Groups output by suggesting package (default).
* **--by-suggestion:** Groups output by suggested package. * **--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. * **--no-dev:** Excludes suggestions from `require-dev` packages.
## fund ## 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 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. 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) → ← [Libraries](02-libraries.md) | [Schema](04-schema.md) →

View File

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

View File

@ -43,8 +43,8 @@ Composer fires the following named events during its execution process:
### Installer Events ### Installer Events
- **pre-dependencies-solving**: occurs before the dependencies are resolved. - **pre-operations-exec**: occurs before the install/upgrade/.. operations
- **post-dependencies-solving**: occurs after the dependencies have been resolved. are executed when installing a lock file.
### Package Events ### 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 - **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. provides you with access to the input and output objects of the program.
- **pre-file-download**: occurs before files are downloaded and allows - **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. based on the URL to be downloaded.
- **pre-command-run**: occurs before a command is executed and allows you to - **pre-command-run**: occurs before a command is executed and allows you to
manipulate the `InputInterface` object's options and arguments to tweak manipulate the `InputInterface` object's options and arguments to tweak
a command's behavior. 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 > **Note:** Composer makes no assumptions about the state of your dependencies
> prior to `install` or `update`. Therefore, you should not specify scripts > 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); $classes = self::findClasses($filePath);
if (null !== $autoloadType) { 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 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; $scannedFiles[$realPath] = true;
} }
} else { } else {
@ -126,8 +126,7 @@ class ClassMapGenerator
foreach ($classes as $class) { foreach ($classes as $class) {
// skip classes not within the given namespace prefix // 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; continue;
} }
@ -196,19 +195,15 @@ class ClassMapGenerator
// warn only if no valid classes, else silently skip invalid // warn only if no valid classes, else silently skip invalid
if (empty($validClasses)) { if (empty($validClasses)) {
foreach ($rejectedClasses as $class) { foreach ($rejectedClasses as $class) {
trigger_error( if ($io) {
"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.", $io->writeError("<warning>Class $class located in ".preg_replace('{^'.preg_quote(getcwd()).'}', '.', $filePath, 1)." does not comply with $namespaceType autoloading standard. Skipping.</warning>");
E_USER_DEPRECATED }
);
} }
// TODO enable in Composer 2.0 return array();
//return array();
} }
// TODO enable in Composer 2.0 & unskip test in AutoloadGeneratorTest::testPSRToClassMapIgnoresNonPSRClasses return $validClasses;
//return $validClasses;
return array($classes, $validClasses);
} }
/** /**

View File

@ -28,20 +28,20 @@ class Cache
private $io; private $io;
private $root; private $root;
private $enabled = true; private $enabled = true;
private $whitelist; private $allowlist;
private $filesystem; private $filesystem;
/** /**
* @param IOInterface $io * @param IOInterface $io
* @param string $cacheDir location of the cache * @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 * @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->io = $io;
$this->root = rtrim($cacheDir, '/\\') . '/'; $this->root = rtrim($cacheDir, '/\\') . '/';
$this->whitelist = $whitelist; $this->allowlist = $allowlist;
$this->filesystem = $filesystem ?: new Filesystem(); $this->filesystem = $filesystem ?: new Filesystem();
if (!self::isUsable($cacheDir)) { if (!self::isUsable($cacheDir)) {
@ -77,7 +77,7 @@ class Cache
public function read($file) public function read($file)
{ {
if ($this->enabled) { if ($this->enabled) {
$file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
if (file_exists($this->root . $file)) { if (file_exists($this->root . $file)) {
$this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG); $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG);
@ -91,7 +91,7 @@ class Cache
public function write($file, $contents) public function write($file, $contents)
{ {
if ($this->enabled) { 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); $this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG);
@ -129,7 +129,7 @@ class Cache
public function copyFrom($file, $source) public function copyFrom($file, $source)
{ {
if ($this->enabled) { if ($this->enabled) {
$file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
$this->filesystem->ensureDirectoryExists(dirname($this->root . $file)); $this->filesystem->ensureDirectoryExists(dirname($this->root . $file));
if (!file_exists($source)) { if (!file_exists($source)) {
@ -150,7 +150,7 @@ class Cache
public function copyTo($file, $target) public function copyTo($file, $target)
{ {
if ($this->enabled) { if ($this->enabled) {
$file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
if (file_exists($this->root . $file)) { if (file_exists($this->root . $file)) {
try { try {
touch($this->root . $file, filemtime($this->root . $file), time()); touch($this->root . $file, filemtime($this->root . $file), time());
@ -177,7 +177,7 @@ class Cache
public function remove($file) public function remove($file)
{ {
if ($this->enabled) { if ($this->enabled) {
$file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
if (file_exists($this->root . $file)) { if (file_exists($this->root . $file)) {
return $this->filesystem->unlink($this->root . $file); return $this->filesystem->unlink($this->root . $file);
} }
@ -229,7 +229,7 @@ class Cache
public function sha1($file) public function sha1($file)
{ {
if ($this->enabled) { if ($this->enabled) {
$file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
if (file_exists($this->root . $file)) { if (file_exists($this->root . $file)) {
return sha1_file($this->root . $file); return sha1_file($this->root . $file);
} }
@ -241,7 +241,7 @@ class Cache
public function sha256($file) public function sha256($file)
{ {
if ($this->enabled) { if ($this->enabled) {
$file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
if (file_exists($this->root . $file)) { if (file_exists($this->root . $file)) {
return hash_file('sha256', $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\CommandEvent;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Loop;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@ -111,8 +112,9 @@ EOT
$archiveManager = $composer->getArchiveManager(); $archiveManager = $composer->getArchiveManager();
} else { } else {
$factory = new Factory; $factory = new Factory;
$downloadManager = $factory->createDownloadManager($io, $config); $httpDownloader = $factory->createHttpDownloader($io, $config);
$archiveManager = $factory->createArchiveManager($config, $downloadManager); $downloadManager = $factory->createDownloadManager($io, $config, $httpDownloader);
$archiveManager = $factory->createArchiveManager($config, $downloadManager, new Loop($httpDownloader));
} }
if ($packageName) { if ($packageName) {

View File

@ -27,6 +27,8 @@ use Symfony\Component\Console\Command\Command;
/** /**
* Base class for Composer commands * Base class for Composer commands
* *
* @method Application getApplication()
*
* @author Ryan Weaver <ryan@knplabs.com> * @author Ryan Weaver <ryan@knplabs.com>
* @author Konstantin Kudryashov <ever.zet@gmail.com> * @author Konstantin Kudryashov <ever.zet@gmail.com>
*/ */
@ -46,7 +48,7 @@ abstract class BaseCommand extends Command
* @param bool $required * @param bool $required
* @param bool|null $disablePlugins * @param bool|null $disablePlugins
* @throws \RuntimeException * @throws \RuntimeException
* @return Composer * @return Composer|null
*/ */
public function getComposer($required = true, $disablePlugins = 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'))) { 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')); $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); return array($preferSource, $preferDist);

View File

@ -12,11 +12,12 @@
namespace Composer\Command; namespace Composer\Command;
use Composer\DependencyResolver\Pool;
use Composer\Package\Link; use Composer\Package\Link;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Repository\ArrayRepository; use Composer\Repository\InstalledArrayRepository;
use Composer\Repository\CompositeRepository; use Composer\Repository\CompositeRepository;
use Composer\Repository\RootPackageRepository;
use Composer\Repository\InstalledRepository;
use Composer\Repository\PlatformRepository; use Composer\Repository\PlatformRepository;
use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositoryFactory;
use Composer\Plugin\CommandEvent; use Composer\Plugin\CommandEvent;
@ -71,15 +72,12 @@ class BaseDependencyCommand extends BaseCommand
$commandEvent = new CommandEvent(PluginEvents::COMMAND, $this->getName(), $input, $output); $commandEvent = new CommandEvent(PluginEvents::COMMAND, $this->getName(), $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
// Prepare repositories and set up a pool
$platformOverrides = $composer->getConfig()->get('platform') ?: array(); $platformOverrides = $composer->getConfig()->get('platform') ?: array();
$repository = new CompositeRepository(array( $installedRepo = new InstalledRepository(array(
new ArrayRepository(array($composer->getPackage())), new RootPackageRepository($composer->getPackage()),
$composer->getRepositoryManager()->getLocalRepository(), $composer->getRepositoryManager()->getLocalRepository(),
new PlatformRepository(array(), $platformOverrides), new PlatformRepository(array(), $platformOverrides),
)); ));
$pool = new Pool();
$pool->addRepository($repository);
// Parse package name and constraint // Parse package name and constraint
list($needle, $textConstraint) = array_pad( list($needle, $textConstraint) = array_pad(
@ -89,17 +87,17 @@ class BaseDependencyCommand extends BaseCommand
); );
// Find packages that are or provide the requested package first // Find packages that are or provide the requested package first
$packages = $pool->whatProvides(strtolower($needle)); $packages = $installedRepo->findPackagesWithReplacersAndProviders($needle);
if (empty($packages)) { if (empty($packages)) {
throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle)); 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. // 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. // 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())); $defaultRepos = new CompositeRepository(RepositoryFactory::defaultRepos($this->getIO()));
if ($match = $defaultRepos->findPackage($needle, $textConstraint)) { 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); $recursive = $renderTree || $input->getOption(self::OPTION_RECURSIVE);
// Resolve dependencies // Resolve dependencies
$results = $repository->getDependents($needles, $constraint, $inverted, $recursive); $results = $installedRepo->getDependents($needles, $constraint, $inverted, $recursive);
if (empty($results)) { if (empty($results)) {
$extra = (null !== $constraint) ? sprintf(' in versions %smatching %s', $inverted ? 'not ' : '', $textConstraint) : ''; $extra = (null !== $constraint) ? sprintf(' in versions %smatching %s', $inverted ? 'not ' : '', $textConstraint) : '';
$this->getIO()->writeError(sprintf( $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\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Composer\Repository\PlatformRepository; use Composer\Repository\PlatformRepository;
use Composer\Repository\InstalledRepository;
class CheckPlatformReqsCommand extends BaseCommand class CheckPlatformReqsCommand extends BaseCommand
{ {
@ -48,12 +49,13 @@ EOT
$requires = $composer->getPackage()->getRequires(); $requires = $composer->getPackage()->getRequires();
if ($input->getOption('no-dev')) { 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 { } else {
$dependencies = $composer->getRepositoryManager()->getLocalRepository()->getPackages(); $installedRepo = $composer->getRepositoryManager()->getLocalRepository();
// fallback to lockfile if installed repo is empty // fallback to lockfile if installed repo is empty
if (!$dependencies) { if (!$installedRepo->getPackages()) {
$dependencies = $composer->getLocker()->getLockedRepository(true)->getPackages(); $installedRepo = $composer->getLocker()->getLockedRepository(true);
} }
$requires += $composer->getPackage()->getDevRequires(); $requires += $composer->getPackage()->getDevRequires();
} }
@ -61,7 +63,8 @@ EOT
$requires[$require] = array($link); $requires[$require] = array($link);
} }
foreach ($dependencies as $package) { $installedRepo = new InstalledRepository(array($installedRepo));
foreach ($installedRepo->getPackages() as $package) {
foreach ($package->getRequires() as $require => $link) { foreach ($package->getRequires() as $require => $link) {
$requires[$require][] = $link; $requires[$require][] = $link;
} }
@ -69,19 +72,9 @@ EOT
ksort($requires); ksort($requires);
$platformRepo = new PlatformRepository(array(), array()); $installedRepo->addRepository(new PlatformRepository(array(), array()));
$currentPlatformPackages = $platformRepo->getPackages();
$currentPlatformPackageMap = array();
/**
* @var PackageInterface $currentPlatformPackage
*/
foreach ($currentPlatformPackages as $currentPlatformPackage) {
$currentPlatformPackageMap[$currentPlatformPackage->getName()] = $currentPlatformPackage;
}
$results = array(); $results = array();
$exitCode = 0; $exitCode = 0;
/** /**
@ -89,42 +82,62 @@ EOT
*/ */
foreach ($requires as $require => $links) { foreach ($requires as $require => $links) {
if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $require)) { if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $require)) {
if (isset($currentPlatformPackageMap[$require])) { $candidates = $installedRepo->findPackagesWithReplacersAndProviders($require);
$pass = true; if ($candidates) {
$version = $currentPlatformPackageMap[$require]->getVersion(); $reqResults = array();
foreach ($candidates as $candidate) {
foreach ($links as $link) { if ($candidate->getName() === $require) {
if (!$link->getConstraint()->matches(new Constraint('=', $version))) { $candidateConstraint = new Constraint('=', $candidate->getVersion());
$results[] = array( $candidateConstraint->setPrettyString($candidate->getPrettyVersion());
$currentPlatformPackageMap[$require]->getPrettyName(), } else {
$currentPlatformPackageMap[$require]->getPrettyVersion(), foreach (array_merge($candidate->getProvides(), $candidate->getReplaces()) as $link) {
$link, if ($link->getTarget() === $require) {
'<error>failed</error>', $candidateConstraint = $link->getConstraint();
); break;
$pass = false; }
}
$exitCode = max($exitCode, 1); }
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( $results[] = array(
$currentPlatformPackageMap[$require]->getPrettyName(), $candidate->getName() === $require ? $candidate->getPrettyName() : $require,
$currentPlatformPackageMap[$require]->getPrettyVersion(), $candidateConstraint->getPrettyString(),
null, 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'); $settingKey = $input->getArgument('setting-key');
if (!$settingKey) { if (!$settingKey || !is_string($settingKey)) {
return 0; return 0;
} }

View File

@ -20,7 +20,6 @@ use Composer\Installer\InstallationManager;
use Composer\Installer\SuggestedPackagesReporter; use Composer\Installer\SuggestedPackagesReporter;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Package\BasePackage; use Composer\Package\BasePackage;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\Package\Version\VersionSelector; use Composer\Package\Version\VersionSelector;
use Composer\Package\AliasPackage; use Composer\Package\AliasPackage;
@ -28,6 +27,7 @@ use Composer\Repository\RepositoryFactory;
use Composer\Repository\CompositeRepository; use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository; use Composer\Repository\PlatformRepository;
use Composer\Repository\InstalledFilesystemRepository; use Composer\Repository\InstalledFilesystemRepository;
use Composer\Repository\RepositorySet;
use Composer\Script\ScriptEvents; use Composer\Script\ScriptEvents;
use Composer\Util\Silencer; use Composer\Util\Silencer;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -38,6 +38,7 @@ use Symfony\Component\Finder\Finder;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Config\JsonConfigSource; use Composer\Config\JsonConfigSource;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Loop;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
/** /**
@ -182,8 +183,6 @@ EOT
$composer = Factory::create($io, null, $disablePlugins); $composer = Factory::create($io, null, $disablePlugins);
} }
$composer->getDownloadManager()->setOutputProgress(!$noProgress);
$fs = new Filesystem(); $fs = new Filesystem();
if ($noScripts === false) { if ($noScripts === false) {
@ -334,8 +333,8 @@ EOT
throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities))); throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities)));
} }
$pool = new Pool($stability); $repositorySet = new RepositorySet($stability);
$pool->addRepository($sourceRepo); $repositorySet->addRepository($sourceRepo);
$phpVersion = null; $phpVersion = null;
$prettyPhpVersion = null; $prettyPhpVersion = null;
@ -349,7 +348,7 @@ EOT
} }
// find the latest version if there are multiple // find the latest version if there are multiple
$versionSelector = new VersionSelector($pool); $versionSelector = new VersionSelector($repositorySet);
$package = $versionSelector->findBestCandidate($name, $packageVersion, $phpVersion, $stability); $package = $versionSelector->findBestCandidate($name, $packageVersion, $phpVersion, $stability);
if (!$package) { if (!$package) {
@ -384,15 +383,17 @@ EOT
$package = $package->getAliasOf(); $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) $dm->setPreferSource($preferSource)
->setPreferDist($preferDist) ->setPreferDist($preferDist);
->setOutputProgress(!$noProgress);
$projectInstaller = new ProjectInstaller($directory, $dm); $projectInstaller = new ProjectInstaller($directory, $dm);
$im = $this->createInstallationManager(); $im = $factory->createInstallationManager(new Loop($httpDownloader), $io);
$im->addInstaller($projectInstaller); $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); $im->notifyInstalls($io);
// collect suggestions // collect suggestions
@ -408,16 +409,4 @@ EOT
return $installedFromVcs; 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\ConfigValidator;
use Composer\Util\IniHelper; use Composer\Util\IniHelper;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\Util\StreamContextFactory; use Composer\Util\StreamContextFactory;
use Composer\SelfUpdate\Keys; use Composer\SelfUpdate\Keys;
use Composer\SelfUpdate\Versions; use Composer\SelfUpdate\Versions;
@ -35,8 +35,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class DiagnoseCommand extends BaseCommand class DiagnoseCommand extends BaseCommand
{ {
/** @var RemoteFilesystem */ /** @var HttpDownloader */
protected $rfs; protected $httpDownloader;
/** @var ProcessExecutor */ /** @var ProcessExecutor */
protected $process; protected $process;
@ -86,7 +86,7 @@ EOT
$config->merge(array('config' => array('secure-http' => false))); $config->merge(array('config' => array('secure-http' => false)));
$config->prohibitUrlByConfig('http://repo.packagist.org', new NullIO); $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); $this->process = new ProcessExecutor($io);
$io->write('Checking platform settings: ', false); $io->write('Checking platform settings: ', false);
@ -156,7 +156,7 @@ EOT
$this->outputResult($this->checkVersion($config)); $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(); $platformOverrides = $config->get('platform') ?: array();
$platformRepo = new PlatformRepository(array(), $platformOverrides); $platformRepo = new PlatformRepository(array(), $platformOverrides);
@ -229,7 +229,7 @@ EOT
} }
try { 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) { } catch (TransportException $e) {
if (false !== strpos($e->getMessage(), 'cafile')) { if (false !== strpos($e->getMessage(), 'cafile')) {
$result[] = '<error>[' . get_class($e) . '] ' . $e->getMessage() . '</error>'; $result[] = '<error>[' . get_class($e) . '] ' . $e->getMessage() . '</error>';
@ -256,11 +256,11 @@ EOT
$protocol = extension_loaded('openssl') ? 'https' : 'http'; $protocol = extension_loaded('openssl') ? 'https' : 'http';
try { 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 = reset($json['provider-includes']);
$hash = $hash['sha256']; $hash = $hash['sha256'];
$path = str_replace('%hash%', $hash, key($json['provider-includes'])); $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) { if (hash('sha256', $provider) !== $hash) {
return 'It seems that your proxy is modifying http traffic on the fly'; 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'; $url = 'http://repo.packagist.org/packages.json';
try { try {
$this->rfs->getContents('packagist.org', $url, false); $this->httpDownloader->get($url);
} catch (TransportException $e) { } catch (TransportException $e) {
try { 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) { } catch (TransportException $e) {
return 'Unable to assess the situation, maybe packagist.org is down ('.$e->getMessage().')'; 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'; $url = 'https://api.github.com/repos/Seldaek/jsonlint/zipball/1.0.0';
try { try {
$this->rfs->getContents('github.com', $url, false); $this->httpDownloader->get($url);
} catch (TransportException $e) { } catch (TransportException $e) {
try { 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) { } catch (TransportException $e) {
return 'Unable to assess the situation, maybe github is down ('.$e->getMessage().')'; return 'Unable to assess the situation, maybe github is down ('.$e->getMessage().')';
} }
@ -347,7 +347,7 @@ EOT
try { try {
$url = $domain === 'github.com' ? 'https://api.'.$domain.'/' : 'https://'.$domain.'/api/v3/'; $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, 'retry-auth-failure' => false,
)) ? true : 'Unexpected error'; )) ? true : 'Unexpected error';
} catch (\Exception $e) { } catch (\Exception $e) {
@ -377,8 +377,7 @@ EOT
} }
$url = $domain === 'github.com' ? 'https://api.'.$domain.'/rate_limit' : 'https://'.$domain.'/api/rate_limit'; $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 = $this->httpDownloader->get($url, array('retry-auth-failure' => false))->decodeJson();
$data = json_decode($json, true);
return $data['resources']['core']; return $data['resources']['core'];
} }
@ -431,7 +430,7 @@ EOT
return $result; return $result;
} }
$versionsUtil = new Versions($config, $this->rfs); $versionsUtil = new Versions($config, $this->httpDownloader);
$latest = $versionsUtil->getLatest(); $latest = $versionsUtil->getLatest();
if (Composer::VERSION !== $latest['version'] && Composer::VERSION !== '@package_version@') { 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"; $text .= "Install either of them or recompile php without --disable-iconv";
break; 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': case 'php':
$text = PHP_EOL."Your PHP ({$current}) is too old, you must upgrade to PHP 5.3.2 or higher."; $text = PHP_EOL."Your PHP ({$current}) is too old, you must upgrade to PHP 5.3.2 or higher.";
break; break;
@ -729,7 +714,7 @@ EOT
/** /**
* Check if allow_url_fopen is ON * Check if allow_url_fopen is ON
* *
* @return bool|string * @return true|string
*/ */
private function checkConnectivity() private function checkConnectivity()
{ {

View File

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

View File

@ -12,7 +12,6 @@
namespace Composer\Command; namespace Composer\Command;
use Composer\DependencyResolver\Pool;
use Composer\Factory; use Composer\Factory;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Package\BasePackage; use Composer\Package\BasePackage;
@ -22,6 +21,7 @@ use Composer\Package\Version\VersionSelector;
use Composer\Repository\CompositeRepository; use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository; use Composer\Repository\PlatformRepository;
use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositoryFactory;
use Composer\Repository\RepositorySet;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -42,8 +42,8 @@ class InitCommand extends BaseCommand
/** @var array */ /** @var array */
private $gitConfig; private $gitConfig;
/** @var Pool[] */ /** @var RepositorySet[] */
private $pools; private $repositorySets;
/** /**
* {@inheritdoc} * {@inheritdoc}
@ -86,8 +86,8 @@ EOT
{ {
$io = $this->getIO(); $io = $this->getIO();
$whitelist = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license'); $allowlist = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license');
$options = array_filter(array_intersect_key($input->getOptions(), array_flip($whitelist))); $options = array_filter(array_intersect_key($input->getOptions(), array_flip($allowlist)));
if (isset($options['author'])) { if (isset($options['author'])) {
$options['authors'] = $this->formatAuthors($options['author']); $options['authors'] = $this->formatAuthors($options['author']);
@ -688,16 +688,16 @@ EOT
return false !== filter_var($email, FILTER_VALIDATE_EMAIL); 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'; $key = $minimumStability ?: 'default';
if (!isset($this->pools[$key])) { if (!isset($this->repositorySets[$key])) {
$this->pools[$key] = $pool = new Pool($minimumStability ?: $this->getMinimumStability($input)); $this->repositorySets[$key] = $repositorySet = new RepositorySet($minimumStability ?: $this->getMinimumStability($input));
$pool->addRepository($this->getRepos()); $repositorySet->addRepository($this->getRepos());
} }
return $this->pools[$key]; return $this->repositorySets[$key];
} }
private function getMinimumStability(InputInterface $input) 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) private function findBestVersionAndNameForPackage(InputInterface $input, $name, $phpVersion, $preferredStability = 'stable', $requiredVersion = null, $minimumStability = null, $fixed = null)
{ {
// find the latest version allowed in this pool // find the latest version allowed in this repo set
$versionSelector = new VersionSelector($this->getPool($input, $minimumStability)); $versionSelector = new VersionSelector($this->getRepositorySet($input, $minimumStability));
$ignorePlatformReqs = $input->hasOption('ignore-platform-reqs') && $input->getOption('ignore-platform-reqs'); $ignorePlatformReqs = $input->hasOption('ignore-platform-reqs') && $input->getOption('ignore-platform-reqs');
// ignore phpVersion if platform requirements are ignored // 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-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-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-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('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('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`.'), 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 = $this->getComposer(true, $input->getOption('no-plugins'));
$composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
@ -108,7 +106,6 @@ EOT
->setDevMode(!$input->getOption('no-dev')) ->setDevMode(!$input->getOption('no-dev'))
->setDumpAutoloader(!$input->getOption('no-autoloader')) ->setDumpAutoloader(!$input->getOption('no-autoloader'))
->setRunScripts(!$input->getOption('no-scripts')) ->setRunScripts(!$input->getOption('no-scripts'))
->setSkipSuggest($input->getOption('no-suggest'))
->setOptimizeAutoloader($optimize) ->setOptimizeAutoloader($optimize)
->setClassMapAuthoritative($authoritative) ->setClassMapAuthoritative($authoritative)
->setApcuAutoloader($apcu) ->setApcuAutoloader($apcu)

View File

@ -13,6 +13,7 @@
namespace Composer\Command; namespace Composer\Command;
use Composer\Config\JsonConfigSource; use Composer\Config\JsonConfigSource;
use Composer\DependencyResolver\Request;
use Composer\Installer; use Composer\Installer;
use Composer\Plugin\CommandEvent; use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
@ -38,6 +39,7 @@ class RemoveCommand extends BaseCommand
->setDefinition(array( ->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.'), 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('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-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-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('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) { foreach ($packages as $package) {
if (isset($composer[$type][$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])) { } elseif (isset($composer[$altType][$package])) {
$io->writeError('<warning>' . $composer[$altType][$package] . ' could not be found in ' . $type . ' but it is present in ' . $altType . '</warning>'); $io->writeError('<warning>' . $composer[$altType][$package] . ' could not be found in ' . $type . ' but it is present in ' . $altType . '</warning>');
if ($io->isInteractive()) { if ($io->isInteractive()) {
if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [<comment>yes</comment>]? ', true)) { 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]))) { } elseif (isset($composer[$type]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$type]))) {
foreach ($matches as $matchedPackage) { 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]))) { } elseif (isset($composer[$altType]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$altType]))) {
foreach ($matches as $matchedPackage) { foreach ($matches as $matchedPackage) {
$io->writeError('<warning>' . $matchedPackage . ' could not be found in ' . $type . ' but it is present in ' . $altType . '</warning>'); $io->writeError('<warning>' . $matchedPackage . ' could not be found in ' . $type . ' but it is present in ' . $altType . '</warning>');
if ($io->isInteractive()) { if ($io->isInteractive()) {
if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [<comment>yes</comment>]? ', true)) { 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 // Update packages
$this->resetComposer(); $this->resetComposer();
$composer = $this->getComposer(true, $input->getOption('no-plugins')); $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); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
@ -146,10 +180,11 @@ EOT
->setClassMapAuthoritative($authoritative) ->setClassMapAuthoritative($authoritative)
->setApcuAutoloader($apcu) ->setApcuAutoloader($apcu)
->setUpdate(true) ->setUpdate(true)
->setUpdateWhitelist($packages) ->setUpdateAllowList($packages)
->setWhitelistTransitiveDependencies(!$input->getOption('no-update-with-dependencies')) ->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')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs'))
->setRunScripts(!$input->getOption('no-scripts')) ->setRunScripts(!$input->getOption('no-scripts'))
->setDryRun($dryRun)
; ;
$status = $install->run(); $status = $install->run();

View File

@ -12,6 +12,7 @@
namespace Composer\Command; namespace Composer\Command;
use Composer\DependencyResolver\Request;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@ -21,6 +22,8 @@ use Composer\Installer;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator; use Composer\Json\JsonManipulator;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Package\Loader\ArrayLoader;
use Composer\Package\BasePackage;
use Composer\Plugin\CommandEvent; use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
use Composer\Repository\CompositeRepository; use Composer\Repository\CompositeRepository;
@ -35,9 +38,14 @@ use Composer\Util\Silencer;
class RequireCommand extends InitCommand class RequireCommand extends InitCommand
{ {
private $newlyCreated; private $newlyCreated;
private $firstRequire;
private $json; private $json;
private $file; private $file;
private $composerBackup; private $composerBackup;
/** @var string file name */
private $lock;
/** @var ?string contents before modification if the lock file exists */
private $lockBackup;
protected function configure() protected function configure()
{ {
@ -47,16 +55,19 @@ class RequireCommand extends InitCommand
->setDefinition(array( ->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 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('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-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('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('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-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-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('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-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-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('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('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-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies.'),
new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest 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->json = new JsonFile($this->file);
$this->lock = Factory::getLockFile($this->file);
$this->composerBackup = file_get_contents($this->json->getPath()); $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 // 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 // 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'); $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(); $composerDefinition = $this->json->read();
foreach ($requirements as $package => $version) { foreach ($requirements as $package => $version) {
$composerDefinition[$requireKey][$package] = $version; $composerDefinition[$requireKey][$package] = $version;
@ -202,51 +223,78 @@ EOT
} }
try { try {
return $this->doUpdate($input, $output, $io, $requirements); return $this->doUpdate($input, $output, $io, $requirements, $requireKey, $removeKey);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->revertComposerFile(false); $this->revertComposerFile(false);
throw $e; 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 // Update packages
$this->resetComposer(); $this->resetComposer();
$composer = $this->getComposer(true, $input->getOption('no-plugins')); $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'); $updateDevMode = !$input->getOption('update-no-dev');
$optimize = $input->getOption('optimize-autoloader') || $composer->getConfig()->get('optimize-autoloader'); $optimize = $input->getOption('optimize-autoloader') || $composer->getConfig()->get('optimize-autoloader');
$authoritative = $input->getOption('classmap-authoritative') || $composer->getConfig()->get('classmap-authoritative'); $authoritative = $input->getOption('classmap-authoritative') || $composer->getConfig()->get('classmap-authoritative');
$apcu = $input->getOption('apcu-autoloader') || $composer->getConfig()->get('apcu-autoloader'); $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); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
$install = Installer::create($io, $composer); $install = Installer::create($io, $composer);
$install $install
->setDryRun($input->getOption('dry-run'))
->setVerbose($input->getOption('verbose')) ->setVerbose($input->getOption('verbose'))
->setPreferSource($input->getOption('prefer-source')) ->setPreferSource($input->getOption('prefer-source'))
->setPreferDist($input->getOption('prefer-dist')) ->setPreferDist($input->getOption('prefer-dist'))
->setDevMode($updateDevMode) ->setDevMode($updateDevMode)
->setRunScripts(!$input->getOption('no-scripts')) ->setRunScripts(!$input->getOption('no-scripts'))
->setSkipSuggest($input->getOption('no-suggest'))
->setOptimizeAutoloader($optimize) ->setOptimizeAutoloader($optimize)
->setClassMapAuthoritative($authoritative) ->setClassMapAuthoritative($authoritative)
->setApcuAutoloader($apcu) ->setApcuAutoloader($apcu)
->setUpdate(true) ->setUpdate(true)
->setUpdateWhitelist(array_keys($requirements)) ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies)
->setWhitelistTransitiveDependencies($input->getOption('update-with-dependencies'))
->setWhitelistAllDependencies($input->getOption('update-with-all-dependencies'))
->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs'))
->setPreferStable($input->getOption('prefer-stable')) ->setPreferStable($input->getOption('prefer-stable'))
->setPreferLowest($input->getOption('prefer-lowest')) ->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(); $status = $install->run();
if ($status !== 0) { if ($status !== 0 || $input->getOption('dry-run')) {
$this->revertComposerFile(false); $this->revertComposerFile(false);
} }
@ -285,9 +333,19 @@ EOT
if ($this->newlyCreated) { if ($this->newlyCreated) {
$io->writeError("\n".'<error>Installation failed, deleting '.$this->file.'.</error>'); $io->writeError("\n".'<error>Installation failed, deleting '.$this->file.'.</error>');
unlink($this->json->getPath()); unlink($this->json->getPath());
if (file_exists($this->lock)) {
unlink($this->lock);
}
} else { } 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); file_put_contents($this->json->getPath(), $this->composerBackup);
if ($this->lockBackup) {
file_put_contents($this->lock, $this->lockBackup);
}
} }
if ($hardExit) { if ($hardExit) {

View File

@ -77,9 +77,9 @@ EOT
} }
$io = $this->getIO(); $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 // switch channel if requested
foreach (array('stable', 'preview', 'snapshot') as $channel) { foreach (array('stable', 'preview', 'snapshot') as $channel) {
@ -154,11 +154,11 @@ EOT
$updatingToTag = !preg_match('{^[0-9a-f]{40}$}', $updateVersion); $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'); $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); $io->writeError(' ', false);
$remoteFilesystem->copy(self::HOMEPAGE, $remoteFilename, $tempFilename, !$input->getOption('no-progress')); $httpDownloader->copy($remoteFilename, $tempFilename);
$io->writeError(''); $io->writeError('');
if (!file_exists($tempFilename) || !$signature) { if (!file_exists($tempFilename) || !$signature) {

View File

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

View File

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

View File

@ -13,6 +13,9 @@
namespace Composer\Command; namespace Composer\Command;
use Composer\Repository\PlatformRepository; 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\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@ -26,8 +29,10 @@ class SuggestsCommand extends BaseCommand
->setName('suggests') ->setName('suggests')
->setDescription('Shows package suggestions.') ->setDescription('Shows package suggestions.')
->setDefinition(array( ->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('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 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.'), 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. 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 Read more at https://getcomposer.org/doc/03-cli.md#suggests
EOT EOT
) )
; ;
} }
/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output)
{ {
$lock = $this->getComposer()->getLocker()->getLockData(); $composer = $this->getComposer();
if (empty($lock)) { $installedRepos = array(
throw new \RuntimeException('Lockfile seems to be empty?'); 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']; $installedRepo = new CompositeRepository($installedRepos);
$reporter = new SuggestedPackagesReporter($this->getIO());
if (!$input->getOption('no-dev')) {
$packages += $lock['packages-dev'];
}
$filter = $input->getArgument('packages'); $filter = $input->getArgument('packages');
if (empty($filter) && !$input->getOption('all')) {
// First assemble lookup list of packages that are installed, replaced or provided $filter = array_map(function ($link) {
$installed = array(); return $link->getTarget();
foreach ($packages as $package) { }, array_merge($composer->getPackage()->getRequires(), $composer->getPackage()->getDevRequires()));
$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']));
}
} }
foreach ($installedRepo->getPackages() as $package) {
// Undub and sort the install list into a sorted lookup array if (!empty($filter) && !in_array($package->getName(), $filter)) {
$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'])) {
continue; 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 $reporter->addSuggestionsFromPackage($package);
$mode = 0; }
// Determine output mode, default is by-package
$mode = SuggestedPackagesReporter::MODE_BY_PACKAGE;
$io = $this->getIO(); $io = $this->getIO();
if ($input->getOption('by-package') || $io->isVerbose()) { // if by-suggestion is given we override the default
$mode |= 1;
}
if ($input->getOption('by-suggestion')) { 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 $reporter->output($mode, $installedRepo);
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('');
}
}
return 0; return 0;
} }

View File

@ -13,6 +13,7 @@
namespace Composer\Command; namespace Composer\Command;
use Composer\Composer; use Composer\Composer;
use Composer\DependencyResolver\Request;
use Composer\Installer; use Composer\Installer;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Plugin\CommandEvent; 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-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-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-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, 'Update also dependencies of packages in the argument list, except those which are root requirements.'),
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, 'Update also dependencies of packages in the argument list, including those which are root requirements.'),
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('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'), 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('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`.'), 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); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
@ -135,6 +146,13 @@ EOT
$authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative'); $authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative');
$apcu = $input->getOption('apcu-autoloader') || $config->get('apcu-autoloader'); $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 $install
->setDryRun($input->getOption('dry-run')) ->setDryRun($input->getOption('dry-run'))
->setVerbose($input->getOption('verbose')) ->setVerbose($input->getOption('verbose'))
@ -143,14 +161,13 @@ EOT
->setDevMode(!$input->getOption('no-dev')) ->setDevMode(!$input->getOption('no-dev'))
->setDumpAutoloader(!$input->getOption('no-autoloader')) ->setDumpAutoloader(!$input->getOption('no-autoloader'))
->setRunScripts(!$input->getOption('no-scripts')) ->setRunScripts(!$input->getOption('no-scripts'))
->setSkipSuggest($input->getOption('no-suggest'))
->setOptimizeAutoloader($optimize) ->setOptimizeAutoloader($optimize)
->setClassMapAuthoritative($authoritative) ->setClassMapAuthoritative($authoritative)
->setApcuAutoloader($apcu) ->setApcuAutoloader($apcu)
->setUpdate(true) ->setUpdate(true)
->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $packages) ->setUpdateMirrors($updateMirrors)
->setWhitelistTransitiveDependencies($input->getOption('with-dependencies')) ->setUpdateAllowList($packages)
->setWhitelistAllDependencies($input->getOption('with-all-dependencies')) ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies)
->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs'))
->setPreferStable($input->getOption('prefer-stable')) ->setPreferStable($input->getOption('prefer-stable'))
->setPreferLowest($input->getOption('prefer-lowest')) ->setPreferLowest($input->getOption('prefer-lowest'))

View File

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

View File

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

View File

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

View File

@ -230,6 +230,12 @@ class Application extends BaseApplication
if (function_exists('posix_getuid') && posix_getuid() === 0) { if (function_exists('posix_getuid') && posix_getuid() === 0) {
if ($commandName !== 'self-update' && $commandName !== 'selfupdate') { 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>'); $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')) { if ($uid = (int) getenv('SUDO_UID')) {
// Silently clobber any sudo credentials on the invoking user to avoid privilege escalations later on // 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; return $result;
} catch (ScriptExecutionException $e) { } catch (ScriptExecutionException $e) {
return $e->getCode(); return (int) $e->getCode();
} catch (\Exception $e) { } catch (\Exception $e) {
$this->hintCommonErrors($e); $this->hintCommonErrors($e);
restore_error_handler(); restore_error_handler();

View File

@ -44,54 +44,33 @@ class DefaultPolicy implements PolicyInterface
return $constraint->matchSpecific($version, true); 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) { foreach ($packages as &$nameLiterals) {
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) {
$policy = $this; $policy = $this;
usort($literals, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) { usort($nameLiterals, function ($a, $b) use ($policy, $pool, $requiredPackage) {
return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true); return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true);
}); });
} }
foreach ($packages as &$literals) { foreach ($packages as &$sortedLiterals) {
$literals = $this->pruneToHighestPriorityOrInstalled($pool, $installedMap, $literals); $sortedLiterals = $this->pruneToBestVersion($pool, $sortedLiterals);
$sortedLiterals = $this->pruneRemoteAliases($pool, $sortedLiterals);
$literals = $this->pruneToBestVersion($pool, $literals);
$literals = $this->pruneRemoteAliases($pool, $literals);
} }
$selected = call_user_func_array('array_merge', $packages); $selected = call_user_func_array('array_merge', $packages);
// now sort the result across all packages to respect replaces across packages // now sort the result across all packages to respect replaces across packages
usort($selected, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) { usort($selected, function ($a, $b) use ($policy, $pool, $requiredPackage) {
return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage); return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage);
}); });
return $selected; return $selected;
} }
protected function groupLiteralsByNamePreferInstalled(Pool $pool, array $installedMap, $literals) protected function groupLiteralsByName(Pool $pool, $literals)
{ {
$packages = array(); $packages = array();
foreach ($literals as $literal) { foreach ($literals as $literal) {
@ -100,12 +79,7 @@ class DefaultPolicy implements PolicyInterface
if (!isset($packages[$packageName])) { if (!isset($packages[$packageName])) {
$packages[$packageName] = array(); $packages[$packageName] = array();
} }
$packages[$packageName][] = $literal;
if (isset($installedMap[abs($literal)])) {
array_unshift($packages[$packageName], $literal);
} else {
$packages[$packageName][] = $literal;
}
} }
return $packages; return $packages;
@ -114,61 +88,49 @@ class DefaultPolicy implements PolicyInterface
/** /**
* @protected * @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
// prefer aliases to the original package if ($a->getName() === $b->getName()) {
if ($a->getName() === $b->getName()) { $aAliased = $a instanceof AliasPackage;
$aAliased = $a instanceof AliasPackage; $bAliased = $b instanceof AliasPackage;
$bAliased = $b instanceof AliasPackage; if ($aAliased && !$bAliased) {
if ($aAliased && !$bAliased) { return -1; // use a
return -1; // use a
}
if (!$aAliased && $bAliased) {
return 1; // use b
}
} }
if (!$aAliased && $bAliased) {
if (!$ignoreReplace) { return 1; // use b
// 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;
}
}
} }
// 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])) { if (!$ignoreReplace) {
return -1; // 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])) { // priority equal, sort by package id to make reproducible
return 1; 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; 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 * 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; protected $literals;
/** /**
* @param array $literals * @param array $literals
* @param int $reason A RULE_* constant describing the reason for generating this rule * @param int|null $reason A RULE_* constant describing the reason for generating this rule
* @param Link|PackageInterface $reasonData * @param Link|PackageInterface|int|null $reasonData
* @param array $job The job this rule was created from
*/ */
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 all packages ascending by id
sort($literals); 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 * @return string
*/ */
public function getJobType() public function getOperationType()
{ {
return 'install'; return 'install';
} }
/**
* {@inheritDoc}
*/
public function show($lock)
{
return ($lock ? 'Locking ' : 'Installing ').$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')';
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function __toString() 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 * @return string
*/ */
public function getJobType() public function getOperationType()
{ {
return 'markAliasInstalled'; 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} * {@inheritDoc}
*/ */
public function __toString() 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 * @return string
*/ */
public function getJobType() public function getOperationType()
{ {
return 'markAliasUninstalled'; 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} * {@inheritDoc}
*/ */
public function __toString() 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 interface OperationInterface
{ {
/** /**
* Returns job type. * Returns operation type.
* *
* @return string * @return string
*/ */
public function getJobType(); public function getOperationType();
/** /**
* Returns operation reason. * Returns operation reason.
@ -33,6 +33,14 @@ interface OperationInterface
*/ */
public function getReason(); 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 * Serializes the operation in a human readable format
* *

View File

@ -43,8 +43,9 @@ abstract class SolverOperation implements OperationInterface
return $this->reason; return $this->reason;
} }
protected function formatVersion(PackageInterface $package) /**
{ * @param $lock bool Whether this is an operation on the lock file
return $package->getFullPrettyVersion(); * @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 * @return string
*/ */
public function getJobType() public function getOperationType()
{ {
return 'uninstall'; return 'uninstall';
} }
/**
* {@inheritDoc}
*/
public function show($lock)
{
return 'Removing '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')';
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function __toString() 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 * @return string
*/ */
public function getJobType() public function getOperationType()
{ {
return 'update'; 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} * {@inheritDoc}
*/ */
public function __toString() public function __toString()
{ {
$actionName = VersionParser::isUpgrade($this->initialPackage->getVersion(), $this->targetPackage->getVersion()) ? 'Updating' : 'Downgrading'; return $this->show(false);
return $actionName.' '.$this->initialPackage->getPrettyName().' ('.$this->formatVersion($this->initialPackage).') to '.
$this->targetPackage->getPrettyName(). ' ('.$this->formatVersion($this->targetPackage).')';
} }
} }

View File

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

View File

@ -12,143 +12,56 @@
namespace Composer\DependencyResolver; namespace Composer\DependencyResolver;
use Composer\Package\BasePackage;
use Composer\Package\AliasPackage; use Composer\Package\AliasPackage;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\EmptyConstraint; 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; 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 Nils Adermann <naderman@naderman.de>
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
*/ */
class Pool implements \Countable class Pool implements \Countable
{ {
const MATCH_NAME = -1;
const MATCH_NONE = 0; const MATCH_NONE = 0;
const MATCH = 1; const MATCH = 1;
const MATCH_PROVIDE = 2; const MATCH_PROVIDE = 2;
const MATCH_REPLACE = 3; const MATCH_REPLACE = 3;
const MATCH_FILTERED = 4;
protected $repositories = array();
protected $providerRepos = array();
protected $packages = array(); protected $packages = array();
protected $packageByName = array(); protected $packageByName = array();
protected $packageByExactName = array(); protected $packageByExactName = array();
protected $acceptableStabilities;
protected $stabilityFlags;
protected $versionParser; protected $versionParser;
protected $providerCache = array(); protected $providerCache = array();
protected $filterRequires; protected $unacceptableFixedPackages;
protected $whitelist = null;
protected $id = 1;
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->versionParser = new VersionParser;
$this->acceptableStabilities = array(); $this->setPackages($packages);
foreach (BasePackage::$stabilities as $stability => $value) { $this->unacceptableFixedPackages = $unacceptableFixedPackages;
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]);
}
}
} }
public function setWhitelist($whitelist) private function setPackages(array $packages)
{ {
$this->whitelist = $whitelist; $id = 1;
$this->providerCache = array();
}
/** foreach ($packages as $package) {
* Adds a repository and its packages to this package pool $this->packages[] = $package;
*
* @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 ($repos as $repo) { $package->id = $id++;
$this->repositories[] = $repo; $this->packageByExactName[$package->getName()][$package->id] = $package;
$exempt = $repo instanceof PlatformRepository || $repo instanceof InstalledRepositoryInterface; foreach ($package->getNames() as $provided) {
$this->packageByName[$provided][] = $package;
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;
}
}
}
}
} }
} }
} }
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. * 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 * packages must match or null to return all
* @param bool $mustMatchName Whether the name of returned packages * @param bool $mustMatchName Whether the name of returned packages
* must match the given name * must match the given name
* @param bool $bypassFilters If enabled, filterRequires and stability matching is ignored
* @return PackageInterface[] A set of packages * @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; $key = ((int) $mustMatchName).$constraint;
if (isset($this->providerCache[$name][$key])) { if (isset($this->providerCache[$name][$key])) {
return $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 * @see whatProvides
*/ */
private function computeWhatProvides($name, $constraint, $mustMatchName = false, $bypassFilters = false) private function computeWhatProvides($name, $constraint, $mustMatchName = false)
{ {
$candidates = array(); $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) { if ($mustMatchName) {
$candidates = array_filter($candidates, function ($candidate) use ($name) {
return $candidate->getName() == $name;
});
if (isset($this->packageByExactName[$name])) { if (isset($this->packageByExactName[$name])) {
$candidates = array_merge($candidates, $this->packageByExactName[$name]); $candidates = $this->packageByExactName[$name];
} }
} elseif (isset($this->packageByName[$name])) { } elseif (isset($this->packageByName[$name])) {
$candidates = array_merge($candidates, $this->packageByName[$name]); $candidates = $this->packageByName[$name];
} }
$matches = $provideMatches = array(); $matches = array();
$nameMatch = false;
foreach ($candidates as $candidate) { foreach ($candidates as $candidate) {
$aliasOfCandidate = null; switch ($this->match($candidate, $name, $constraint)) {
// 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)) {
case self::MATCH_NONE: case self::MATCH_NONE:
break; break;
case self::MATCH_NAME:
$nameMatch = true;
break;
case self::MATCH: case self::MATCH:
$nameMatch = true;
$matches[] = $candidate;
break;
case self::MATCH_PROVIDE: case self::MATCH_PROVIDE:
$provideMatches[] = $candidate;
break;
case self::MATCH_REPLACE: case self::MATCH_REPLACE:
$matches[] = $candidate; $matches[] = $candidate;
break; break;
case self::MATCH_FILTERED:
break;
default: default:
throw new \UnexpectedValueException('Unexpected match type'); throw new \UnexpectedValueException('Unexpected match type');
} }
} }
// if a package with the required name exists, we ignore providers return $matches;
if ($nameMatch) {
return $matches;
}
return array_merge($matches, $provideMatches);
} }
public function literalToPackage($literal) public function literalToPackage($literal)
@ -296,23 +157,6 @@ class Pool implements \Countable
return $prefix.' '.$package->getPrettyString(); 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 * Checks if the package matches the given constraint directly or through
* provided or replaced packages * provided or replaced packages
@ -322,27 +166,19 @@ class Pool implements \Countable
* @param ConstraintInterface $constraint The constraint to verify * @param ConstraintInterface $constraint The constraint to verify
* @return int One of the MATCH* constants of this class or 0 if there is no match * @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(); $candidateName = $candidate->getName();
$candidateVersion = $candidate->getVersion(); $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) { if ($candidateName === $name) {
$pkgConstraint = new Constraint('==', $candidateVersion); $pkgConstraint = new Constraint('==', $candidateVersion);
if ($constraint === null || $constraint->matches($pkgConstraint)) { 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(); $provides = $candidate->getProvides();
@ -352,13 +188,13 @@ class Pool implements \Countable
if (isset($replaces[0]) || isset($provides[0])) { if (isset($replaces[0]) || isset($provides[0])) {
foreach ($provides as $link) { foreach ($provides as $link) {
if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) { 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) { foreach ($replaces as $link) {
if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) { 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()))) { 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()))) { 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; 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; namespace Composer\DependencyResolver;
use Composer\Package\CompletePackageInterface; use Composer\Package\CompletePackageInterface;
use Composer\Repository\RepositorySet;
use Composer\Semver\Constraint\Constraint;
/** /**
* Represents a problem detected while solving dependencies * Represents a problem detected while solving dependencies
@ -28,20 +30,13 @@ class Problem
protected $reasonSeen; 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 * @var array
*/ */
protected $reasons = array(); protected $reasons = array();
protected $section = 0; protected $section = 0;
protected $pool;
public function __construct(Pool $pool)
{
$this->pool = $pool;
}
/** /**
* Add a rule as a reason * Add a rule as a reason
* *
@ -49,10 +44,7 @@ class Problem
*/ */
public function addRule(Rule $rule) public function addRule(Rule $rule)
{ {
$this->addReason(spl_object_hash($rule), array( $this->addReason(spl_object_hash($rule), $rule);
'rule' => $rule,
'job' => $rule->getJob(),
));
} }
/** /**
@ -68,124 +60,67 @@ class Problem
/** /**
* A human readable textual representation of the problem's reasons * 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 * @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)); $reasons = call_user_func_array('array_merge', array_reverse($this->reasons));
if (count($reasons) === 1) { if (count($reasons) === 1) {
reset($reasons); 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']; $reasonData = $rule->getReasonData();
$constraint = $job['constraint']; $packageName = $reasonData['packageName'];
$constraint = $reasonData['constraint'];
if (isset($constraint)) { if (isset($constraint)) {
$packages = $this->pool->whatProvides($packageName, $constraint); $packages = $pool->whatProvides($packageName, $constraint);
} else { } else {
$packages = array(); $packages = array();
} }
if ($job && $job['cmd'] === 'install' && empty($packages)) { if (empty($packages)) {
return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $packageName, $constraint));
// 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.';
} }
} }
$messages = array(); $messages = array();
foreach ($reasons as $rule) {
foreach ($reasons as $reason) { $messages[] = $rule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool);
$rule = $reason['rule'];
$job = $reason['job'];
if ($job) {
$messages[] = $this->jobToText($job);
} elseif ($rule) {
if ($rule instanceof Rule) {
$messages[] = $rule->getPrettyString($this->pool, $installedMap);
}
}
} }
return "\n - ".implode("\n - ", $messages); 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 * Store a reason descriptor but ignore duplicates
* *
* @param string $id A canonical identifier for the reason * @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])) { if (!isset($this->reasonSeen[$id])) {
$this->reasonSeen[$id] = true; $this->reasonSeen[$id] = true;
$this->reasons[$this->section][] = $reason; $this->reasons[$this->section][] = $reason;
@ -198,39 +133,150 @@ class Problem
} }
/** /**
* Turns a job into a human readable description * @internal
*
* @param array $job
* @return string
*/ */
protected function jobToText($job) public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $packageName, $constraint = null)
{ {
$packageName = $job['packageName']; // handle php/hhvm
$constraint = $job['constraint']; if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') {
switch ($job['cmd']) { $version = phpversion();
case 'install': $available = $pool->whatProvides($packageName);
$packages = $this->pool->whatProvides($packageName, $constraint);
if (!$packages) { if (count($available)) {
return 'No package found to satisfy install request for '.$packageName.$this->constraintToText($constraint); $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).'.'; 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.');
case 'update': }
return 'Update request for '.$packageName.$this->constraintToText($constraint).'.';
case 'remove': 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.');
return 'Removal request for '.$packageName.$this->constraintToText($constraint).'';
} }
if (isset($constraint)) { if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) {
$packages = $this->pool->whatProvides($packageName, $constraint); $illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName);
} else {
$packages = array(); 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(); $prepared = array();
foreach ($packages as $package) { foreach ($packages as $package) {
@ -238,19 +284,37 @@ class Problem
$prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion(); $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion();
} }
foreach ($prepared as $name => $package) { 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']).']'; $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']';
} }
return implode(', ', $prepared); 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 * @param \Composer\Semver\Constraint\ConstraintInterface $constraint
* @return string * @return string
*/ */
protected function constraintToText($constraint) protected static function constraintToText($constraint)
{ {
return $constraint ? ' '.$constraint->getPrettyString() : ''; return $constraint ? ' '.$constraint->getPrettyString() : '';
} }

View File

@ -12,6 +12,10 @@
namespace Composer\DependencyResolver; namespace Composer\DependencyResolver;
use Composer\Package\Package;
use Composer\Package\PackageInterface;
use Composer\Package\RootAliasPackage;
use Composer\Repository\LockArrayRepository;
use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\ConstraintInterface;
/** /**
@ -19,60 +23,129 @@ use Composer\Semver\Constraint\ConstraintInterface;
*/ */
class Request 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); $packageName = strtolower($packageName);
} $this->requires[$packageName] = $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);
} }
/** /**
* Mark an existing package as being installed and having to remain installed * Mark an existing package as being installed and having to remain installed
* *
* These jobs will not be tempered with by the solver * @param bool $lockable if set to false, the package will not be written to the lock file
*
* @param string $packageName
* @param ConstraintInterface|null $constraint
*/ */
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); unset($this->fixedPackages[spl_object_hash($package)]);
unset($this->unlockables[spl_object_hash($package)]);
$this->jobs[] = array(
'cmd' => $cmd,
'packageName' => $packageName,
'constraint' => $constraint,
'fixed' => $fixed,
);
} }
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\CompletePackage;
use Composer\Package\Link; use Composer\Package\Link;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Repository\RepositorySet;
/** /**
* @author Nils Adermann <naderman@naderman.de> * @author Nils Adermann <naderman@naderman.de>
@ -24,8 +25,8 @@ abstract class Rule
{ {
// reason constants // reason constants
const RULE_INTERNAL_ALLOW_UPDATE = 1; const RULE_INTERNAL_ALLOW_UPDATE = 1;
const RULE_JOB_INSTALL = 2; const RULE_ROOT_REQUIRE = 2;
const RULE_JOB_REMOVE = 3; const RULE_FIXED = 3;
const RULE_PACKAGE_CONFLICT = 6; const RULE_PACKAGE_CONFLICT = 6;
const RULE_PACKAGE_REQUIRES = 7; const RULE_PACKAGE_REQUIRES = 7;
const RULE_PACKAGE_OBSOLETES = 8; const RULE_PACKAGE_OBSOLETES = 8;
@ -41,22 +42,17 @@ abstract class Rule
const BITFIELD_DISABLED = 16; const BITFIELD_DISABLED = 16;
protected $bitfield; protected $bitfield;
protected $job; protected $request;
protected $reasonData; protected $reasonData;
/** /**
* @param int $reason A RULE_* constant describing the reason for generating this rule * @param int $reason A RULE_* constant describing the reason for generating this rule
* @param Link|PackageInterface $reasonData * @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; $this->reasonData = $reasonData;
if ($job) {
$this->job = $job;
}
$this->bitfield = (0 << self::BITFIELD_DISABLED) | $this->bitfield = (0 << self::BITFIELD_DISABLED) |
($reason << self::BITFIELD_REASON) | ($reason << self::BITFIELD_REASON) |
(255 << self::BITFIELD_TYPE); (255 << self::BITFIELD_TYPE);
@ -66,11 +62,6 @@ abstract class Rule
abstract public function getHash(); abstract public function getHash();
public function getJob()
{
return $this->job;
}
abstract public function equals(Rule $rule); abstract public function equals(Rule $rule);
public function getReason() public function getReason()
@ -85,11 +76,17 @@ abstract class Rule
public function getRequiredPackage() public function getRequiredPackage()
{ {
if ($this->getReason() === self::RULE_JOB_INSTALL) { $reason = $this->getReason();
return $this->reasonData;
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(); return $this->reasonData->getTarget();
} }
} }
@ -126,7 +123,12 @@ abstract class Rule
abstract public function isAssertion(); 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(); $literals = $this->getLiterals();
@ -142,17 +144,30 @@ abstract class Rule
case self::RULE_INTERNAL_ALLOW_UPDATE: case self::RULE_INTERNAL_ALLOW_UPDATE:
return $ruleText; return $ruleText;
case self::RULE_JOB_INSTALL: case self::RULE_ROOT_REQUIRE:
return "Install command rule ($ruleText)"; $packageName = $this->reasonData['packageName'];
$constraint = $this->reasonData['constraint'];
case self::RULE_JOB_REMOVE: $packages = $pool->whatProvides($packageName, $constraint);
return "Remove command rule ($ruleText)"; 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: case self::RULE_PACKAGE_CONFLICT:
$package1 = $pool->literalToPackage($literals[0]); $package1 = $pool->literalToPackage($literals[0]);
$package2 = $pool->literalToPackage($literals[1]); $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: case self::RULE_PACKAGE_REQUIRES:
$sourceLiteral = array_shift($literals); $sourceLiteral = array_shift($literals);
@ -169,73 +184,103 @@ abstract class Rule
} else { } else {
$targetName = $this->reasonData->getTarget(); $targetName = $this->reasonData->getTarget();
if ($targetName === 'php' || $targetName === 'php-64bit' || $targetName === 'hhvm') { $reason = Problem::getMissingPackageReason($repositorySet, $request, $pool, $targetName, $this->reasonData->getConstraint());
// handle php/hhvm
if (defined('HHVM_VERSION')) {
return $text . ' -> your HHVM version does not satisfy that requirement.';
}
$packages = $pool->whatProvides($targetName); return $text . ' -> ' . $reason[1];
$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; return $text;
case self::RULE_PACKAGE_OBSOLETES: 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; return $ruleText;
case self::RULE_INSTALLED_PACKAGE_OBSOLETES: case self::RULE_INSTALLED_PACKAGE_OBSOLETES:
return $ruleText; return $ruleText;
case self::RULE_PACKAGE_SAME_NAME: 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: case self::RULE_PACKAGE_IMPLICIT_OBSOLETES:
return $ruleText; return $ruleText;
case self::RULE_LEARNED: 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: case self::RULE_PACKAGE_ALIAS:
return $ruleText; return $ruleText;
default: default:
@ -252,17 +297,22 @@ abstract class Rule
protected function formatPackagesUnique($pool, array $packages) protected function formatPackagesUnique($pool, array $packages)
{ {
$prepared = array(); $prepared = array();
foreach ($packages as $package) { foreach ($packages as $index => $package) {
if (!is_object($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 $literal2
* @param int $reason A RULE_* constant describing the reason for generating this rule * @param int $reason A RULE_* constant describing the reason for generating this rule
* @param Link|PackageInterface $reasonData * @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) { if ($literal1 < $literal2) {
$this->literal1 = $literal1; $this->literal1 = $literal1;

View File

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

View File

@ -12,9 +12,11 @@
namespace Composer\DependencyResolver; namespace Composer\DependencyResolver;
use Composer\Package\LinkConstraint\VersionConstraint;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Package\AliasPackage; use Composer\Package\AliasPackage;
use Composer\Repository\PlatformRepository; use Composer\Repository\PlatformRepository;
use Composer\Semver\Constraint\Constraint;
/** /**
* @author Nils Adermann <naderman@naderman.de> * @author Nils Adermann <naderman@naderman.de>
@ -24,13 +26,11 @@ class RuleSetGenerator
protected $policy; protected $policy;
protected $pool; protected $pool;
protected $rules; protected $rules;
protected $jobs;
protected $installedMap;
protected $whitelistedMap;
protected $addedMap; protected $addedMap;
protected $conflictAddedMap; protected $conflictAddedMap;
protected $addedPackages; protected $addedPackages;
protected $addedPackagesByNames; protected $addedPackagesByNames;
protected $conflictsForName;
public function __construct(PolicyInterface $policy, Pool $pool) public function __construct(PolicyInterface $policy, Pool $pool)
{ {
@ -76,33 +76,17 @@ class RuleSetGenerator
* @param array $packages The set of packages to choose from * @param array $packages The set of packages to choose from
* @param int $reason A RULE_* constant describing the reason for * @param int $reason A RULE_* constant describing the reason for
* generating this rule * 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 * @return Rule The generated rule
*/ */
protected function createInstallOneOfRule(array $packages, $reason, $job) protected function createInstallOneOfRule(array $packages, $reason, $reasonData)
{ {
$literals = array(); $literals = array();
foreach ($packages as $package) { foreach ($packages as $package) {
$literals[] = $package->id; $literals[] = $package->id;
} }
return new GenericRule($literals, $reason, $job['packageName'], $job); return new GenericRule($literals, $reason, $reasonData);
}
/**
* 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);
} }
/** /**
@ -129,6 +113,20 @@ class RuleSetGenerator
return new Rule2Literals(-$issuer->id, -$provider->id, $reason, $reasonData); 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 * Adds a rule unless it duplicates an existing one of any type
* *
@ -147,41 +145,6 @@ class RuleSetGenerator
$this->rules->add($newRule, $type); $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) protected function addRulesForPackage(PackageInterface $package, $ignorePlatformReqs)
{ {
$workQueue = new \SplQueue; $workQueue = new \SplQueue;
@ -225,9 +188,16 @@ class RuleSetGenerator
if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) { if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
$this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, $package)); $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, $package));
} elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) { } else {
$reason = ($packageName == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES; if (!isset($this->conflictsForName[$packageName])) {
$this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $provider, $reason, $package)); $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 */ /** @var PackageInterface $possibleConflict */
foreach ($this->addedPackagesByNames[$link->getTarget()] as $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) { if ($conflictMatch === Pool::MATCH || $conflictMatch === Pool::MATCH_REPLACE) {
$this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $possibleConflict, Rule::RULE_PACKAGE_CONFLICT, $link)); $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 // check obsoletes and implicit obsoletes of a package
$isInstalled = isset($this->installedMap[$package->id]);
foreach ($package->getReplaces() as $link) { foreach ($package->getReplaces() as $link) {
if (!isset($this->addedPackagesByNames[$link->getTarget()])) { if (!isset($this->addedPackagesByNames[$link->getTarget()])) {
continue; continue;
@ -272,12 +240,19 @@ class RuleSetGenerator
} }
if (!$this->obsoleteImpossibleForAlias($package, $provider)) { 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)); $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) protected function obsoleteImpossibleForAlias($package, $provider)
@ -294,77 +269,61 @@ class RuleSetGenerator
return $impossible; return $impossible;
} }
protected function whitelistFromJobs() protected function addRulesForRequest(Request $request, $ignorePlatformReqs)
{ {
foreach ($this->jobs as $job) { $unlockableMap = $request->getUnlockableMap();
switch ($job['cmd']) {
case 'install': foreach ($request->getFixedPackages() as $package) {
$packages = $this->pool->whatProvides($job['packageName'], $job['constraint'], true); if ($package->id == -1) {
foreach ($packages as $package) { // fixed package was not added to the pool as it did not pass the stability requirements, this is fine
$this->whitelistFromPackage($package); if ($this->pool->isUnacceptableFixedPackage($package)) {
} continue;
break; }
// 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->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->addedMap = array();
$this->conflictAddedMap = array(); $this->conflictAddedMap = array();
$this->addedPackages = array(); $this->addedPackages = array();
$this->addedPackagesByNames = array(); $this->addedPackagesByNames = array();
foreach ($this->installedMap as $package) { $this->conflictsForName = array();
$this->addRulesForPackage($package, $ignorePlatformReqs);
}
$this->addRulesForJobs($ignorePlatformReqs); $this->addRulesForRequest($request, $ignorePlatformReqs);
$this->addConflictRules($ignorePlatformReqs); $this->addConflictRules($ignorePlatformReqs);

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@
namespace Composer\DependencyResolver; namespace Composer\DependencyResolver;
use Composer\Util\IniHelper; use Composer\Util\IniHelper;
use Composer\Repository\RepositorySet;
/** /**
* @author Nils Adermann <naderman@naderman.de> * @author Nils Adermann <naderman@naderman.de>
@ -20,29 +21,33 @@ use Composer\Util\IniHelper;
class SolverProblemsException extends \RuntimeException class SolverProblemsException extends \RuntimeException
{ {
protected $problems; protected $problems;
protected $installedMap; protected $learnedPool;
public function __construct(array $problems, array $installedMap) public function __construct(array $problems, array $learnedPool)
{ {
$this->problems = $problems; $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"; $text = "\n";
$hasExtensionProblems = false; $hasExtensionProblems = false;
$isCausedByLock = false;
foreach ($this->problems as $i => $problem) { 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())) { if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) {
$hasExtensionProblems = true; $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."; $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(); $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; return $text;
} }
@ -76,8 +85,8 @@ class SolverProblemsException extends \RuntimeException
private function hasExtensionProblems(array $reasonSets) private function hasExtensionProblems(array $reasonSets)
{ {
foreach ($reasonSets as $reasonSet) { foreach ($reasonSets as $reasonSet) {
foreach ($reasonSet as $reason) { foreach ($reasonSet as $rule) {
if (isset($reason["rule"]) && 0 === strpos($reason["rule"]->getRequiredPackage(), 'ext-')) { if (0 === strpos($rule->getRequiredPackage(), 'ext-')) {
return true; return true;
} }
} }

View File

@ -13,161 +13,205 @@
namespace Composer\DependencyResolver; namespace Composer\DependencyResolver;
use Composer\Package\AliasPackage; use Composer\Package\AliasPackage;
use Composer\Package\Link;
use Composer\Package\PackageInterface;
use Composer\Repository\PlatformRepository;
/** /**
* @author Nils Adermann <naderman@naderman.de> * @author Nils Adermann <naderman@naderman.de>
*/ */
class Transaction class Transaction
{ {
protected $policy; /**
protected $pool; * @var array
protected $installedMap; */
protected $decisions; protected $operations;
protected $transaction;
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->presentPackages = $presentPackages;
$this->pool = $pool; $this->setResultPackageMaps($resultPackages);
$this->installedMap = $installedMap; $this->operations = $this->calculateOperations();
$this->decisions = $decisions;
$this->transaction = array();
} }
public function getOperations() public function getOperations()
{ {
$installMeansUpdateMap = $this->findUpdates(); return $this->operations;
}
$updateMap = array(); private function setResultPackageMaps($resultPackages)
$installMap = array(); {
$uninstallMap = array(); $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) { $this->resultPackageMap = array();
$literal = $decision[Decisions::DECISION_LITERAL]; foreach ($resultPackages as $package) {
$reason = $decision[Decisions::DECISION_REASON]; $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 protected function calculateOperations()
if (($literal > 0) == isset($this->installedMap[$package->id])) { {
$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; continue;
} }
if ($literal > 0) { if (!isset($visited[spl_object_hash($package)])) {
if (isset($installMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) { $visited[spl_object_hash($package)] = true;
$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;
$stack[] = $package;
if ($package instanceof AliasPackage) { if ($package instanceof AliasPackage) {
$queue[] = $package->getAliasOf(); $stack[] = $package->getAliasOf();
} else { } else {
foreach ($package->getRequires() as $link) { foreach ($package->getRequires() as $link) {
$possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); $possibleRequires = $this->getProvidersInResult($link);
foreach ($possibleRequires as $require) { 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; if ($package instanceof AliasPackage) {
} else { $aliasKey = $package->getName().'::'.$package->getVersion();
if (isset($installMap[$packageId])) { if (isset($presentAliasMap[$aliasKey])) {
$this->install( unset($removeAliasMap[$aliasKey]);
$installMap[$packageId]['package'], } else {
$installMap[$packageId]['reason'] $operations[] = new Operation\MarkAliasInstalledOperation($package);
); }
unset($installMap[$packageId]); } else {
} if (isset($presentPackageMap[$package->getName()])) {
if (isset($updateMap[$packageId])) { $source = $presentPackageMap[$package->getName()];
$this->update(
$updateMap[$packageId]['source'], // do we need to update?
$updateMap[$packageId]['package'], // TODO different for lock?
$updateMap[$packageId]['reason'] if ($package->getVersion() != $presentPackageMap[$package->getName()]->getVersion() ||
); $package->getDistReference() !== $presentPackageMap[$package->getName()]->getDistReference() ||
unset($updateMap[$packageId]); $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) { foreach ($removeMap as $name => $package) {
$this->uninstall($uninstall['package'], $uninstall['reason']); 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 = $this->resultPackageMap;
$roots = $packages;
foreach ($packages as $packageId => $operation) { foreach ($this->resultPackageMap as $packageHash => $package) {
$package = $operation['package']; if (!isset($roots[$packageHash])) {
if (!isset($roots[$packageId])) {
continue; continue;
} }
foreach ($package->getRequires() as $link) { foreach ($package->getRequires() as $link) {
$possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); $possibleRequires = $this->getProvidersInResult($link);
foreach ($possibleRequires as $require) { foreach ($possibleRequires as $require) {
if ($require !== $package) { if ($require !== $package) {
unset($roots[$require->id]); unset($roots[spl_object_hash($require)]);
} }
} }
} }
@ -176,69 +220,87 @@ class Transaction
return $roots; 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]; * Workaround: if your packages depend on plugins, we must be sure
$package = $this->pool->literalToPackage($literal); * 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; continue;
} }
// !wanted & installed // is this package a plugin?
if ($literal <= 0 && isset($this->installedMap[$package->id])) { $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer';
$updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package);
$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) { // is this a plugin with no meaningful dependencies?
$literals[] = $update->id; 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) { unset($operations[$idx]);
if ($updateLiteral !== $literal) {
$installMeansUpdateMap[abs($updateLiteral)] = $package;
}
}
} }
} }
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) { $uninstOps = array();
return $this->markAliasInstalled($package, $reason); foreach ($operations as $idx => $op) {
if ($op instanceof Operation\UninstallOperation) {
$uninstOps[] = $op;
unset($operations[$idx]);
}
} }
$this->transaction[] = new Operation\InstallOperation($package, $reason); return array_merge($uninstOps, $operations);
}
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);
} }
} }

View File

@ -30,33 +30,56 @@ abstract class ArchiveDownloader extends FileDownloader
* @throws \RuntimeException * @throws \RuntimeException
* @throws \UnexpectedValueException * @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); if ($output) {
$retries = 3; $this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): Extracting archive");
while ($retries--) { } else {
$fileName = parent::download($package, $path, $output); $this->io->writeError('Extracting archive', false);
}
if ($output) { $this->filesystem->ensureDirectoryExists($path);
$this->io->writeError(' Extracting archive', false, IOInterface::VERBOSE); 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->unlink($fileName);
$this->filesystem->ensureDirectoryExists($temporaryDir);
try { $renameAsOne = false;
$this->extract($fileName, $temporaryDir); if (!file_exists($path) || ($this->filesystem->isDirEmpty($path) && $this->filesystem->removeDirectory($path))) {
} catch (\Exception $e) { $renameAsOne = true;
// remove cache if the file was corrupted }
parent::clearLastCacheWrite($package);
throw $e; $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->rename($extractedDir, $path);
$this->filesystem->unlink($fileName); } else {
$contentDir = $this->getFolderContent($temporaryDir);
// only one dir in the archive, extract its contents out of it // 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)); $contentDir = $this->getFolderContent((string) reset($contentDir));
} }
@ -65,44 +88,16 @@ abstract class ArchiveDownloader extends FileDownloader
$file = (string) $file; $file = (string) $file;
$this->filesystem->rename($file, $path . '/' . basename($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);
/** throw $e;
* {@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), '.');
} }
/** /**
@ -113,7 +108,7 @@ abstract class ArchiveDownloader extends FileDownloader
* *
* @throws \UnexpectedValueException If can not extract downloaded file to path * @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 * Returns the folder content, excluding dotfiles

View File

@ -15,6 +15,7 @@ namespace Composer\Downloader;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use React\Promise\PromiseInterface;
/** /**
* Downloaders manager. * Downloaders manager.
@ -24,6 +25,7 @@ use Composer\Util\Filesystem;
class DownloadManager class DownloadManager
{ {
private $io; private $io;
private $httpDownloader;
private $preferDist = false; private $preferDist = false;
private $preferSource = false; private $preferSource = false;
private $packagePreferences = array(); private $packagePreferences = array();
@ -33,9 +35,9 @@ class DownloadManager
/** /**
* Initializes download manager. * Initializes download manager.
* *
* @param IOInterface $io The Input Output Interface * @param IOInterface $io The Input Output Interface
* @param bool $preferSource prefer downloading from source * @param bool $preferSource prefer downloading from source
* @param Filesystem|null $filesystem custom Filesystem object * @param Filesystem|null $filesystem custom Filesystem object
*/ */
public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null) public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null)
{ {
@ -83,22 +85,6 @@ class DownloadManager
return $this; 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. * Sets installer downloader for a specific installation type.
* *
@ -140,7 +126,7 @@ class DownloadManager
* wrong type * wrong type
* @return DownloaderInterface|null * @return DownloaderInterface|null
*/ */
public function getDownloaderForInstalledPackage(PackageInterface $package) public function getDownloaderForPackage(PackageInterface $package)
{ {
$installationSource = $package->getInstallationSource(); $installationSource = $package->getInstallationSource();
@ -154,7 +140,7 @@ class DownloadManager
$downloader = $this->getDownloader($package->getSourceType()); $downloader = $this->getDownloader($package->getSourceType());
} else { } else {
throw new \InvalidArgumentException( 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; return $downloader;
} }
public function getDownloaderType(DownloaderInterface $downloader)
{
return array_search($downloader, $this->downloaders);
}
/** /**
* Downloads package into target dir. * Downloads package into target dir.
* *
* @param PackageInterface $package package instance * @param PackageInterface $package package instance
* @param string $targetDir target dir * @param string $targetDir target dir
* @param bool $preferSource prefer installation from source * @param PackageInterface|null $prevPackage previous package instance in case of updates
* *
* @return PromiseInterface
* @throws \InvalidArgumentException if package have no urls to download from * @throws \InvalidArgumentException if package have no urls to download from
* @throws \RuntimeException * @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; $targetDir = $this->normalizeTargetDir($targetDir);
$sourceType = $package->getSourceType(); $this->filesystem->ensureDirectoryExists(dirname($targetDir));
$distType = $package->getDistType();
$sources = array(); $sources = $this->getAvailableSources($package, $prevPackage);
if ($sourceType) {
$sources[] = 'source';
}
if ($distType) {
$sources[] = 'dist';
}
if (empty($sources)) { $io = $this->io;
throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified'); $self = $this;
}
if (!$preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) { $download = function ($retry = false) use (&$sources, $io, $package, $self, $targetDir, &$download, $prevPackage) {
$sources = array_reverse($sources); $source = array_shift($sources);
} if ($retry) {
$io->writeError(' <warning>Now trying to download from ' . $source . '</warning>');
$this->filesystem->ensureDirectoryExists($targetDir);
foreach ($sources as $i => $source) {
if (isset($e)) {
$this->io->writeError(' <warning>Now trying to download from ' . $source . '</warning>');
} }
$package->setInstallationSource($source); $package->setInstallationSource($source);
try {
$downloader = $this->getDownloaderForInstalledPackage($package); $downloader = $self->getDownloaderForPackage($package);
if ($downloader) { if (!$downloader) {
$downloader->download($package, $targetDir); return \React\Promise\resolve();
} }
break;
} catch (\RuntimeException $e) { $handleError = function ($e) use ($sources, $source, $package, $io, $download) {
if ($i === count($sources) - 1) { if ($e instanceof \RuntimeException) {
throw $e; if (!$sources) {
throw $e;
}
$io->writeError(
' <warning>Failed to download '.
$package->getPrettyName().
' from ' . $source . ': '.
$e->getMessage().'</warning>'
);
return $download(true);
} }
$this->io->writeError( throw $e;
' <warning>Failed to download '. };
$package->getPrettyName().
' from ' . $source . ': '. try {
$e->getMessage().'</warning>' $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 PackageInterface $target target package version
* @param string $targetDir target dir * @param string $targetDir target dir
* *
* @return PromiseInterface|null
* @throws \InvalidArgumentException if initial package is not installed * @throws \InvalidArgumentException if initial package is not installed
*/ */
public function update(PackageInterface $initial, PackageInterface $target, $targetDir) 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) { if (!$downloader) {
return; return $initialDownloader->remove($initial, $targetDir);
}
$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;
} }
$initialType = $this->getDownloaderType($initialDownloader);
$targetType = $this->getDownloaderType($downloader);
if ($initialType === $targetType) { if ($initialType === $targetType) {
$target->setInstallationSource($installationSource);
try { try {
$downloader->update($initial, $target, $targetDir); return $downloader->update($initial, $target, $targetDir);
return;
} catch (\RuntimeException $e) { } catch (\RuntimeException $e) {
if (!$this->io->isInteractive()) { if (!$this->io->isInteractive()) {
throw $e; throw $e;
@ -282,8 +313,17 @@ class DownloadManager
} }
} }
$downloader->remove($initial, $targetDir); // if downloader type changed, or update failed and user asks for reinstall,
$this->download($target, $targetDir, 'source' === $installationSource); // 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 PackageInterface $package package instance
* @param string $targetDir target dir * @param string $targetDir target dir
*
* @return PromiseInterface|null
*/ */
public function remove(PackageInterface $package, $targetDir) public function remove(PackageInterface $package, $targetDir)
{ {
$downloader = $this->getDownloaderForInstalledPackage($package); $targetDir = $this->normalizeTargetDir($targetDir);
$downloader = $this->getDownloaderForPackage($package);
if ($downloader) { 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 $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; namespace Composer\Downloader;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use React\Promise\PromiseInterface;
/** /**
* Downloader interface. * Downloader interface.
@ -30,12 +31,35 @@ interface DownloaderInterface
public function getInstallationSource(); 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 PackageInterface $package package instance
* @param string $path download path * @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. * Updates specific package in specific folder from initial to target version.
@ -55,10 +79,17 @@ interface DownloaderInterface
public function remove(PackageInterface $package, $path); 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 * Note that cleanup will be called for all packages regardless if they failed an operation or not, to give
* @return DownloaderInterface * 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\Plugin\PreFileDownloadEvent;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\Util\Url as UrlUtil; use Composer\Util\Url as UrlUtil;
use Composer\Downloader\TransportException;
/** /**
* Base downloader for files * Base downloader for files
@ -39,11 +40,13 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
{ {
protected $io; protected $io;
protected $config; protected $config;
protected $rfs; protected $httpDownloader;
protected $filesystem; protected $filesystem;
protected $cache; 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; private $eventDispatcher;
/** /**
@ -51,17 +54,17 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
* *
* @param IOInterface $io The IO instance * @param IOInterface $io The IO instance
* @param Config $config The config * @param Config $config The config
* @param HttpDownloader $httpDownloader The remote filesystem
* @param EventDispatcher $eventDispatcher The event dispatcher * @param EventDispatcher $eventDispatcher The event dispatcher
* @param Cache $cache Optional cache instance * @param Cache $cache Cache instance
* @param RemoteFilesystem $rfs The remote filesystem
* @param Filesystem $filesystem The filesystem * @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->io = $io;
$this->config = $config; $this->config = $config;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $config); $this->httpDownloader = $httpDownloader;
$this->filesystem = $filesystem ?: new Filesystem(); $this->filesystem = $filesystem ?: new Filesystem();
$this->cache = $cache; $this->cache = $cache;
@ -81,127 +84,191 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function download(PackageInterface $package, $path, $output = true) public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
{ {
if (!$package->getDistUrl()) { if (!$package->getDistUrl()) {
throw new \InvalidArgumentException('The given package is missing url information'); throw new \InvalidArgumentException('The given package is missing url information');
} }
if ($output) { $retries = 3;
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): ", false);
}
$urls = $package->getDistUrls(); $urls = $package->getDistUrls();
while ($url = array_shift($urls)) { foreach ($urls as $index => $url) {
try { $processedUrl = $this->processUrl($package, $url);
$fileName = $this->doDownload($package, $path, $url); $urls[$index] = array(
break; 'base' => $url,
} catch (\Exception $e) { 'processed' => $processedUrl,
if ($this->io->isDebug()) { 'cacheKey' => $this->getCacheKey($package, $processedUrl)
$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;
}
}
} }
if ($output) {
$this->io->writeError('');
}
return $fileName;
}
protected function doDownload(PackageInterface $package, $path, $url)
{
$this->filesystem->emptyDirectory($path);
$fileName = $this->getFileName($package, $path); $fileName = $this->getFileName($package, $path);
$this->filesystem->ensureDirectoryExists($path);
$this->filesystem->ensureDirectoryExists(dirname($fileName));
$processedUrl = $this->processUrl($package, $url); $io = $this->io;
$origin = RemoteFilesystem::getOrigin($processedUrl); $cache = $this->cache;
$httpDownloader = $this->httpDownloader;
$eventDispatcher = $this->eventDispatcher;
$filesystem = $this->filesystem;
$self = $this;
$preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $processedUrl); $accept = null;
if ($this->eventDispatcher) { $reject = null;
$this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); $download = function () use ($io, $output, $httpDownloader, $cache, $eventDispatcher, $package, $fileName, &$urls, &$accept, &$reject) {
} $url = reset($urls);
$rfs = $preFileDownloadEvent->getRemoteFilesystem();
if ($eventDispatcher) {
$preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed']);
$eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
}
try {
$checksum = $package->getDistSha1Checksum(); $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 // 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)) { if ($cache && (!$checksum || $checksum === $cache->sha1($cacheKey)) && $cache->copyTo($cacheKey, $fileName)) {
$this->io->writeError('Loading from cache', false); 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 { } else {
// download if cache restore failed if ($output) {
if (!$this->outputProgress) { $io->writeError(" - Downloading <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
$this->io->writeError('Downloading', false);
} }
// try to download 3 times then fail hard $result = $httpDownloader->addCopy($url['processed'], $fileName, $package->getTransportOptions())
$retries = 3; ->then($accept, $reject);
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);
}
} }
if (!file_exists($fileName)) { return $result->then(function ($result) use ($fileName, $checksum, $url) {
throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the' // in case of retry, the first call's Promise chain finally calls this twice at the end,
.' directory is writable and you have internet connectivity'); // 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) { $response->collect();
throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url.')');
} return $fileName;
} catch (\Exception $e) { };
$reject = function ($e) use ($io, &$urls, $download, $fileName, $package, &$retries, $filesystem, $self) {
// clean up // clean up
$this->filesystem->removeDirectory($path); if (file_exists($fileName)) {
$this->clearLastCacheWrite($package); $filesystem->unlink($fileName);
throw $e; }
} $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} * {@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()])) { if ($this->cache && isset($this->lastCacheWrites[$package->getName()])) {
$this->cache->remove($this->lastCacheWrites[$package->getName()]); $this->cache->remove($this->lastCacheWrites[$package->getName()]);
@ -218,11 +285,11 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
$from = $initial->getFullPrettyVersion(); $from = $initial->getFullPrettyVersion();
$to = $target->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->io->writeError(" - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
$this->remove($initial, $path, false); $this->remove($initial, $path, false);
$this->download($target, $path, false); $this->install($target, $path, false);
$this->io->writeError(''); $this->io->writeError('');
} }
@ -249,7 +316,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
*/ */
protected function getFileName(PackageInterface $package, $path) 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) public function getLocalChanges(PackageInterface $package, $targetDir)
{ {
$prevIO = $this->io; $prevIO = $this->io;
$prevProgress = $this->outputProgress;
$this->io = new NullIO; $this->io = new NullIO;
$this->io->loadConfiguration($this->config); $this->io->loadConfiguration($this->config);
$this->outputProgress = false;
$e = null; $e = null;
try { 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 = new Comparer();
$comparer->setSource($targetDir.'_compare'); $comparer->setSource($targetDir.'_compare');
@ -311,7 +378,6 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
} }
$this->io = $prevIO; $this->io = $prevIO;
$this->outputProgress = $prevProgress;
if ($e) { if ($e) {
throw $e; throw $e;

View File

@ -23,7 +23,15 @@ class FossilDownloader extends VcsDownloader
/** /**
* {@inheritDoc} * {@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 // Ensure we are allowed to use this URL by config
$this->config->prohibitUrlByConfig($url, $this->io); $this->config->prohibitUrlByConfig($url, $this->io);
@ -49,7 +57,7 @@ class FossilDownloader extends VcsDownloader
/** /**
* {@inheritDoc} * {@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 // Ensure we are allowed to use this URL by config
$this->config->prohibitUrlByConfig($url, $this->io); $this->config->prohibitUrlByConfig($url, $this->io);

View File

@ -17,6 +17,7 @@ use Composer\IO\IOInterface;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Git as GitUtil; use Composer\Util\Git as GitUtil;
use Composer\Util\Url;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Cache; use Composer\Cache;
@ -29,6 +30,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
private $hasStashedChanges = false; private $hasStashedChanges = false;
private $hasDiscardedChanges = false; private $hasDiscardedChanges = false;
private $gitUtil; private $gitUtil;
private $cachedPackages = array();
public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, Filesystem $fs = null) public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, Filesystem $fs = null)
{ {
@ -39,7 +41,28 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
/** /**
* {@inheritDoc} * {@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(); GitUtil::cleanEnv();
$path = $this->normalizePath($path); $path = $this->normalizePath($path);
@ -47,31 +70,20 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
$ref = $package->getSourceReference(); $ref = $package->getSourceReference();
$flag = Platform::isWindows() ? '/D ' : ''; $flag = Platform::isWindows() ? '/D ' : '';
// --dissociate option is only available since git 2.3.0-rc0 if (!empty($this->cachedPackages[$package->getId()][$ref])) {
$gitVersion = $this->gitUtil->getVersion(); $msg = "Cloning ".$this->getShortHash($ref).' from cache';
$msg = "Cloning ".$this->getShortHash($ref); $command =
'git clone --no-checkout %cachePath% %path% --dissociate --reference %cachePath% '
$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%'; . '&& cd '.$flag.'%path% '
if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) { . '&& git remote set-url origin %sanitizedUrl% && git remote add composer %sanitizedUrl%';
$this->io->writeError('', true, IOInterface::DEBUG); } else {
$this->io->writeError(sprintf(' Cloning to cache at %s', ProcessExecutor::escape($cachePath)), true, IOInterface::DEBUG); $msg = "Cloning ".$this->getShortHash($ref);
try { $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 (!$this->gitUtil->fetchRefOrSyncMirror($url, $cachePath, $ref)) { if (getenv('COMPOSER_DISABLE_NETWORK')) {
$this->io->writeError('<error>Failed to update '.$url.' in cache, package installation for '.$package->getPrettyName().' might fail.</error>'); throw new \RuntimeException('The required git reference for '.$package->getName().' is not in cache and network is disabled, aborting');
}
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;
}
} }
} }
$this->io->writeError($msg); $this->io->writeError($msg);
$commandCallable = function ($url) use ($path, $command, $cachePath) { $commandCallable = function ($url) use ($path, $command, $cachePath) {
@ -105,13 +117,52 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
{ {
GitUtil::cleanEnv(); GitUtil::cleanEnv();
$path = $this->normalizePath($path);
if (!$this->hasMetadataRepository($path)) { if (!$this->hasMetadataRepository($path)) {
throw new \RuntimeException('The .git directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); 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; $updateOriginUrl = false;
if ( if (
0 === $this->process->execute('git remote -v', $output, $path) 0 === $this->process->execute('git remote -v', $output, $path)
@ -122,28 +173,6 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
$updateOriginUrl = true; $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) { if ($updateOriginUrl) {
$this->updateOriginUrl($path, $target->getSourceUrl()); $this->updateOriginUrl($path, $target->getSourceUrl());
} }
@ -272,7 +301,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
$changes = array_map(function ($elem) { $changes = array_map(function ($elem) {
return ' '.$elem; return ' '.$elem;
}, preg_split('{\s*\r?\n\s*}', $changes)); }, 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)); $this->io->writeError(array_slice($changes, 0, 10));
if (count($changes) > 10) { if (count($changes) > 10) {
$this->io->writeError(' <info>' . (count($changes) - 10) . ' more files modified, choose "v" to view the full list</info>'); $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)); $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)) { 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)); $command = sprintf('git reset --hard %s --', ProcessExecutor::escape($reference));
if (0 === $this->process->execute($command, $output, $path)) { if (0 === $this->process->execute($command, $output, $path)) {
return; return null;
} }
} }
} }
$command = sprintf($template, ProcessExecutor::escape($gitRef)); $command = sprintf($template, ProcessExecutor::escape($gitRef));
if (0 === $this->process->execute($command, $output, $path)) { if (0 === $this->process->execute($command, $output, $path)) {
return; return null;
} }
// reference was not found (prints "fatal: reference is not a tree: $ref") // 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>'); $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) protected function updateOriginUrl($path, $url)

View File

@ -18,7 +18,7 @@ use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
/** /**
@ -28,17 +28,19 @@ use Composer\IO\IOInterface;
*/ */
class GzipDownloader extends ArchiveDownloader class GzipDownloader extends ArchiveDownloader
{ {
/** @var ProcessExecutor */
protected $process; 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); $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 // Try to use gunzip on *nix
if (!Platform::isWindows()) { if (!Platform::isWindows()) {
@ -63,14 +65,6 @@ class GzipDownloader extends ArchiveDownloader
$this->extractUsingExt($file, $targetFilepath); $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) private function extractUsingExt($file, $targetFilepath)
{ {
$archiveFile = gzopen($file, 'rb'); $archiveFile = gzopen($file, 'rb');

View File

@ -24,7 +24,15 @@ class HgDownloader extends VcsDownloader
/** /**
* {@inheritDoc} * {@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); $hgUtils = new HgUtils($this->io, $this->config, $this->process);
@ -44,7 +52,7 @@ class HgDownloader extends VcsDownloader
/** /**
* {@inheritDoc} * {@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); $hgUtils = new HgUtils($this->io, $this->config, $this->process);

View File

@ -37,7 +37,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function download(PackageInterface $package, $path, $output = true) public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
{ {
$url = $package->getDistUrl(); $url = $package->getDistUrl();
$realUrl = realpath($url); $realUrl = realpath($url);
@ -50,14 +50,6 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
} }
if (realpath($path) === $realUrl) { 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; return;
} }
@ -73,6 +65,29 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
$realUrl $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 // Get the transport options with default values
$transportOptions = $package->getTransportOptions() + array('symlink' => null, 'relative' => true); $transportOptions = $package->getTransportOptions() + array('symlink' => null, 'relative' => true);
@ -154,7 +169,9 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
$fileSystem->mirror($realUrl, $path, $iterator); $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()); $realUrl = realpath($package->getDistUrl());
if (realpath($path) === $realUrl) { if ($path === $realUrl) {
if ($output) { if ($output) {
$this->io->writeError(" - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>), source is still present in $path"); $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} * {@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(); $ref = $package->getSourceReference();
$label = $this->getLabelFromSourceReference($ref); $label = $this->getLabelFromSourceReference($ref);
@ -76,9 +84,9 @@ class PerforceDownloader extends VcsDownloader
/** /**
* {@inheritDoc} * {@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; namespace Composer\Downloader;
use Composer\Package\PackageInterface;
/** /**
* Downloader for phar files * Downloader for phar files
* *
@ -22,7 +24,7 @@ class PharDownloader extends ArchiveDownloader
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
protected function extract($file, $path) protected function extract(PackageInterface $package, $file, $path)
{ {
// Can throw an UnexpectedValueException // Can throw an UnexpectedValueException
$archive = new \Phar($file); $archive = new \Phar($file);

View File

@ -18,8 +18,9 @@ use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\IniHelper; use Composer\Util\IniHelper;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
use RarArchive; use RarArchive;
/** /**
@ -31,15 +32,16 @@ use RarArchive;
*/ */
class RarDownloader extends ArchiveDownloader class RarDownloader extends ArchiveDownloader
{ {
/** @var ProcessExecutor */
protected $process; 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); $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; $processError = null;

View File

@ -28,7 +28,15 @@ class SvnDownloader extends VcsDownloader
/** /**
* {@inheritDoc} * {@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(); SvnUtil::cleanEnv();
$ref = $package->getSourceReference(); $ref = $package->getSourceReference();
@ -42,13 +50,13 @@ class SvnDownloader extends VcsDownloader
} }
$this->io->writeError(" Checking out ".$package->getSourceReference()); $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} * {@inheritDoc}
*/ */
public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url)
{ {
SvnUtil::cleanEnv(); SvnUtil::cleanEnv();
$ref = $target->getSourceReference(); $ref = $target->getSourceReference();
@ -64,7 +72,7 @@ class SvnDownloader extends VcsDownloader
} }
$this->io->writeError(" Checking out " . $ref); $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 * @throws \RuntimeException
* @return string * @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 = new SvnUtil($baseUrl, $this->io, $this->config);
$util->setCacheCredentials($this->cacheCredentials); $util->setCacheCredentials($this->cacheCredentials);
@ -101,7 +109,7 @@ class SvnDownloader extends VcsDownloader
return $util->execute($command, $url, $cwd, $path, $this->io->isVerbose()); return $util->execute($command, $url, $cwd, $path, $this->io->isVerbose());
} catch (\RuntimeException $e) { } catch (\RuntimeException $e) {
throw new \RuntimeException( 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; return ' '.$elem;
}, preg_split('{\s*\r?\n\s*}', $changes)); }, preg_split('{\s*\r?\n\s*}', $changes));
$countChanges = count($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)); $this->io->writeError(array_slice($changes, 0, 10));
if ($countChanges > 10) { if ($countChanges > 10) {
$remainingChanges = $countChanges - 10; $remainingChanges = $countChanges - 10;

View File

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

View File

@ -20,6 +20,7 @@ use Composer\Package\Version\VersionParser;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use React\Promise\PromiseInterface;
/** /**
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
@ -54,44 +55,78 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
/** /**
* {@inheritDoc} * {@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()) { if (!$package->getSourceReference()) {
throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information'); throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information');
} }
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): ", false); $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)) { while ($url = array_shift($urls)) {
try { try {
if (Filesystem::isLocalPath($url)) { $this->doInstall($package, $path, $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);
break; break;
} catch (\Exception $e) { } catch (\Exception $e) {
// rethrow phpunit exceptions to avoid hard to debug bug failures // rethrow phpunit exceptions to avoid hard to debug bug failures
if ($e instanceof \PHPUnit_Framework_Exception) { if ($e instanceof \PHPUnit\Framework\Exception) {
throw $e; throw $e;
} }
if ($this->io->isDebug()) { if ($this->io->isDebug()) {
@ -130,25 +165,21 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
$to = $target->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->io->writeError(" - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
$this->cleanChanges($initial, $path, true); $urls = $this->prepareUrls($target->getSourceUrls());
$urls = $target->getSourceUrls();
$exception = null; $exception = null;
while ($url = array_shift($urls)) { while ($url = array_shift($urls)) {
try { try {
if (Filesystem::isLocalPath($url)) {
$url = realpath($url);
}
$this->doUpdate($initial, $target, $path, $url); $this->doUpdate($initial, $target, $path, $url);
$exception = null; $exception = null;
break; break;
} catch (\Exception $exception) { } catch (\Exception $exception) {
// rethrow phpunit exceptions to avoid hard to debug bug failures // rethrow phpunit exceptions to avoid hard to debug bug failures
if ($exception instanceof \PHPUnit_Framework_Exception) { if ($exception instanceof \PHPUnit\Framework\Exception) {
throw $exception; throw $exception;
} }
if ($this->io->isDebug()) { 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 // print the commit logs if in verbose mode and VCS metadata is present
// because in case of missing metadata code would trigger another exception // because in case of missing metadata code would trigger another exception
if (!$exception && $this->io->isVerbose() && $this->hasMetadataRepository($path)) { if (!$exception && $this->io->isVerbose() && $this->hasMetadataRepository($path)) {
@ -196,21 +225,11 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
public function remove(PackageInterface $package, $path) public function remove(PackageInterface $package, $path)
{ {
$this->io->writeError(" - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getPrettyVersion() . "</comment>)"); $this->io->writeError(" - Removing <info>" . $package->getName() . "</info> (<comment>" . $package->getPrettyVersion() . "</comment>)");
$this->cleanChanges($package, $path, false);
if (!$this->filesystem->removeDirectory($path)) { if (!$this->filesystem->removeDirectory($path)) {
throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); 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} * {@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 * @param string $path
* @throws \RuntimeException in case the operation must be aborted or the patch does not apply cleanly * @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. * Downloads specific package into specific folder.
* *
* @param PackageInterface $package package instance * @param PackageInterface $package package instance
* @param string $path download path * @param string $path download path
* @param string $url package url * @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. * 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 PackageInterface $target updated package
* @param string $path download path * @param string $path download path
* @param string $url package url * @param string $url package url
*
* @return PromiseInterface|null
*/ */
abstract protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url); abstract protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url);
@ -290,4 +325,33 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
* @return bool * @return bool
*/ */
abstract protected function hasMetadataRepository($path); 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\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
/** /**
@ -28,16 +28,17 @@ use Composer\IO\IOInterface;
*/ */
class XzDownloader extends ArchiveDownloader class XzDownloader extends ArchiveDownloader
{ {
/** @var ProcessExecutor */
protected $process; 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); $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); $command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path);
@ -49,12 +50,4 @@ class XzDownloader extends ArchiveDownloader
throw new \RuntimeException($processError); 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\IniHelper;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\ExecutableFinder;
use ZipArchive; use ZipArchive;
@ -33,19 +33,21 @@ class ZipDownloader extends ArchiveDownloader
private static $hasZipArchive; private static $hasZipArchive;
private static $isWindows; private static $isWindows;
/** @var ProcessExecutor */
protected $process; protected $process;
/** @var ZipArchive|null */
private $zipArchiveObject; 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); $this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $eventDispatcher, $cache, $rfs); parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
} }
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function download(PackageInterface $package, $path, $output = true) public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true)
{ {
if (null === self::$hasSystemUnzip) { if (null === self::$hasSystemUnzip) {
$finder = new ExecutableFinder; $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 $file File to extract
* @param string $path Path where to extract file * @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 // Each extract calls its alternative if not available or fails
if (self::$isWindows) { if (self::$isWindows) {

View File

@ -13,13 +13,16 @@
namespace Composer\EventDispatcher; namespace Composer\EventDispatcher;
use Composer\DependencyResolver\PolicyInterface; use Composer\DependencyResolver\PolicyInterface;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Request;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Transaction;
use Composer\Installer\InstallerEvent; use Composer\Installer\InstallerEvent;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Composer; use Composer\Composer;
use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\Repository\CompositeRepository; use Composer\Repository\CompositeRepository;
use Composer\Repository\RepositoryInterface;
use Composer\Repository\RepositorySet;
use Composer\Script; use Composer\Script;
use Composer\Installer\PackageEvent; use Composer\Installer\PackageEvent;
use Composer\Installer\BinaryInstaller; use Composer\Installer\BinaryInstaller;
@ -46,7 +49,7 @@ class EventDispatcher
protected $io; protected $io;
protected $loader; protected $loader;
protected $process; protected $process;
protected $listeners; protected $listeners = array();
private $eventStack; private $eventStack;
/** /**
@ -99,40 +102,34 @@ class EventDispatcher
/** /**
* Dispatch a package event. * Dispatch a package event.
* *
* @param string $eventName The constant in PackageEvents * @param string $eventName The constant in PackageEvents
* @param bool $devMode Whether or not we are in dev mode * @param bool $devMode Whether or not we are in dev mode
* @param PolicyInterface $policy The policy * @param RepositoryInterface $localRepo The installed repository
* @param Pool $pool The pool * @param array $operations The list of operations
* @param CompositeRepository $installedRepo The installed repository * @param OperationInterface $operation The package being installed/updated/removed
* @param Request $request The request
* @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 * @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 * 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. * Dispatch a installer event.
* *
* @param string $eventName The constant in InstallerEvents * @param string $eventName The constant in InstallerEvents
* @param bool $devMode Whether or not we are in dev mode * @param bool $devMode Whether or not we are in dev mode
* @param PolicyInterface $policy The policy * @param bool $executeOperations True if operations will be executed, false in --dry-run
* @param Pool $pool The pool * @param Transaction $transaction The transaction contains the list of operations
* @param CompositeRepository $installedRepo The installed repository
* @param Request $request The request
* @param array $operations The list of operations
* *
* @return int return code of the executed script if any, for php scripts a false return * @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 * 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'); 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); $event = $this->checkListenerExpectedEvent($callable, $event);
$return = false === call_user_func($callable, $event) ? 1 : 0; $return = false === call_user_func($callable, $event) ? 1 : 0;
} elseif ($this->isComposerScript($callable)) { } elseif ($this->isComposerScript($callable)) {
@ -172,8 +172,8 @@ class EventDispatcher
$args = array_merge($script, $event->getArguments()); $args = array_merge($script, $event->getArguments());
$flags = $event->getFlags(); $flags = $event->getFlags();
if (substr($callable, 0, 10) === '@composer ') { if (substr($callable, 0, 10) === '@composer ') {
$exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . substr($callable, 9); $exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . ' ' . implode(' ', $args);
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); $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); throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
@ -184,6 +184,7 @@ class EventDispatcher
} }
try { try {
/** @var InstallerEvent $event */
$scriptEvent = new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags); $scriptEvent = new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags);
$scriptEvent->setOriginatingEvent($event); $scriptEvent->setOriginatingEvent($event);
$return = $this->dispatch($scriptName, $scriptEvent); $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); $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); throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
@ -264,6 +265,15 @@ class EventDispatcher
return $return; return $return;
} }
protected function executeTty($exec)
{
if ($this->io->isInteractive()) {
return $this->process->executeTty($exec);
}
return $this->process->execute($exec);
}
protected function getPhpExecCommand() protected function getPhpExecCommand()
{ {
$finder = new PhpExecutableFinder(); $finder = new PhpExecutableFinder();
@ -327,44 +337,6 @@ class EventDispatcher
$expected = $typehint->getName(); $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; return $event;
} }
@ -397,6 +369,22 @@ class EventDispatcher
$this->listeners[$eventName][$priority][] = $listener; $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 * Adds object methods as listeners for the events in getSubscribedEvents
* *
@ -513,7 +501,7 @@ class EventDispatcher
* *
* @param Event $event * @param Event $event
* @throws \RuntimeException * @throws \RuntimeException
* @return number * @return int
*/ */
protected function pushEvent(Event $event) protected function pushEvent(Event $event)
{ {

View File

@ -23,7 +23,8 @@ use Composer\Repository\WritableRepositoryInterface;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\Util\Loop;
use Composer\Util\Silencer; use Composer\Util\Silencer;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
use Composer\EventDispatcher\Event; use Composer\EventDispatcher\Event;
@ -222,6 +223,13 @@ class Factory
return trim(getenv('COMPOSER')) ?: './composer.json'; 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() public static function createAdditionalStyles()
{ {
return array( return array(
@ -325,14 +333,15 @@ class Factory
$io->loadConfiguration($config); $io->loadConfiguration($config);
} }
$rfs = self::createRemoteFilesystem($io, $config); $httpDownloader = self::createHttpDownloader($io, $config);
$loop = new Loop($httpDownloader);
// initialize event dispatcher // initialize event dispatcher
$dispatcher = new EventDispatcher($composer, $io); $dispatcher = new EventDispatcher($composer, $io);
$composer->setEventDispatcher($dispatcher); $composer->setEventDispatcher($dispatcher);
// initialize repository manager // initialize repository manager
$rm = RepositoryFactory::manager($io, $config, $dispatcher, $rfs); $rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher);
$composer->setRepositoryManager($rm); $composer->setRepositoryManager($rm);
// load local repository // load local repository
@ -352,12 +361,12 @@ class Factory
$composer->setPackage($package); $composer->setPackage($package);
// initialize installation manager // initialize installation manager
$im = $this->createInstallationManager(); $im = $this->createInstallationManager($loop, $io, $dispatcher);
$composer->setInstallationManager($im); $composer->setInstallationManager($im);
if ($fullLoad) { if ($fullLoad) {
// initialize download manager // initialize download manager
$dm = $this->createDownloadManager($io, $config, $dispatcher, $rfs); $dm = $this->createDownloadManager($io, $config, $httpDownloader, $dispatcher);
$composer->setDownloadManager($dm); $composer->setDownloadManager($dm);
// initialize autoload generator // initialize autoload generator
@ -365,7 +374,7 @@ class Factory
$composer->setAutoloadGenerator($generator); $composer->setAutoloadGenerator($generator);
// initialize archive manager // initialize archive manager
$am = $this->createArchiveManager($config, $dm); $am = $this->createArchiveManager($config, $dm, $loop);
$composer->setArchiveManager($am); $composer->setArchiveManager($am);
} }
@ -386,11 +395,9 @@ class Factory
// init locker if possible // init locker if possible
if ($fullLoad && isset($composerFile)) { if ($fullLoad && isset($composerFile)) {
$lockFile = "json" === pathinfo($composerFile, PATHINFO_EXTENSION) $lockFile = self::getLockFile($composerFile);
? substr($composerFile, 0, -4).'lock'
: $composerFile . '.lock';
$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); $composer->setLocker($locker);
} }
@ -411,7 +418,7 @@ class Factory
/** /**
* @param IOInterface $io IO instance * @param IOInterface $io IO instance
* @param bool $disablePlugins Whether plugins should not be loaded * @param bool $disablePlugins Whether plugins should not be loaded
* @return Composer * @return Composer|null
*/ */
public static function createGlobal(IOInterface $io, $disablePlugins = false) public static function createGlobal(IOInterface $io, $disablePlugins = false)
{ {
@ -451,7 +458,7 @@ class Factory
* @param EventDispatcher $eventDispatcher * @param EventDispatcher $eventDispatcher
* @return Downloader\DownloadManager * @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; $cache = null;
if ($config->get('cache-files-ttl') > 0) { 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('fossil', new Downloader\FossilDownloader($io, $config, $executor, $fs));
$dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs)); $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs));
$dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config)); $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config));
$dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache, $rfs)); $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
$dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); $dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache, $rfs)); $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
$dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache, $rfs)); $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
$dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $eventDispatcher, $cache, $rfs)); $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
return $dm; return $dm;
} }
@ -501,15 +508,9 @@ class Factory
* @param Downloader\DownloadManager $dm Manager use to download sources * @param Downloader\DownloadManager $dm Manager use to download sources
* @return Archiver\ArchiveManager * @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) { $am = new Archiver\ArchiveManager($dm, $loop);
$io = new IO\NullIO();
$io->loadConfiguration($config);
$dm = $this->createDownloadManager($io, $config);
}
$am = new Archiver\ArchiveManager($dm);
$am->addArchiver(new Archiver\ZipArchiver); $am->addArchiver(new Archiver\ZipArchiver);
$am->addArchiver(new Archiver\PharArchiver); $am->addArchiver(new Archiver\PharArchiver);
@ -531,9 +532,9 @@ class Factory
/** /**
* @return Installer\InstallationManager * @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 IOInterface $io IO instance
* @param Config $config Config instance * @param Config $config Config instance
* @param array $options Array of options passed directly to RemoteFilesystem constructor * @param array $options Array of options passed directly to HttpDownloader constructor
* @return RemoteFilesystem * @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; static $warned = false;
$disableTls = 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. ' 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.'); . '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 ($disableTls === false) {
if ($config && $config->get('cafile')) { if ($config && $config->get('cafile')) {
$remoteFilesystemOptions['ssl']['cafile'] = $config->get('cafile'); $httpDownloaderOptions['ssl']['cafile'] = $config->get('cafile');
} }
if ($config && $config->get('capath')) { 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 { try {
$remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls); $httpDownloader = new HttpDownloader($io, $config, $httpDownloaderOptions, $disableTls);
} catch (TransportException $e) { } catch (TransportException $e) {
if (false !== strpos($e->getMessage(), 'cafile')) { 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>'); $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; throw $e;
} }
return $remoteFilesystem; return $httpDownloader;
} }
/** /**

View File

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

View File

@ -13,13 +13,14 @@
namespace Composer\IO; namespace Composer\IO;
use Composer\Config; use Composer\Config;
use Psr\Log\LoggerInterface;
/** /**
* The Input/Output helper interface. * The Input/Output helper interface.
* *
* @author François Pluchino <francois.pluchino@opendisplay.com> * @author François Pluchino <francois.pluchino@opendisplay.com>
*/ */
interface IOInterface interface IOInterface extends LoggerInterface
{ {
const QUIET = 1; const QUIET = 1;
const NORMAL = 2; const NORMAL = 2;
@ -80,6 +81,24 @@ interface IOInterface
*/ */
public function writeError($messages, $newline = true, $verbosity = self::NORMAL); 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. * 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 * @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 * @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); public function ask($question, $default = null);
@ -145,7 +164,7 @@ interface IOInterface
* *
* @param string $question The question to ask * @param string $question The question to ask
* *
* @return string The answer * @return string|null The answer
*/ */
public function askAndHideAnswer($question); public function askAndHideAnswer($question);
@ -160,7 +179,7 @@ interface IOInterface
* @param bool $multiselect Select more than one value separated by comma * @param bool $multiselect Select more than one value separated by comma
* *
* @throws \InvalidArgumentException * @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); 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\UninstallOperation;
use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation; use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation;
use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\StreamContextFactory; use Composer\Util\StreamContextFactory;
use Composer\Util\Loop;
/** /**
* Package operation manager. * Package operation manager.
@ -37,6 +39,16 @@ class InstallationManager
private $installers = array(); private $installers = array();
private $cache = array(); private $cache = array();
private $notifiablePackages = 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() public function reset()
{ {
@ -151,13 +163,105 @@ class InstallationManager
/** /**
* Executes solver operation. * Executes solver operation.
* *
* @param RepositoryInterface $repo repository in which to check * @param RepositoryInterface $repo repository in which to add/remove/update packages
* @param OperationInterface $operation operation instance * @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(); $promises = array();
$this->$method($repo, $operation);
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(); $package = $operation->getPackage();
$installer = $this->getInstaller($package->getType()); $installer = $this->getInstaller($package->getType());
$installer->install($repo, $package); $promise = $installer->install($repo, $package);
$this->markForNotification($package); $this->markForNotification($package);
return $promise;
} }
/** /**
@ -190,12 +296,15 @@ class InstallationManager
if ($initialType === $targetType) { if ($initialType === $targetType) {
$installer = $this->getInstaller($initialType); $installer = $this->getInstaller($initialType);
$installer->update($repo, $initial, $target); $promise = $installer->update($repo, $initial, $target);
$this->markForNotification($target); $this->markForNotification($target);
} else { } else {
$this->getInstaller($initialType)->uninstall($repo, $initial); $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(); $package = $operation->getPackage();
$installer = $this->getInstaller($package->getType()); $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\Composer;
use Composer\DependencyResolver\PolicyInterface; use Composer\DependencyResolver\PolicyInterface;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Request;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Transaction;
use Composer\EventDispatcher\Event; use Composer\EventDispatcher\Event;
use Composer\IO\IOInterface; 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 class InstallerEvent extends Event
{ {
/** /**
@ -44,29 +39,14 @@ class InstallerEvent extends Event
private $devMode; private $devMode;
/** /**
* @var PolicyInterface * @var bool
*/ */
private $policy; private $executeOperations;
/** /**
* @var Pool * @var Transaction
*/ */
private $pool; private $transaction;
/**
* @var CompositeRepository
*/
private $installedRepo;
/**
* @var Request
*/
private $request;
/**
* @var OperationInterface[]
*/
private $operations;
/** /**
* Constructor. * Constructor.
@ -75,24 +55,18 @@ class InstallerEvent extends Event
* @param Composer $composer * @param Composer $composer
* @param IOInterface $io * @param IOInterface $io
* @param bool $devMode * @param bool $devMode
* @param PolicyInterface $policy * @param bool $executeOperations
* @param Pool $pool * @param Transaction $transaction
* @param CompositeRepository $installedRepo
* @param Request $request
* @param OperationInterface[] $operations
*/ */
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); parent::__construct($eventName);
$this->composer = $composer; $this->composer = $composer;
$this->io = $io; $this->io = $io;
$this->devMode = $devMode; $this->devMode = $devMode;
$this->policy = $policy; $this->executeOperations = $executeOperations;
$this->pool = $pool; $this->transaction = $transaction;
$this->installedRepo = $installedRepo;
$this->request = $request;
$this->operations = $operations;
} }
/** /**
@ -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 $this->transaction;
}
/**
* @return CompositeRepository
*/
public function getInstalledRepo()
{
return $this->installedRepo;
}
/**
* @return Request
*/
public function getRequest()
{
return $this->request;
}
/**
* @return OperationInterface[]
*/
public function getOperations()
{
return $this->operations;
} }
} }

View File

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

View File

@ -15,6 +15,7 @@ namespace Composer\Installer;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Repository\InstalledRepositoryInterface; use Composer\Repository\InstalledRepositoryInterface;
use InvalidArgumentException; use InvalidArgumentException;
use React\Promise\PromiseInterface;
/** /**
* Interface for the package installation manager. * Interface for the package installation manager.
@ -42,20 +43,46 @@ interface InstallerInterface
*/ */
public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package); 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. * Installs specific package.
* *
* @param InstalledRepositoryInterface $repo repository in which to check * @param InstalledRepositoryInterface $repo repository in which to check
* @param PackageInterface $package package instance * @param PackageInterface $package package instance
* @return PromiseInterface|null
*/ */
public function install(InstalledRepositoryInterface $repo, PackageInterface $package); public function install(InstalledRepositoryInterface $repo, PackageInterface $package);
/** /**
* Updates specific package. * Updates specific package.
* *
* @param InstalledRepositoryInterface $repo repository in which to check * @param InstalledRepositoryInterface $repo repository in which to check
* @param PackageInterface $initial already installed package version * @param PackageInterface $initial already installed package version
* @param PackageInterface $target updated version * @param PackageInterface $target updated version
* @return PromiseInterface|null
* *
* @throws InvalidArgumentException if $initial package is not installed * @throws InvalidArgumentException if $initial package is not installed
*/ */
@ -64,11 +91,26 @@ interface InstallerInterface
/** /**
* Uninstalls specific package. * Uninstalls specific package.
* *
* @param InstalledRepositoryInterface $repo repository in which to check * @param InstalledRepositoryInterface $repo repository in which to check
* @param PackageInterface $package package instance * @param PackageInterface $package package instance
* @return PromiseInterface|null
*/ */
public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package); 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 * Returns the installation path of a package
* *

View File

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

View File

@ -47,6 +47,30 @@ class MetapackageInstaller implements InstallerInterface
return $repo->hasPackage($package); 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} * {@inheritDoc}
*/ */
@ -69,7 +93,7 @@ class MetapackageInstaller implements InstallerInterface
$name = $target->getName(); $name = $target->getName();
$from = $initial->getFullPrettyVersion(); $from = $initial->getFullPrettyVersion();
$to = $target->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>)"); $this->io->writeError(" - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>)");
$repo->removePackage($initial); $repo->removePackage($initial);

View File

@ -40,6 +40,27 @@ class NoopInstaller implements InstallerInterface
return $repo->hasPackage($package); 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} * {@inheritDoc}
*/ */

View File

@ -16,19 +16,45 @@ use Composer\Composer;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\PolicyInterface; use Composer\DependencyResolver\PolicyInterface;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Request;
use Composer\Repository\CompositeRepository; use Composer\Repository\RepositoryInterface;
use Composer\Repository\RepositorySet;
use Composer\EventDispatcher\Event;
/** /**
* The Package Event. * The Package Event.
* *
* @author Jordi Boggiano <j.boggiano@seld.be> * @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; private $operation;
@ -39,20 +65,63 @@ class PackageEvent extends InstallerEvent
* @param Composer $composer * @param Composer $composer
* @param IOInterface $io * @param IOInterface $io
* @param bool $devMode * @param bool $devMode
* @param PolicyInterface $policy * @param RepositoryInterface $localRepo
* @param Pool $pool
* @param CompositeRepository $installedRepo
* @param Request $request * @param Request $request
* @param OperationInterface[] $operations * @param OperationInterface[] $operations
* @param OperationInterface $operation * @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; $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. * Returns the package instance.
* *

View File

@ -50,19 +50,27 @@ class PluginInstaller extends LibraryInstaller
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function install(InstalledRepositoryInterface $repo, PackageInterface $package) public function download(PackageInterface $package, PackageInterface $prevPackage = null)
{ {
$extra = $package->getExtra(); $extra = $package->getExtra();
if (empty($extra['class'])) { 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.'); 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); parent::install($repo, $package);
try { try {
$this->composer->getPluginManager()->registerPackage($package, true); $this->composer->getPluginManager()->registerPackage($package, true);
} catch (\Exception $e) { } catch (\Exception $e) {
// Rollback installation // Rollback installation
$this->io->writeError('Plugin installation failed, rolling back'); $this->io->writeError('Plugin initialization failed, uninstalling plugin');
parent::uninstall($repo, $package); parent::uninstall($repo, $package);
throw $e; throw $e;
} }
@ -73,12 +81,22 @@ class PluginInstaller extends LibraryInstaller
*/ */
public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) 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); 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} * {@inheritDoc}
*/ */
public function install(InstalledRepositoryInterface $repo, PackageInterface $package) public function download(PackageInterface $package, PackageInterface $prevPackage = null)
{ {
$installPath = $this->installPath; $installPath = $this->installPath;
if (file_exists($installPath) && !$this->filesystem->isDirEmpty($installPath)) { if (file_exists($installPath) && !$this->filesystem->isDirEmpty($installPath)) {
@ -67,7 +67,32 @@ class ProjectInstaller implements InstallerInterface
if (!is_dir($installPath)) { if (!is_dir($installPath)) {
mkdir($installPath, 0777, true); 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 class SuggestedPackagesReporter
{ {
const MODE_LIST = 1;
const MODE_BY_PACKAGE = 2;
const MODE_BY_SUGGESTION = 4;
/** /**
* @var array * @var array
*/ */
@ -91,38 +95,105 @@ class SuggestedPackagesReporter
/** /**
* Output suggested packages. * Output suggested packages.
*
* Do not list the ones already installed if installed repository provided. * 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 * @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(); $suggestedPackages = $this->getPackages();
$installedPackages = array(); $installedNames = array();
if (null !== $installedRepo && ! empty($suggestedPackages)) { if (null !== $installedRepo && !empty($suggestedPackages)) {
foreach ($installedRepo->getPackages() as $package) { foreach ($installedRepo->getPackages() as $package) {
$installedPackages = array_merge( $installedNames = array_merge(
$installedPackages, $installedNames,
$package->getNames() $package->getNames()
); );
} }
} }
$suggestions = array();
foreach ($suggestedPackages as $suggestion) { foreach ($suggestedPackages as $suggestion) {
if (in_array($suggestion['target'], $installedPackages)) { if (in_array($suggestion['target'], $installedNames)) {
continue; continue;
} }
$this->io->writeError(sprintf( $suggestions[] = $suggestion;
'%s suggests installing %s%s',
$suggestion['source'],
$this->escapeOutput($suggestion['target']),
$this->escapeOutput('' !== $suggestion['reason'] ? ' ('.$suggestion['reason'].')' : '')
));
} }
return $this; return $suggestions;
} }
/** /**

View File

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

View File

@ -416,4 +416,9 @@ class AliasPackage extends BasePackage implements CompletePackageInterface
{ {
return $this->aliasOf->setDistType($type); 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\PackageInterface;
use Composer\Package\RootPackageInterface; use Composer\Package\RootPackageInterface;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Loop;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
/** /**
@ -25,6 +26,7 @@ use Composer\Json\JsonFile;
class ArchiveManager class ArchiveManager
{ {
protected $downloadManager; protected $downloadManager;
protected $loop;
protected $archivers = array(); protected $archivers = array();
@ -36,9 +38,10 @@ class ArchiveManager
/** /**
* @param DownloadManager $downloadManager A manager used to download package sources * @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->downloadManager = $downloadManager;
$this->loop = $loop;
} }
/** /**
@ -149,7 +152,9 @@ class ArchiveManager
try { try {
// Download sources // 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) { } catch (\Exception $e) {
$filesystem->removeDirectory($sourcePath); $filesystem->removeDirectory($sourcePath);
throw $e; throw $e;

View File

@ -210,18 +210,30 @@ abstract class BasePackage implements PackageInterface
/** /**
* {@inheritDoc} * {@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(); return $this->getPrettyVersion();
} }
// if source reference is a sha1 hash -- truncate switch ($displayMode) {
if ($truncate && strlen($this->getSourceReference()) === 40) { case PackageInterface::DISPLAY_SOURCE_REF_IF_DEV:
return $this->getPrettyVersion() . ' ' . substr($this->getSourceReference(), 0, 7); 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() public function getStabilityPriority()
@ -238,14 +250,14 @@ abstract class BasePackage implements PackageInterface
/** /**
* Build a regexp from a package name, expanding * globs as required * 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 * @param string $wrap Wrap the cleaned string by the given string
* @return 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