diff --git a/.travis.yml b/.travis.yml index 800a4f2f1..4aed11625 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,19 +5,19 @@ php: - 5.3 - 5.4 - 5.5 + - 5.6 - hhvm -matrix: - allow_failures: - - php: hhvm - before_script: - sudo apt-get install parallel - rm -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini - - composer install --dev --prefer-source - - bin/composer install --dev --prefer-source + - composer install --prefer-source + - bin/composer install --prefer-source - git config --global user.name travis-ci - git config --global user.email travis@example.com script: - - ls -d tests/Composer/Test/* | parallel --gnu --keep-order 'echo "Running {} tests"; ./vendor/bin/phpunit -c tests/complete.phpunit.xml {};' || exit 1 + - ls -d tests/Composer/Test/* | parallel --gnu --keep-order 'echo "Running {} tests"; ./vendor/bin/phpunit -c tests/complete.phpunit.xml {};' + +git: + depth: 5 diff --git a/CHANGELOG.md b/CHANGELOG.md index e152f1ddc..d78b8e109 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +### 1.0.0-alpha9 (2014-12-07) + + * Added `remove` command to do the reverse of `require` + * Added --ignore-platform-reqs to `install`/`update` commands to install even if you are missing a php extension or have an invalid php version + * Added a warning when abandoned packages are being installed + * Added auto-selection of the version constraint in the `require` command, which can now be used simply as `composer require foo/bar` + * Added ability to define custom composer commands using scripts + * Added `browse` command to open a browser to the given package's repo URL (or homepage with `-H`) + * Added an `autoload-dev` section to declare dev-only autoload rules + a --no-dev flag to dump-autoload + * Added an `auth.json` file, with `store-auths` config option + * Added a `http-basic` config option to store login/pwds to hosts + * Added failover to source/dist and vice-versa in case a download method fails + * Added --path (-P) flag to the show command to see the install path of packages + * Added --update-with-dependencies and --update-no-dev flags to the require command + * Added `optimize-autoloader` config option to force the `-o` flag from the config + * Added `clear-cache` command + * Added a GzipDownloader to download single gzipped files + * Added `ssh` support in the `github-protocols` config option + * Added `pre-dependencies-solving` and `post-dependencies-solving` events + * Added `pre-archive-cmd` and `post-archive-cmd` script events to the `archive` command + * Added a `no-api` flag to GitHub VCS repos to skip the API but still get zip downloads + * Added http-basic auth support for private git repos not on github + * Added support for autoloading `.hh` files when running HHVM + * Added support for PHP 5.6 + * Added support for OTP auth when retrieving a GitHub API key + * Fixed isolation of `files` autoloaded scripts to ensure they can not affect anything + * Improved performance of solving dependencies + * Improved SVN and Perforce support + * A boatload of minor fixes, documentation additions and UX improvements + ### 1.0.0-alpha8 (2014-01-06) * Break: The `install` command now has --dev enabled by default. --no-dev can be used to install without dev requirements diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..820a4002e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +Contributing to Composer +======================== + +Installation from Source +------------------------ + +Prior to contributing to Composer, you must use be able to run the tests. +To achieve this, you must use the sources and not the phar file. + +1. Run `git clone https://github.com/composer/composer.git` +2. Download the [`composer.phar`](https://getcomposer.org/composer.phar) executable +3. Run Composer to get the dependencies: `cd composer && php ../composer.phar install` + +You can now run Composer by executing the `bin/composer` script: `php /path/to/composer/bin/composer` + +Contributing policy +------------------- + +All code contributions - including those of people having commit access - +must go through a pull request and approved by a core developer before being +merged. This is to ensure proper review of all the code. + +Fork the project, create a feature branch, and send us a pull request. + +To ensure a consistent code base, you should make sure the code follows +the [Coding Standards](http://symfony.com/doc/current/contributing/code/standards.html) +which we borrowed from Symfony. + +If you would like to help, take a look at the [list of issues](http://github.com/composer/composer/issues). diff --git a/README.md b/README.md index e0e4e73b0..728e11f2e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ Composer - Dependency Management for PHP ======================================== -Composer is a dependency manager tracking local dependencies of your projects and libraries. +Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere. See [https://getcomposer.org/](https://getcomposer.org/) for more information and documentation. -[![Build Status](https://secure.travis-ci.org/composer/composer.png?branch=master)](http://travis-ci.org/composer/composer) +[![Build Status](https://travis-ci.org/composer/composer.svg?branch=master)](https://travis-ci.org/composer/composer) Installation / Usage -------------------- @@ -32,18 +32,6 @@ themselves. To create libraries/packages please read the 3. Run Composer: `php composer.phar install` 4. Browse for more packages on [Packagist](https://packagist.org). -Installation from Source ------------------------- - -To run tests, or develop Composer itself, you must use the sources and not the phar -file as described above. - -1. Run `git clone https://github.com/composer/composer.git` -2. Download the [`composer.phar`](https://getcomposer.org/composer.phar) executable -3. Run Composer to get the dependencies: `cd composer && php ../composer.phar install` - -You can now run Composer by executing the `bin/composer` script: `php /path/to/composer/bin/composer` - Global installation of Composer (manual) ---------------------------------------- @@ -55,20 +43,6 @@ Updating Composer Running `php composer.phar self-update` or equivalent will update a phar install with the latest version. -Contributing ------------- - -All code contributions - including those of people having commit access - -must go through a pull request and approved by a core developer before being -merged. This is to ensure proper review of all the code. - -Fork the project, create a feature branch, and send us a pull request. - -To ensure a consistent code base, you should make sure the code follows -the [Coding Standards](http://symfony.com/doc/2.0/contributing/code/standards.html) -which we borrowed from Symfony. - -If you would like to help take a look at the [list of issues](http://github.com/composer/composer/issues). Community --------- diff --git a/composer.json b/composer.json index f0d4ee56e..9fca7d74c 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "composer/composer", - "description": "Dependency Manager", + "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.", "keywords": ["package", "dependency", "autoload"], "homepage": "http://getcomposer.org/", "type": "library", @@ -23,14 +23,14 @@ }, "require": { "php": ">=5.3.2", - "justinrainbow/json-schema": "1.1.*", - "seld/jsonlint": "1.*", + "justinrainbow/json-schema": "~1.3", + "seld/jsonlint": "~1.0", "symfony/console": "~2.3", "symfony/finder": "~2.2", "symfony/process": "~2.1" }, "require-dev": { - "phpunit/phpunit": "~3.7.10" + "phpunit/phpunit": "~4.0" }, "suggest": { "ext-zip": "Enabling the zip extension allows you to unzip archives, and allows gzip compression of all internet traffic", @@ -39,10 +39,16 @@ "autoload": { "psr-0": { "Composer": "src/" } }, + "autoload-dev": { + "psr-0": { "Composer\\Test": "tests/" } + }, "bin": ["bin/composer"], "extra": { "branch-alias": { "dev-master": "1.0-dev" } + }, + "scripts": { + "test": "phpunit" } } diff --git a/composer.lock b/composer.lock index e09b12af3..7d28fcb09 100644 --- a/composer.lock +++ b/composer.lock @@ -1,47 +1,89 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" ], - "hash": "370b764a9317165e8ea7a2e1623e031b", + "hash": "2bc9cc8aa706b68d611d7058e4eb8de7", "packages": [ { "name": "justinrainbow/json-schema", - "version": "1.1.0", + "version": "1.3.7", "source": { "type": "git", - "url": "git://github.com/justinrainbow/json-schema.git", - "reference": "v1.1.0" + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "87b54b460febed69726c781ab67462084e97a105" }, "dist": { "type": "zip", - "url": "https://github.com/justinrainbow/json-schema/zipball/v1.1.0", - "reference": "v1.1.0", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/87b54b460febed69726c781ab67462084e97a105", + "reference": "87b54b460febed69726c781ab67462084e97a105", "shasum": "" }, "require": { "php": ">=5.3.0" }, + "require-dev": { + "json-schema/json-schema-test-suite": "1.1.0", + "phpdocumentor/phpdocumentor": "~2", + "phpunit/phpunit": "~3.7" + }, + "bin": [ + "bin/validate-json" + ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, "autoload": { "psr-0": { "JsonSchema": "src/" } }, - "time": "2012-01-02 21:33:17" + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "time": "2014-08-25 02:48:14" }, { "name": "seld/jsonlint", - "version": "1.1.2", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "7cd4c4965e17e6e4c07f26d566619a4c76f8c672" + "reference": "a7bc2ec9520ad15382292591b617c43bdb1fec35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/7cd4c4965e17e6e4c07f26d566619a4c76f8c672", - "reference": "7cd4c4965e17e6e4c07f26d566619a4c76f8c672", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/a7bc2ec9520ad15382292591b617c43bdb1fec35", + "reference": "a7bc2ec9520ad15382292591b617c43bdb1fec35", "shasum": "" }, "require": { @@ -52,8 +94,8 @@ ], "type": "library", "autoload": { - "psr-0": { - "Seld\\JsonLint": "src/" + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" } }, "notification-url": "https://packagist.org/downloads/", @@ -64,8 +106,7 @@ { "name": "Jordi Boggiano", "email": "j.boggiano@seld.be", - "homepage": "http://seld.be", - "role": "Developer" + "homepage": "http://seld.be" } ], "description": "JSON Linter", @@ -75,36 +116,40 @@ "parser", "validator" ], - "time": "2013-11-04 15:41:11" + "time": "2014-09-05 15:36:20" }, { "name": "symfony/console", - "version": "v2.4.1", + "version": "v2.6.1", "target-dir": "Symfony/Component/Console", "source": { "type": "git", "url": "https://github.com/symfony/Console.git", - "reference": "4c1ed2ff514bd85ee186eebb010ccbdeeab05af7" + "reference": "ef825fd9f809d275926547c9e57cbf14968793e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/4c1ed2ff514bd85ee186eebb010ccbdeeab05af7", - "reference": "4c1ed2ff514bd85ee186eebb010ccbdeeab05af7", + "url": "https://api.github.com/repos/symfony/Console/zipball/ef825fd9f809d275926547c9e57cbf14968793e8", + "reference": "ef825fd9f809d275926547c9e57cbf14968793e8", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "symfony/event-dispatcher": "~2.1" + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.1", + "symfony/process": "~2.1" }, "suggest": { - "symfony/event-dispatcher": "" + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/process": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.6-dev" } }, "autoload": { @@ -117,32 +162,32 @@ "MIT" ], "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, { "name": "Symfony Community", "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" } ], "description": "Symfony Console Component", "homepage": "http://symfony.com", - "time": "2014-01-01 08:14:50" + "time": "2014-12-02 20:19:20" }, { "name": "symfony/finder", - "version": "v2.4.1", + "version": "v2.6.1", "target-dir": "Symfony/Component/Finder", "source": { "type": "git", "url": "https://github.com/symfony/Finder.git", - "reference": "6904345cf2b3bbab1f6d6e4ce1724cb99df9f00a" + "reference": "0d3ef7f6ec55a7af5eca7914eaa0dacc04ccc721" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Finder/zipball/6904345cf2b3bbab1f6d6e4ce1724cb99df9f00a", - "reference": "6904345cf2b3bbab1f6d6e4ce1724cb99df9f00a", + "url": "https://api.github.com/repos/symfony/Finder/zipball/0d3ef7f6ec55a7af5eca7914eaa0dacc04ccc721", + "reference": "0d3ef7f6ec55a7af5eca7914eaa0dacc04ccc721", "shasum": "" }, "require": { @@ -151,7 +196,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.6-dev" } }, "autoload": { @@ -164,32 +209,32 @@ "MIT" ], "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, { "name": "Symfony Community", "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" } ], "description": "Symfony Finder Component", "homepage": "http://symfony.com", - "time": "2014-01-01 08:14:50" + "time": "2014-12-02 20:19:20" }, { "name": "symfony/process", - "version": "v2.4.1", + "version": "v2.6.1", "target-dir": "Symfony/Component/Process", "source": { "type": "git", "url": "https://github.com/symfony/Process.git", - "reference": "58fdccb311e44f28866f976c2d7b3227e9f713db" + "reference": "bf0c9bd625f13b0b0bbe39919225cf145dfb935a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Process/zipball/58fdccb311e44f28866f976c2d7b3227e9f713db", - "reference": "58fdccb311e44f28866f976c2d7b3227e9f713db", + "url": "https://api.github.com/repos/symfony/Process/zipball/bf0c9bd625f13b0b0bbe39919225cf145dfb935a", + "reference": "bf0c9bd625f13b0b0bbe39919225cf145dfb935a", "shasum": "" }, "require": { @@ -198,7 +243,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.6-dev" } }, "autoload": { @@ -211,57 +256,115 @@ "MIT" ], "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, { "name": "Symfony Community", "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" } ], "description": "Symfony Process Component", "homepage": "http://symfony.com", - "time": "2014-01-05 02:10:50" + "time": "2014-12-02 20:19:20" } ], "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "f976e5de371104877ebc89bd8fecb0019ed9c119" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f976e5de371104877ebc89bd8fecb0019ed9c119", + "reference": "f976e5de371104877ebc89bd8fecb0019ed9c119", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "2.0.*@ALPHA" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Instantiator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2014-10-13 12:58:55" + }, { "name": "phpunit/php-code-coverage", - "version": "1.2.13", + "version": "2.0.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "466e7cd2554b4e264c9e3f31216d25ac0e5f3d94" + "reference": "ca158276c1200cc27f5409a5e338486bc0b4fc94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/466e7cd2554b4e264c9e3f31216d25ac0e5f3d94", - "reference": "466e7cd2554b4e264c9e3f31216d25ac0e5f3d94", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca158276c1200cc27f5409a5e338486bc0b4fc94", + "reference": "ca158276c1200cc27f5409a5e338486bc0b4fc94", "shasum": "" }, "require": { "php": ">=5.3.3", - "phpunit/php-file-iterator": ">=1.3.0@stable", - "phpunit/php-text-template": ">=1.1.1@stable", - "phpunit/php-token-stream": ">=1.1.3@stable" + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "~1.3", + "sebastian/environment": "~1.0", + "sebastian/version": "~1.0" }, "require-dev": { - "phpunit/phpunit": "3.7.*@dev" + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "~4.1" }, "suggest": { "ext-dom": "*", - "ext-xdebug": ">=2.0.5" + "ext-xdebug": ">=2.2.1", + "ext-xmlwriter": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { "classmap": [ - "PHP/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -285,7 +388,7 @@ "testing", "xunit" ], - "time": "2013-09-10 08:14:32" + "time": "2014-12-26 13:28:33" }, { "name": "phpunit/php-file-iterator", @@ -334,16 +437,16 @@ }, { "name": "phpunit/php-text-template", - "version": "1.1.4", + "version": "1.2.0", "source": { "type": "git", - "url": "git://github.com/sebastianbergmann/php-text-template.git", - "reference": "1.1.4" + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a" }, "dist": { "type": "zip", - "url": "https://github.com/sebastianbergmann/php-text-template/zipball/1.1.4", - "reference": "1.1.4", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", + "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", "shasum": "" }, "require": { @@ -374,7 +477,7 @@ "keywords": [ "template" ], - "time": "2012-10-31 11:15:28" + "time": "2014-01-30 17:20:04" }, { "name": "phpunit/php-timer", @@ -422,45 +525,44 @@ }, { "name": "phpunit/php-token-stream", - "version": "1.2.1", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "5220af2a7929aa35cf663d97c89ad3d50cf5fa3e" + "reference": "f8d5d08c56de5cfd592b3340424a81733259a876" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/5220af2a7929aa35cf663d97c89ad3d50cf5fa3e", - "reference": "5220af2a7929aa35cf663d97c89ad3d50cf5fa3e", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/f8d5d08c56de5cfd592b3340424a81733259a876", + "reference": "f8d5d08c56de5cfd592b3340424a81733259a876", "shasum": "" }, "require": { "ext-tokenizer": "*", "php": ">=5.3.3" }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { "classmap": [ - "PHP/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "" - ], "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" + "email": "sebastian@phpunit.de" } ], "description": "Wrapper around PHP's tokenizer extension.", @@ -468,63 +570,60 @@ "keywords": [ "tokenizer" ], - "time": "2013-09-13 04:58:23" + "time": "2014-08-31 06:12:13" }, { "name": "phpunit/phpunit", - "version": "3.7.28", + "version": "4.4.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3b97c8492bcafbabe6b6fbd2ab35f2f04d932a8d" + "reference": "6a5e49a86ce5e33b8d0657abe145057fc513543a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3b97c8492bcafbabe6b6fbd2ab35f2f04d932a8d", - "reference": "3b97c8492bcafbabe6b6fbd2ab35f2f04d932a8d", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6a5e49a86ce5e33b8d0657abe145057fc513543a", + "reference": "6a5e49a86ce5e33b8d0657abe145057fc513543a", "shasum": "" }, "require": { "ext-dom": "*", + "ext-json": "*", "ext-pcre": "*", "ext-reflection": "*", "ext-spl": "*", "php": ">=5.3.3", - "phpunit/php-code-coverage": "~1.2.1", - "phpunit/php-file-iterator": ">=1.3.1", - "phpunit/php-text-template": ">=1.1.1", - "phpunit/php-timer": ">=1.0.4", - "phpunit/phpunit-mock-objects": "~1.2.0", + "phpunit/php-code-coverage": "~2.0", + "phpunit/php-file-iterator": "~1.3.2", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "~1.0.2", + "phpunit/phpunit-mock-objects": "~2.3", + "sebastian/comparator": "~1.0", + "sebastian/diff": "~1.1", + "sebastian/environment": "~1.1", + "sebastian/exporter": "~1.0", + "sebastian/global-state": "~1.0", + "sebastian/version": "~1.0", "symfony/yaml": "~2.0" }, - "require-dev": { - "pear-pear/pear": "1.9.4" - }, "suggest": { - "ext-json": "*", - "ext-simplexml": "*", - "ext-tokenizer": "*", - "phpunit/php-invoker": ">=1.1.0,<1.2.0" + "phpunit/php-invoker": "~1.1" }, "bin": [ - "composer/bin/phpunit" + "phpunit" ], "type": "library", "extra": { "branch-alias": { - "dev-master": "3.7.x-dev" + "dev-master": "4.4.x-dev" } }, "autoload": { "classmap": [ - "PHPUnit/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "", - "../../symfony/yaml/" - ], "license": [ "BSD-3-Clause" ], @@ -536,45 +635,51 @@ } ], "description": "The PHP Unit Testing framework.", - "homepage": "http://www.phpunit.de/", + "homepage": "https://phpunit.de/", "keywords": [ "phpunit", "testing", "xunit" ], - "time": "2013-10-17 07:27:40" + "time": "2014-12-28 07:57:05" }, { "name": "phpunit/phpunit-mock-objects", - "version": "1.2.3", + "version": "2.3.0", "source": { "type": "git", - "url": "git://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "1.2.3" + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "c63d2367247365f688544f0d500af90a11a44c65" }, "dist": { "type": "zip", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects/archive/1.2.3.zip", - "reference": "1.2.3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/c63d2367247365f688544f0d500af90a11a44c65", + "reference": "c63d2367247365f688544f0d500af90a11a44c65", "shasum": "" }, "require": { + "doctrine/instantiator": "~1.0,>=1.0.1", "php": ">=5.3.3", - "phpunit/php-text-template": ">=1.1.1@stable" + "phpunit/php-text-template": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.3" }, "suggest": { "ext-soap": "*" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + }, "autoload": { "classmap": [ - "PHPUnit/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "" - ], "license": [ "BSD-3-Clause" ], @@ -591,21 +696,338 @@ "mock", "xunit" ], - "time": "2013-01-13 10:24:48" + "time": "2014-10-03 05:12:11" + }, + { + "name": "sebastian/comparator", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "c484a80f97573ab934e37826dba0135a3301b26a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c484a80f97573ab934e37826dba0135a3301b26a", + "reference": "c484a80f97573ab934e37826dba0135a3301b26a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.1", + "sebastian/exporter": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2014-11-16 21:32:38" + }, + { + "name": "sebastian/diff", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "5843509fed39dee4b356a306401e9dd1a931fec7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/5843509fed39dee4b356a306401e9dd1a931fec7", + "reference": "5843509fed39dee4b356a306401e9dd1a931fec7", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "http://www.github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2014-08-15 10:29:00" + }, + { + "name": "sebastian/environment", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "6e6c71d918088c251b181ba8b3088af4ac336dd7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6e6c71d918088c251b181ba8b3088af4ac336dd7", + "reference": "6e6c71d918088c251b181ba8b3088af4ac336dd7", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2014-10-25 08:00:45" + }, + { + "name": "sebastian/exporter", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "c7d59948d6e82818e1bdff7cadb6c34710eb7dc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c7d59948d6e82818e1bdff7cadb6c34710eb7dc0", + "reference": "c7d59948d6e82818e1bdff7cadb6c34710eb7dc0", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2014-09-10 00:51:36" + }, + { + "name": "sebastian/global-state", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/c7428acdb62ece0a45e6306f1ae85e1c05b09c01", + "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2014-10-06 09:23:50" + }, + { + "name": "sebastian/version", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "a77d9123f8e809db3fbdea15038c27a95da4058b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/a77d9123f8e809db3fbdea15038c27a95da4058b", + "reference": "a77d9123f8e809db3fbdea15038c27a95da4058b", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2014-12-15 14:25:24" }, { "name": "symfony/yaml", - "version": "v2.4.1", + "version": "v2.6.1", "target-dir": "Symfony/Component/Yaml", "source": { "type": "git", "url": "https://github.com/symfony/Yaml.git", - "reference": "4e1a237fc48145fae114b96458d799746ad89aa0" + "reference": "3346fc090a3eb6b53d408db2903b241af51dcb20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/4e1a237fc48145fae114b96458d799746ad89aa0", - "reference": "4e1a237fc48145fae114b96458d799746ad89aa0", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/3346fc090a3eb6b53d408db2903b241af51dcb20", + "reference": "3346fc090a3eb6b53d408db2903b241af51dcb20", "shasum": "" }, "require": { @@ -614,7 +1036,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.6-dev" } }, "autoload": { @@ -627,31 +1049,27 @@ "MIT" ], "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, { "name": "Symfony Community", "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" } ], "description": "Symfony Yaml Component", "homepage": "http://symfony.com", - "time": "2013-12-28 08:12:03" + "time": "2014-12-02 20:19:20" } ], - "aliases": [ - - ], + "aliases": [], "minimum-stability": "stable", - "stability-flags": [ - - ], + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, "platform": { "php": ">=5.3.2" }, - "platform-dev": [ - - ] + "platform-dev": [] } diff --git a/doc/00-intro.md b/doc/00-intro.md index 7b62fee16..714b55be0 100644 --- a/doc/00-intro.md +++ b/doc/00-intro.md @@ -33,11 +33,13 @@ You decide to use [monolog](https://github.com/Seldaek/monolog). In order to add it to your project, all you need to do is create a `composer.json` file which describes the project's dependencies. - { - "require": { - "monolog/monolog": "1.2.*" - } +```json +{ + "require": { + "monolog/monolog": "1.2.*" } +} +``` We are simply stating that our project requires some `monolog/monolog` package, any version beginning with `1.2`. @@ -45,7 +47,7 @@ any version beginning with `1.2`. ## System Requirements Composer requires PHP 5.3.2+ to run. A few sensitive php settings and compile -flags are also required, but the installer will warn you about any +flags are also required, but when using the installer you will be warned about any incompatibilities. To install packages from sources instead of simple zip archives, you will need @@ -54,26 +56,40 @@ git, svn or hg depending on how the package is version-controlled. Composer is multi-platform and we strive to make it run equally well on Windows, Linux and OSX. -## Installation - *nix +## Installation - Linux / Unix / OSX ### Downloading the Composer Executable +There are in short, two ways to install Composer. Locally as part of your +project, or globally as a system wide executable. + #### Locally -To actually get Composer, we need to do two things. The first one is installing -Composer (again, this means downloading it into your project): +Installing Composer locally is a matter of just running the installer in your +project directory: + +```sh +curl -sS https://getcomposer.org/installer | php +``` + +> **Note:** If the above fails for some reason, you can download the installer +> with `php` instead: - $ curl -sS https://getcomposer.org/installer | php +```sh +php -r "readfile('https://getcomposer.org/installer');" | php +``` -This will just check a few PHP settings and then download `composer.phar` to -your working directory. This file is the Composer binary. It is a PHAR (PHP +The installer will just check a few PHP settings and then download `composer.phar` +to your working directory. This file is the Composer binary. It is a PHAR (PHP archive), which is an archive format for PHP which can be run on the command line, amongst other things. You can install Composer to a specific directory by using the `--install-dir` option and providing a target directory (it can be an absolute or relative path): - $ curl -sS https://getcomposer.org/installer | php -- --install-dir=bin +```sh +curl -sS https://getcomposer.org/installer | php -- --install-dir=bin +``` #### Globally @@ -83,25 +99,17 @@ executable and invoke it without `php`. You can run these commands to easily access `composer` from anywhere on your system: - $ curl -sS https://getcomposer.org/installer | php - $ mv composer.phar /usr/local/bin/composer +```sh +curl -sS https://getcomposer.org/installer | php +mv composer.phar /usr/local/bin/composer +``` > **Note:** If the above fails due to permissions, run the `mv` line > again with sudo. -Then, just run `composer` in order to run Composer instead of `php composer.phar`. - -#### Globally (on OSX via homebrew) +> **Note:** In OSX Yosemite the `/usr` directory does not exist by default. If you receive the error "/usr/local/bin/composer: No such file or directory" then you must create `/usr/local/bin/` manually before proceeding. -Composer is part of the homebrew-php project. - -1. Tap the homebrew-php repository into your brew installation if you haven't done - so yet: `brew tap josegonzalez/homebrew-php` -2. Run `brew install josegonzalez/php/composer`. -3. Use Composer with the `composer` command. - -> **Note:** If you receive an error saying PHP53 or higher is missing use this command to install php -> `brew install php53-intl` +Then, just run `composer` in order to run Composer instead of `php composer.phar`. ## Installation - Windows @@ -113,26 +121,33 @@ Download and run [Composer-Setup.exe](https://getcomposer.org/Composer-Setup.exe it will install the latest Composer version and set up your PATH so that you can just call `composer` from any directory in your command line. +> **Note:** Close your current terminal. Test usage with a new terminal: +> That is important since the PATH only gets loaded when the terminal starts. + ### Manual Installation Change to a directory on your `PATH` and run the install snippet to download composer.phar: - C:\Users\username>cd C:\bin - C:\bin>php -r "eval('?>'.file_get_contents('https://getcomposer.org/installer'));" +```sh +C:\Users\username>cd C:\bin +C:\bin>php -r "readfile('https://getcomposer.org/installer');" | php +``` -> **Note:** If the above fails due to file_get_contents, use the `http` url or enable php_openssl.dll in php.ini +> **Note:** If the above fails due to readfile, use the `http` url or enable php_openssl.dll in php.ini Create a new `composer.bat` file alongside `composer.phar`: - C:\bin>echo @php "%~dp0composer.phar" %*>composer.bat +```sh +C:\bin>echo @php "%~dp0composer.phar" %*>composer.bat +``` Close your current terminal. Test usage with a new terminal: - C:\Users\username>composer -V - Composer version 27d8904 - - C:\Users\username> +```sh +C:\Users\username>composer -V +Composer version 27d8904 +``` ## Using Composer @@ -142,12 +157,16 @@ don't have a `composer.json` file in the current directory please skip to the To resolve and download dependencies, run the `install` command: - $ php composer.phar install +```sh +php composer.phar install +``` If you did a global install and do not have the phar in that directory run this instead: - $ composer install +```sh +composer install +``` Following the [example above](#declaring-dependencies), this will download monolog into the `vendor/monolog/monolog` directory. @@ -159,7 +178,9 @@ capable of autoloading all of the classes in any of the libraries that it downloads. To use it, just add the following line to your code's bootstrap process: - require 'vendor/autoload.php'; +```php +require 'vendor/autoload.php'; +``` Woah! Now start using monolog! To keep learning more about Composer, keep reading the "Basic Usage" chapter. diff --git a/doc/01-basic-usage.md b/doc/01-basic-usage.md index 1aa3eee05..ef72f556c 100644 --- a/doc/01-basic-usage.md +++ b/doc/01-basic-usage.md @@ -1,23 +1,8 @@ # Basic usage -## Installation +## Installing -To install Composer, you just need to download the `composer.phar` executable. - - $ curl -sS https://getcomposer.org/installer | php - -For the details, see the [Introduction](00-intro.md) chapter. - -To check if Composer is working, just run the PHAR through `php`: - - $ php composer.phar - -This should give you a list of available commands. - -> **Note:** You can also perform the checks only without downloading Composer -> by using the `--check` option. For more information, just use `--help`. -> -> $ curl -sS https://getcomposer.org/installer | php -- --help +If you have not yet installed Composer, refer to the [Intro](00-intro.md) chapter. ## `composer.json`: Project Setup @@ -34,11 +19,13 @@ The first (and often only) thing you specify in `composer.json` is the `require` key. You're simply telling Composer which packages your project depends on. - { - "require": { - "monolog/monolog": "1.0.*" - } +```json +{ + "require": { + "monolog/monolog": "1.0.*" } +} +``` As you can see, `require` takes an object that maps **package names** (e.g. `monolog/monolog`) to **package versions** (e.g. `1.0.*`). @@ -64,17 +51,19 @@ means any version in the `1.0` development branch. It would match `1.0.0`, Version constraints can be specified in a few different ways. -Name | Example | Description --------------- | --------------------- | ----------- -Exact version | `1.0.2` | You can specify the exact version of a package. -Range | `>=1.0` `>=1.0,<2.0` `>=1.0,<1.1 | >=1.2` | By using comparison operators you can specify ranges of valid versions. Valid operators are `>`, `>=`, `<`, `<=`, `!=`.
You can define multiple ranges, separated by a comma, which will be treated as a **logical AND**. A pipe symbol `|` will be treated as a **logical OR**.
AND has higher precedence than OR. -Wildcard | `1.0.*` | You can specify a pattern with a `*` wildcard. `1.0.*` is the equivalent of `>=1.0,<1.1`. -Tilde Operator | `~1.2` | Very useful for projects that follow semantic versioning. `~1.2` is equivalent to `>=1.2,<2.0`. For more details, read the next section below. +Name | Example | Description +-------------- | ------------------------------------------------------------------------ | ----------- +Exact version | `1.0.2` | You can specify the exact version of a package. +Range | `>=1.0` `>=1.0 <2.0` >=1.0 <1.1 || >=1.2 | By using comparison operators you can specify ranges of valid versions. Valid operators are `>`, `>=`, `<`, `<=`, `!=`.
You can define multiple ranges. Ranges separated by a space ( ) or comma (`,`) will be treated as a **logical AND**. A double pipe (||) will be treated as a **logical OR**. AND has higher precedence than OR. +Hyphen Range | `1.0 - 2.0` | Inclusive set of versions. Partial versions on the right include are completed with a wildcard. For example `1.0 - 2.0` is equivalent to `>=1.0.0 <2.1` as the `2.0` becomes `2.0.*`. On the other hand `1.0.0 - 2.1.0` is equivalent to `>=1.0.0 <=2.1.0`. +Wildcard | `1.0.*` | You can specify a pattern with a `*` wildcard. `1.0.*` is the equivalent of `>=1.0 <1.1`. +Tilde Operator | `~1.2` | Very useful for projects that follow semantic versioning. `~1.2` is equivalent to `>=1.2 <2.0`. For more details, read the next section below. +Caret Operator | `^1.2.3` | Very useful for projects that follow semantic versioning. `^1.2.3` is equivalent to `>=1.2.3 <2.0`. For more details, read the next section below. -### Next Significant Release (Tilde Operator) +### Next Significant Release (Tilde and Caret Operators) The `~` operator is best explained by example: `~1.2` is equivalent to -`>=1.2,<2.0`, while `~1.2.3` is equivalent to `>=1.2.3,<1.3`. As you can see +`>=1.2 <2.0.0`, while `~1.2.3` is equivalent to `>=1.2.3 <1.3.0`. As you can see it is mostly useful for projects respecting [semantic versioning](http://semver.org/). A common usage would be to mark the minimum minor version you depend on, like `~1.2` (which allows anything up to, but not @@ -82,6 +71,21 @@ including, 2.0). Since in theory there should be no backwards compatibility breaks until 2.0, that works well. Another way of looking at it is that using `~` specifies a minimum version, but allows the last digit specified to go up. +The `^` operator behaves very similarly but it sticks closer to semantic +versioning, and will always allow non-breaking updates. For example `^1.2.3` +is equivalent to `>=1.2.3 <2.0.0` as none of the releases until 2.0 should +break backwards compatibility. For pre-1.0 versions it also acts with safety +in mind and treats `^0.3` as `>=0.3.0 <0.4.0` + +> **Note:** Though `2.0-beta.1` is strictly before `2.0`, a version constraint +> like `~1.2` would not install it. As said above `~1.2` only means the `.2` +> can change but the `1.` part is fixed. + +> **Note:** The `~` operator has an exception on its behavior for the major +> release number. This means for example that `~1` is the same as `~1.0` as +> it will not allow the major number to increase trying to keep backwards +> compatibility. + ### Stability By default only stable releases are taken into consideration. If you would like @@ -95,7 +99,9 @@ packages instead of doing per dependency you can also use the To fetch the defined dependencies into your local project, just run the `install` command of `composer.phar`. - $ php composer.phar install +```sh +php composer.phar install +``` This will find the latest version of `monolog/monolog` that matches the supplied version constraint and download it into the `vendor` directory. @@ -130,18 +136,25 @@ dependencies installed are still working even if your dependencies released many new versions since then. If no `composer.lock` file exists, Composer will read the dependencies and -versions from `composer.json` and create the lock file. +versions from `composer.json` and create the lock file after executing the `update` or the `install` +command. This means that if any of the dependencies get a new version, you won't get the updates automatically. To update to the new version, use `update` command. This will fetch the latest matching versions (according to your `composer.json` file) and also update the lock file with the new version. - $ php composer.phar update - +```sh +php composer.phar update +``` +> **Note:** Composer will display a Warning when executing an `install` command if + `composer.lock` and `composer.json` are not synchronized. + If you only want to install or update one dependency, you can whitelist them: - $ php composer.phar update monolog/monolog [...] +```sh +php composer.phar update monolog/monolog [...] +``` > **Note:** For libraries it is not necessarily recommended to commit the lock file, > see also: [Libraries - Lock file](02-libraries.md#lock-file). @@ -167,25 +180,31 @@ For libraries that specify autoload information, Composer generates a `vendor/autoload.php` file. You can simply include this file and you will get autoloading for free. - require 'vendor/autoload.php'; +```php +require 'vendor/autoload.php'; +``` This makes it really easy to use third party code. For example: If your project depends on monolog, you can just start using classes from it, and they will be autoloaded. - $log = new Monolog\Logger('name'); - $log->pushHandler(new Monolog\Handler\StreamHandler('app.log', Monolog\Logger::WARNING)); +```php +$log = new Monolog\Logger('name'); +$log->pushHandler(new Monolog\Handler\StreamHandler('app.log', Monolog\Logger::WARNING)); - $log->addWarning('Foo'); +$log->addWarning('Foo'); +``` You can even add your own code to the autoloader by adding an `autoload` field to `composer.json`. - { - "autoload": { - "psr-4": {"Acme\\": "src/"} - } +```json +{ + "autoload": { + "psr-4": {"Acme\\": "src/"} } +} +``` Composer will register a [PSR-4](http://www.php-fig.org/psr/psr-4/) autoloader for the `Acme` namespace. @@ -194,15 +213,17 @@ You define a mapping from namespaces to directories. The `src` directory would be in your project root, on the same level as `vendor` directory is. An example filename would be `src/Foo.php` containing an `Acme\Foo` class. -After adding the `autoload` field, you have to re-run `install` to re-generate +After adding the `autoload` field, you have to re-run `dump-autoload` to re-generate the `vendor/autoload.php` file. Including that file will also return the autoloader instance, so you can store the return value of the include call in a variable and add more namespaces. This can be useful for autoloading classes in a test suite, for example. - $loader = require 'vendor/autoload.php'; - $loader->add('Acme\\Test\\', __DIR__); +```php +$loader = require 'vendor/autoload.php'; +$loader->add('Acme\\Test\\', __DIR__); +``` In addition to PSR-4 autoloading, classmap is also supported. This allows classes to be autoloaded even if they do not conform to PSR-4. See the diff --git a/doc/02-libraries.md b/doc/02-libraries.md index 27428064f..913f996b7 100644 --- a/doc/02-libraries.md +++ b/doc/02-libraries.md @@ -12,12 +12,14 @@ libraries is that your project is a package without a name. In order to make that package installable you need to give it a name. You do this by adding a `name` to `composer.json`: - { - "name": "acme/hello-world", - "require": { - "monolog/monolog": "1.0.*" - } +```json +{ + "name": "acme/hello-world", + "require": { + "monolog/monolog": "1.0.*" } +} +``` In this case the project name is `acme/hello-world`, where `acme` is the vendor name. Supplying a vendor name is mandatory. @@ -33,8 +35,11 @@ installed on the system but are not actually installable by Composer. This includes PHP itself, PHP extensions and some system libraries. * `php` represents the PHP version of the user, allowing you to apply - constraints, e.g. `>=5.4.0`. To require a 64bit version of php, you can - require the `php-64bit` package. + constraints, e.g. `>=5.4.0`. To require a 64bit version of php, you can + require the `php-64bit` package. + +* `hhvm` represents the version of the HHVM runtime (aka HipHop Virtual + Machine) and allows you to apply a constraint, e.g., '>=2.3.3'. * `ext-` allows you to require PHP extensions (includes core extensions). Versioning can be quite inconsistent here, so it's often @@ -42,8 +47,8 @@ includes PHP itself, PHP extensions and some system libraries. package name is `ext-gd`. * `lib-` allows constraints to be made on versions of libraries used by - PHP. The following are available: `curl`, `iconv`, `libxml`, `openssl`, - `pcre`, `uuid`, `xsl`. + PHP. The following are available: `curl`, `iconv`, `icu`, `libxml`, + `openssl`, `pcre`, `uuid`, `xsl`. You can use `composer show --platform` to get a list of your locally available platform packages. @@ -59,9 +64,11 @@ version numbers are extracted from these. If you are creating packages by hand and really have to specify it explicitly, you can just add a `version` field: - { - "version": "1.0.0" - } +```json +{ + "version": "1.0.0" +} +``` > **Note:** You should avoid specifying the version field explicitly, because > for tags the value must match the tag name. @@ -70,17 +77,17 @@ you can just add a `version` field: For every tag that looks like a version, a package version of that tag will be created. It should match 'X.Y.Z' or 'vX.Y.Z', with an optional suffix -of `-patch`, `-alpha`, `-beta` or `-RC`. The suffixes can also be followed by -a number. +of `-patch` (`-p`), `-alpha` (`-a`), `-beta` (`-b`) or `-RC`. The suffixes +can also be followed by a number. Here are a few examples of valid tag names: - 1.0.0 - v1.0.0 - 1.10.5-RC1 - v4.4.4beta2 - v2.0.0-alpha - v2.0.4-p1 +- 1.0.0 +- v1.0.0 +- 1.10.5-RC1 +- v4.4.4-beta2 +- v2.0.0-alpha +- v2.0.4-p1 > **Note:** Even if your tag is prefixed with `v`, a [version constraint](01-basic-usage.md#package-versions) > in a `require` statement has to be specified without prefix @@ -98,9 +105,9 @@ like a version, it will be `dev-{branchname}`. `master` results in a Here are some examples of version branch names: - 1.x - 1.0 (equals 1.0.x) - 1.1.x +- 1.x +- 1.0 (equals 1.0.x) +- 1.1.x > **Note:** When you install a development version, it will be automatically > pulled from its `source`. See the [`install`](03-cli.md#install) command @@ -137,12 +144,14 @@ project locally. We will call it `acme/blog`. This blog will depend on accomplish this by creating a new `blog` directory somewhere, containing a `composer.json`: - { - "name": "acme/blog", - "require": { - "acme/hello-world": "dev-master" - } +```json +{ + "name": "acme/blog", + "require": { + "acme/hello-world": "dev-master" } +} +``` The name is not needed in this case, since we don't want to publish the blog as a library. It is added here to clarify which `composer.json` is being @@ -152,18 +161,20 @@ Now we need to tell the blog app where to find the `hello-world` dependency. We do this by adding a package repository specification to the blog's `composer.json`: - { - "name": "acme/blog", - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/username/hello-world" - } - ], - "require": { - "acme/hello-world": "dev-master" +```json +{ + "name": "acme/blog", + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/username/hello-world" } + ], + "require": { + "acme/hello-world": "dev-master" } +} +``` For more details on how package repositories work and what other types are available, see [Repositories](05-repositories.md). diff --git a/doc/03-cli.md b/doc/03-cli.md index 8d34fb49e..40478ee9b 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -1,4 +1,4 @@ -# Command-line interface +# Command-line interface / Commands You've already learned how to use the command-line interface to do some things. This chapter documents all the available commands. @@ -36,7 +36,9 @@ it a bit easier to do this. When you run the command it will interactively ask you to fill in the fields, while using some smart defaults. - $ php composer.phar init +```sh +php composer.phar init +``` ### Options @@ -54,7 +56,9 @@ while using some smart defaults. The `install` command reads the `composer.json` file from the current directory, resolves the dependencies, and installs them into `vendor`. - $ php composer.phar install +```sh +php composer.phar install +``` If there is a `composer.lock` file in the current directory, it will use the exact versions from there instead of resolving them. This ensures that @@ -76,11 +80,15 @@ resolution. servers and other use cases where you typically do not run updates of the vendors. It is also a way to circumvent problems with git if you do not have a proper setup. +* **--ignore-platform-reqs:** ignore `php`, `hhvm`, `lib-*` and `ext-*` + requirements and force the installation even if the local machine does not + fulfill these. * **--dry-run:** If you want to run through an installation without actually installing a package, you can use `--dry-run`. This will simulate the installation and show you what would happen. * **--dev:** Install packages listed in `require-dev` (this is the default behavior). -* **--no-dev:** Skip installing packages listed in `require-dev`. +* **--no-dev:** Skip installing packages listed in `require-dev`. The autoloader generation skips the `autoload-dev` rules. +* **--no-autoloader:** Skips autoloader generation. * **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--no-plugins:** Disables plugins. * **--no-progress:** Removes the progress display that can mess with some @@ -94,26 +102,36 @@ resolution. In order to get the latest versions of the dependencies and to update the `composer.lock` file, you should use the `update` command. - $ php composer.phar update +```sh +php composer.phar update +``` This will resolve all dependencies of the project and write the exact versions into `composer.lock`. If you just want to update a few packages and not all, you can list them as such: - $ php composer.phar update vendor/package vendor/package2 +```sh +php composer.phar update vendor/package vendor/package2 +``` You can also use wildcards to update a bunch of packages at once: - $ php composer.phar update vendor/* +```sh +php composer.phar update vendor/* +``` ### Options * **--prefer-source:** Install packages from `source` when available. * **--prefer-dist:** Install packages from `dist` when available. +* **--ignore-platform-reqs:** ignore `php`, `hhvm`, `lib-*` and `ext-*` + requirements and force the installation even if the local machine does not + fulfill these. * **--dry-run:** Simulate the command without actually doing anything. * **--dev:** Install packages listed in `require-dev` (this is the default behavior). -* **--no-dev:** Skip installing packages listed in `require-dev`. +* **--no-dev:** Skip installing packages listed in `require-dev`. The autoloader generation skips the `autoload-dev` rules. +* **--no-autoloader:** Skips autoloader generation. * **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--no-plugins:** Disables plugins. * **--no-progress:** Removes the progress display that can mess with some @@ -124,14 +142,18 @@ You can also use wildcards to update a bunch of packages at once: * **--lock:** Only updates the lock file hash to suppress warning about the lock file being out of date. * **--with-dependencies** Add also all dependencies of whitelisted packages to the whitelist. - So all packages with their dependencies are updated recursively. +* **--prefer-stable:** Prefer stable versions of dependencies. +* **--prefer-lowest:** Prefer lowest versions of dependencies. Useful for testing minimal + versions of requirements, generally used with `--prefer-stable`. ## require The `require` command adds new packages to the `composer.json` file from -the current directory. +the current directory. If no file exists one will be created on the fly. - $ php composer.phar require +```sh +php composer.phar require +``` After adding/changing the requirements, the modified requirements will be installed or updated. @@ -139,16 +161,47 @@ installed or updated. If you do not want to choose requirements interactively, you can just pass them to the command. - $ php composer.phar require vendor/package:2.* vendor/package2:dev-master +```sh +php composer.phar require vendor/package:2.* vendor/package2:dev-master +``` ### Options * **--prefer-source:** Install packages from `source` when available. * **--prefer-dist:** Install packages from `dist` when available. +* **--ignore-platform-reqs:** ignore `php`, `hhvm`, `lib-*` and `ext-*` + requirements and force the installation even if the local machine does not + fulfill these. * **--dev:** Add packages to `require-dev`. * **--no-update:** Disables the automatic update of the dependencies. * **--no-progress:** Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters. +* **--update-no-dev** Run the dependency update with the --no-dev option. +* **--update-with-dependencies** Also update dependencies of the newly + required packages. + +## remove + +The `remove` command removes packages from the `composer.json` file from +the current directory. + +```sh +php composer.phar remove vendor/package vendor/package2 +``` + +After removing the requirements, the modified requirements will be +uninstalled. + +### Options +* **--ignore-platform-reqs:** ignore `php`, `hhvm`, `lib-*` and `ext-*` + requirements and force the installation even if the local machine does not + fulfill these. +* **--dev:** Remove packages from `require-dev`. +* **--no-update:** Disables the automatic update of the dependencies. +* **--no-progress:** Removes the progress display that can mess with some + terminals or scripts which don't handle backspace characters. +* **--update-no-dev** Run the dependency update with the --no-dev option. +* **--update-with-dependencies** Also update dependencies of the removed packages. ## global @@ -160,13 +213,17 @@ This can be used to install CLI utilities globally and if you add `$COMPOSER_HOME/vendor/bin` to your `$PATH` environment variable. Here is an example: - $ php composer.phar global require fabpot/php-cs-fixer:dev-master +```sh +php composer.phar global require fabpot/php-cs-fixer:dev-master +``` Now the `php-cs-fixer` binary is available globally (assuming you adjusted your PATH). If you wish to update the binary later on you can just run a global update: - $ php composer.phar global update +```sh +php composer.phar global update +``` ## search @@ -174,7 +231,9 @@ The search command allows you to search through the current project's package repositories. Usually this will be just packagist. You simply pass it the terms you want to search for. - $ php composer.phar search monolog +```sh +php composer.phar search monolog +``` You can also search for more than one term by passing multiple arguments. @@ -186,32 +245,38 @@ You can also search for more than one term by passing multiple arguments. To list all of the available packages, you can use the `show` command. - $ php composer.phar show +```sh +php composer.phar show +``` If you want to see the details of a certain package, you can pass the package name. - $ php composer.phar show monolog/monolog +```sh +php composer.phar show monolog/monolog - name : monolog/monolog - versions : master-dev, 1.0.2, 1.0.1, 1.0.0, 1.0.0-RC1 - type : library - names : monolog/monolog - source : [git] http://github.com/Seldaek/monolog.git 3d4e60d0cbc4b888fe5ad223d77964428b1978da - dist : [zip] http://github.com/Seldaek/monolog/zipball/3d4e60d0cbc4b888fe5ad223d77964428b1978da 3d4e60d0cbc4b888fe5ad223d77964428b1978da - license : MIT +name : monolog/monolog +versions : master-dev, 1.0.2, 1.0.1, 1.0.0, 1.0.0-RC1 +type : library +names : monolog/monolog +source : [git] http://github.com/Seldaek/monolog.git 3d4e60d0cbc4b888fe5ad223d77964428b1978da +dist : [zip] http://github.com/Seldaek/monolog/zipball/3d4e60d0cbc4b888fe5ad223d77964428b1978da 3d4e60d0cbc4b888fe5ad223d77964428b1978da +license : MIT - autoload - psr-0 - Monolog : src/ +autoload +psr-0 +Monolog : src/ - requires - php >=5.3.0 +requires +php >=5.3.0 +``` You can even pass the package version, which will tell you the details of that specific version. - $ php composer.phar show monolog/monolog 1.0.2 +```sh +php composer.phar show monolog/monolog 1.0.2 +``` ### Options @@ -219,19 +284,30 @@ specific version. * **--platform (-p):** List only platform packages (php & extensions). * **--self (-s):** List the root package info. +## browse / home + +The `browse` (aliased to `home`) opens a package's repository URL or homepage +in your browser. + +### Options + +* **--homepage (-H):** Open the homepage instead of the repository URL. + ## depends The `depends` command tells you which other packages depend on a certain package. You can specify which link types (`require`, `require-dev`) should be included in the listing. By default both are used. - $ php composer.phar depends --link-type=require monolog/monolog +```sh +php composer.phar depends --link-type=require monolog/monolog - nrk/monolog-fluent - poc/poc - propel/propel - symfony/monolog-bridge - symfony/symfony +nrk/monolog-fluent +poc/poc +propel/propel +symfony/monolog-bridge +symfony/symfony +``` ### Options @@ -244,7 +320,13 @@ You should always run the `validate` command before you commit your `composer.json` file, and before you tag a release. It will check if your `composer.json` is valid. - $ php composer.phar validate +```sh +php composer.phar validate +``` + +### Options + +* **--no-check-all:** Whether or not composer do a complete validation. ## status @@ -252,31 +334,42 @@ If you often need to modify the code of your dependencies and they are installed from source, the `status` command allows you to check if you have local changes in any of them. - $ php composer.phar status +```sh +php composer.phar status +``` With the `--verbose` option you get some more information about what was changed: - $ php composer.phar status -v - You have changes in the following dependencies: - vendor/seld/jsonlint: - M README.mdown +```sh +php composer.phar status -v + +You have changes in the following dependencies: +vendor/seld/jsonlint: + M README.mdown +``` ## self-update To update composer itself to the latest version, just run the `self-update` command. It will replace your `composer.phar` with the latest version. - $ php composer.phar self-update +```sh +php composer.phar self-update +``` If you would like to instead update to a specific release simply specify it: - $ composer self-update 1.0.0-alpha7 +```sh +php composer.phar self-update 1.0.0-alpha7 +``` If you have installed composer for your entire system (see [global installation](00-intro.md#globally)), you may have to run the command with `root` privileges - $ sudo composer self-update +```sh +sudo composer self-update +``` ### Options @@ -288,7 +381,9 @@ you may have to run the command with `root` privileges The `config` command allows you to edit some basic composer settings in either the local composer.json file or the global config.json file. - $ php composer.phar config --list +```sh +php composer.phar config --list +``` ### Usage @@ -304,23 +399,27 @@ options. ### Options * **--global (-g):** Operate on the global config file located at -`$COMPOSER_HOME/config.json` by default. Without this option, this command -affects the local composer.json file or a file specified by `--file`. + `$COMPOSER_HOME/config.json` by default. Without this option, this command + affects the local composer.json file or a file specified by `--file`. * **--editor (-e):** Open the local composer.json file using in a text editor as -defined by the `EDITOR` env variable. With the `--global` option, this opens -the global config file. + defined by the `EDITOR` env variable. With the `--global` option, this opens + the global config file. * **--unset:** Remove the configuration element named by `setting-key`. * **--list (-l):** Show the list of current config variables. With the `--global` - option this lists the global configuration only. + option this lists the global configuration only. * **--file="..." (-f):** Operate on a specific file instead of composer.json. Note - that this cannot be used in conjunction with the `--global` option. + that this cannot be used in conjunction with the `--global` option. +* **--absolute:** Returns absolute paths when fetching *-dir config values + instead of relative. ### Modifying Repositories In addition to modifying the config section, the `config` command also supports making changes to the repositories section by using it the following way: - $ php composer.phar config repositories.foo vcs http://github.com/foo/bar +```sh +php composer.phar config repositories.foo vcs http://github.com/foo/bar +``` ## create-project @@ -341,7 +440,9 @@ provide a version as third argument, otherwise the latest version is used. If the directory does not currently exist, it will be created during installation. - php composer.phar create-project doctrine/orm path 2.2.* +```sh +php composer.phar create-project doctrine/orm path 2.2.* +``` It is also possible to run the command without params in a directory with an existing `composer.json` file to bootstrap a project. @@ -366,6 +467,9 @@ By default the command checks for the packages on packagist.org. * **--keep-vcs:** Skip the deletion of the VCS metadata for the created project. This is mostly useful if you run the command in non-interactive mode. +* **--ignore-platform-reqs:** ignore `php`, `hhvm`, `lib-*` and `ext-*` + requirements and force the installation even if the local machine does not + fulfill these. ## dump-autoload @@ -385,12 +489,22 @@ performance. * **--optimize (-o):** Convert PSR-0/4 autoloading to classmap to get a faster autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default. +* **--no-dev:** Disables autoload-dev rules. + +## clear-cache + +Deletes all content from Composer's cache directories. ## licenses Lists the name, version and license of every package installed. Use `--format=json` to get machine readable output. +### Options + +* **--no-dev:** Remove dev dependencies from the output +* **--format:** Format of the output: text or json (default: "text") + ## run-script To run [scripts](articles/scripts.md) manually you can use this command, @@ -402,7 +516,9 @@ If you think you found a bug, or something is behaving strangely, you might want to run the `diagnose` command to perform automated checks for many common problems. - $ php composer.phar diagnose +```sh +php composer.phar diagnose +``` ## archive @@ -410,7 +526,9 @@ This command is used to generate a zip/tar archive for a given package in a given version. It can also be used to archive your entire project without excluded/ignored files. - $ php composer.phar archive vendor/package 2.0.21 --format=zip +```sh +php composer.phar archive vendor/package 2.0.21 --format=zip +``` ### Options @@ -422,7 +540,9 @@ excluded/ignored files. To get more information about a certain command, just use `help`. - $ php composer.phar help install +```sh +php composer.phar help install +``` ## Environment variables @@ -438,7 +558,9 @@ By setting the `COMPOSER` env variable it is possible to set the filename of For example: - $ COMPOSER=composer-other.json php composer.phar install +```sh +COMPOSER=composer-other.json php composer.phar install +``` ### COMPOSER_ROOT_VERSION diff --git a/doc/04-schema.md b/doc/04-schema.md index 96641e8fe..e422d5c38 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -1,4 +1,4 @@ -# composer.json +# The composer.json Schema This chapter will explain all of the fields available in `composer.json`. @@ -54,19 +54,20 @@ The version of the package. In most cases this is not required and should be omitted (see below). This must follow the format of `X.Y.Z` or `vX.Y.Z` with an optional suffix -of `-dev`, `-patch`, `-alpha`, `-beta` or `-RC`. The patch, alpha, beta and -RC suffixes can also be followed by a number. +of `-dev`, `-patch` (`-p`), `-alpha` (`-a`), `-beta` (`-b`) or `-RC`. +The patch, alpha, beta and RC suffixes can also be followed by a number. Examples: - 1.0.0 - 1.0.2 - 1.1.0 - 0.2.5 - 1.0.0-dev - 1.0.0-alpha3 - 1.0.0-beta2 - 1.0.0-RC5 +- 1.0.0 +- 1.0.2 +- 1.1.0 +- 0.2.5 +- 1.0.0-dev +- 1.0.0-alpha3 +- 1.0.0-beta2 +- 1.0.0-RC5 +- v2.0.4-p1 Optional if the package repository can infer the version from somewhere, such as the VCS tag name in the VCS repository. In that case it is also recommended @@ -113,11 +114,11 @@ searching and filtering. Examples: - logging - events - database - redis - templating +- logging +- events +- database +- redis +- templating Optional. @@ -141,19 +142,19 @@ The license of the package. This can be either a string or an array of strings. The recommended notation for the most common licenses is (alphabetical): - Apache-2.0 - BSD-2-Clause - BSD-3-Clause - BSD-4-Clause - GPL-2.0 - GPL-2.0+ - GPL-3.0 - GPL-3.0+ - LGPL-2.1 - LGPL-2.1+ - LGPL-3.0 - LGPL-3.0+ - MIT +- Apache-2.0 +- BSD-2-Clause +- BSD-3-Clause +- BSD-4-Clause +- GPL-2.0 +- GPL-2.0+ +- GPL-3.0 +- GPL-3.0+ +- LGPL-2.1 +- LGPL-2.1+ +- LGPL-3.0 +- LGPL-3.0+ +- MIT Optional, but it is highly recommended to supply this. More identifiers are listed at the [SPDX Open Source License Registry](http://www.spdx.org/licenses/). @@ -162,28 +163,33 @@ For closed-source software, you may use `"proprietary"` as the license identifie An Example: - { - "license": "MIT" - } - +```json +{ + "license": "MIT" +} +``` For a package, when there is a choice between licenses ("disjunctive license"), multiple can be specified as array. An Example for disjunctive licenses: - { - "license": [ - "LGPL-2.1", - "GPL-3.0+" - ] - } +```json +{ + "license": [ + "LGPL-2.1", + "GPL-3.0+" + ] +} +``` Alternatively they can be separated with "or" and enclosed in parenthesis; - { - "license": "(LGPL-2.1 or GPL-3.0+)" - } +```json +{ + "license": "(LGPL-2.1 or GPL-3.0+)" +} +``` Similarly when multiple licenses need to be applied ("conjunctive license"), they should be separated with "and" and enclosed in parenthesis. @@ -201,22 +207,24 @@ Each author object can have following properties: An example: - { - "authors": [ - { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de", - "role": "Developer" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be", - "role": "Developer" - } - ] - } +```json +{ + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de", + "role": "Developer" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be", + "role": "Developer" + } + ] +} +``` Optional, but highly recommended. @@ -235,12 +243,14 @@ Support information includes the following: An example: - { - "support": { - "email": "support@example.org", - "irc": "irc://irc.freenode.org/composer" - } +```json +{ + "support": { + "email": "support@example.org", + "irc": "irc://irc.freenode.org/composer" } +} +``` Optional. @@ -251,11 +261,13 @@ All of the following take an object which maps package names to Example: - { - "require": { - "monolog/monolog": "1.0.*" - } +```json +{ + "require": { + "monolog/monolog": "1.0.*" } +} +``` All links are optional fields. @@ -267,24 +279,28 @@ allow unstable packages of a dependency for example. Example: - { - "require": { - "monolog/monolog": "1.0.*@beta", - "acme/foo": "@dev" - } +```json +{ + "require": { + "monolog/monolog": "1.0.*@beta", + "acme/foo": "@dev" } +} +``` If one of your dependencies has a dependency on an unstable package you need to explicitly require it as well, along with its sufficient stability flag. Example: - { - "require": { - "doctrine/doctrine-fixtures-bundle": "dev-master", - "doctrine/data-fixtures": "@dev" - } +```json +{ + "require": { + "doctrine/doctrine-fixtures-bundle": "dev-master", + "doctrine/data-fixtures": "@dev" } +} +``` `require` and `require-dev` additionally support explicit references (i.e. commit) for dev versions to make sure they are locked to a given state, even @@ -293,12 +309,14 @@ and append the reference with `#`. Example: - { - "require": { - "monolog/monolog": "dev-master#2eb0c0978d290a1c45346a1955188929cb4e5db7", - "acme/foo": "1.0.x-dev#abc123" - } +```json +{ + "require": { + "monolog/monolog": "dev-master#2eb0c0978d290a1c45346a1955188929cb4e5db7", + "acme/foo": "1.0.x-dev#abc123" } +} +``` > **Note:** While this is convenient at times, it should not be how you use > packages in the long term because it comes with a technical limitation. The @@ -328,10 +346,10 @@ dependencies from being installed. Lists packages that conflict with this version of this package. They will not be allowed to be installed together with your package. -Note that when specifying ranges like `<1.0, >= 1.1` in a `conflict` link, +Note that when specifying ranges like `<1.0 >=1.1` in a `conflict` link, this will state a conflict with all versions that are less than 1.0 *and* equal or newer than 1.1 at the same time, which is probably not what you want. You -probably want to go for `<1.0 | >= 1.1` in this case. +probably want to go for `<1.0 | >=1.1` in this case. #### replace @@ -358,7 +376,7 @@ useful for common interfaces. A package could depend on some virtual `logger` package, any library that implements this logger interface would simply list it in `provide`. -### suggest +#### suggest Suggested packages that can enhance or work well with this package. These are just informational and are displayed after the package is installed, to give @@ -370,11 +388,13 @@ and not version constraints. Example: - { - "suggest": { - "monolog/monolog": "Allows more advanced logging of the application flow" - } +```json +{ + "suggest": { + "monolog/monolog": "Allows more advanced logging of the application flow" } +} +``` ### autoload @@ -403,32 +423,38 @@ key => value array which may be found in the generated file Example: - { - "autoload": { - "psr-4": { - "Monolog\\": "src/", - "Vendor\\Namespace\\": "", - } +```json +{ + "autoload": { + "psr-4": { + "Monolog\\": "src/", + "Vendor\\Namespace\\": "" } } +} +``` If you need to search for a same prefix in multiple directories, you can specify them as an array as such: - { - "autoload": { - "psr-4": { "Monolog\\": ["src/", "lib/"] } - } +```json +{ + "autoload": { + "psr-4": { "Monolog\\": ["src/", "lib/"] } } +} +``` If you want to have a fallback directory where any namespace will be looked for, you can use an empty prefix like: - { - "autoload": { - "psr-4": { "": "src/" } - } +```json +{ + "autoload": { + "psr-4": { "": "src/" } } +} +``` #### PSR-0 @@ -444,44 +470,52 @@ array which may be found in the generated file `vendor/composer/autoload_namespa Example: - { - "autoload": { - "psr-0": { - "Monolog\\": "src/", - "Vendor\\Namespace\\": "src/", - "Vendor_Namespace_": "src/" - } +```json +{ + "autoload": { + "psr-0": { + "Monolog\\": "src/", + "Vendor\\Namespace\\": "src/", + "Vendor_Namespace_": "src/" } } +} +``` If you need to search for a same prefix in multiple directories, you can specify them as an array as such: - { - "autoload": { - "psr-0": { "Monolog\\": ["src/", "lib/"] } - } +```json +{ + "autoload": { + "psr-0": { "Monolog\\": ["src/", "lib/"] } } +} +``` The PSR-0 style is not limited to namespace declarations only but may be specified right down to the class level. This can be useful for libraries with only one class in the global namespace. If the php source file is also located in the root of the package, for example, it may be declared like this: - { - "autoload": { - "psr-0": { "UniqueGlobalClass": "" } - } +```json +{ + "autoload": { + "psr-0": { "UniqueGlobalClass": "" } } +} +``` If you want to have a fallback directory where any namespace can be, you can use an empty prefix like: - { - "autoload": { - "psr-0": { "": "src/" } - } +```json +{ + "autoload": { + "psr-0": { "": "src/" } } +} +``` #### Classmap @@ -496,11 +530,13 @@ to search for classes. Example: - { - "autoload": { - "classmap": ["src/", "lib/", "Something.php"] - } +```json +{ + "autoload": { + "classmap": ["src/", "lib/", "Something.php"] } +} +``` #### Files @@ -510,11 +546,37 @@ that cannot be autoloaded by PHP. Example: - { - "autoload": { - "files": ["src/MyLibrary/functions.php"] - } +```json +{ + "autoload": { + "files": ["src/MyLibrary/functions.php"] } +} +``` + +### autoload-dev (root-only) + +This section allows to define autoload rules for development purposes. + +Classes needed to run the test suite should not be included in the main autoload +rules to avoid polluting the autoloader in production and when other people use +your package as a dependency. + +Therefore, it is a good idea to rely on a dedicated path for your unit tests +and to add it within the autoload-dev section. + +Example: + +```json +{ + "autoload": { + "psr-4": { "MyLibrary\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "MyLibrary\\Tests\\": "tests/" } + } +} +``` ### include-path @@ -526,9 +588,11 @@ A list of paths which should get appended to PHP's `include_path`. Example: - { - "include-path": ["lib/"] - } +```json +{ + "include-path": ["lib/"] +} +``` Optional. @@ -552,12 +616,14 @@ it from `vendor/symfony/yaml`. To do that, `autoload` and `target-dir` are defined as follows: - { - "autoload": { - "psr-0": { "Symfony\\Component\\Yaml\\": "" } - }, - "target-dir": "Symfony/Component/Yaml" - } +```json +{ + "autoload": { + "psr-0": { "Symfony\\Component\\Yaml\\": "" } + }, + "target-dir": "Symfony/Component/Yaml" +} +``` Optional. @@ -615,47 +681,49 @@ For more information on any of these, see [Repositories](05-repositories.md). Example: - { - "repositories": [ - { - "type": "composer", - "url": "http://packages.example.com" - }, - { - "type": "composer", - "url": "https://packages.example.com", - "options": { - "ssl": { - "verify_peer": "true" - } +```json +{ + "repositories": [ + { + "type": "composer", + "url": "http://packages.example.com" + }, + { + "type": "composer", + "url": "https://packages.example.com", + "options": { + "ssl": { + "verify_peer": "true" } - }, - { - "type": "vcs", - "url": "https://github.com/Seldaek/monolog" - }, - { - "type": "pear", - "url": "http://pear2.php.net" - }, - { - "type": "package", - "package": { - "name": "smarty/smarty", - "version": "3.1.7", - "dist": { - "url": "http://www.smarty.net/files/Smarty-3.1.7.zip", - "type": "zip" - }, - "source": { - "url": "http://smarty-php.googlecode.com/svn/", - "type": "svn", - "reference": "tags/Smarty_3_1_7/distribution/" - } + } + }, + { + "type": "vcs", + "url": "https://github.com/Seldaek/monolog" + }, + { + "type": "pear", + "url": "http://pear2.php.net" + }, + { + "type": "package", + "package": { + "name": "smarty/smarty", + "version": "3.1.7", + "dist": { + "url": "http://www.smarty.net/files/Smarty-3.1.7.zip", + "type": "zip" + }, + "source": { + "url": "http://smarty-php.googlecode.com/svn/", + "type": "svn", + "reference": "tags/Smarty_3_1_7/distribution/" } } - ] - } + } + ] +} +``` > **Note:** Order is significant here. When looking for a package, Composer will look from the first to the last repository, and pick the first match. @@ -676,21 +744,29 @@ The following options are supported: * **preferred-install:** Defaults to `auto` and can be any of `source`, `dist` or `auto`. This option allows you to set the install method Composer will prefer to use. -* **github-protocols:** Defaults to `["git", "https"]`. A list of protocols to +* **store-auths:** What to do after prompting for authentication, one of: + `true` (always store), `false` (do not store) and `"prompt"` (ask every + time), defaults to `"prompt"`. +* **github-protocols:** Defaults to `["git", "https", "ssh"]`. A list of protocols to use when cloning from github.com, in priority order. You can reconfigure it to - prioritize the https protocol if you are behind a proxy or have somehow bad - performances with the git protocol. + for example prioritize the https protocol if you are behind a proxy or have somehow + bad performances with the git protocol. * **github-oauth:** A list of domain names and oauth keys. For example using `{"github.com": "oauthtoken"}` as the value of this option will use `oauthtoken` to access private repositories on github and to circumvent the low IP-based rate limiting of their API. - [Read more](articles/troubleshooting.md#api-rate-limit-and-two-factor-authentication) - on how to get an oauth token for GitHub. + [Read more](articles/troubleshooting.md#api-rate-limit-and-oauth-tokens) + on how to get an OAuth token for GitHub. +* **http-basic:** A list of domain names and username/passwords to authenticate + against them. For example using + `{"example.org": {"username": "alice", "password": "foo"}` as the value of this + option will let composer authenticate against example.org. * **vendor-dir:** Defaults to `vendor`. You can install dependencies into a - different directory if you want to. + different directory if you want to. `$HOME` and `~` will be replaced by your + home directory's path in vendor-dir and all `*-dir` options below. * **bin-dir:** Defaults to `vendor/bin`. If a project includes binaries, they will be symlinked into this directory. -* **cache-dir:** Defaults to `$home/cache` on unix systems and +* **cache-dir:** Defaults to `$COMPOSER_HOME/cache` on unix systems and `C:\Users\\AppData\Local\Composer` on Windows. Stores all the caches used by composer. See also [COMPOSER_HOME](03-cli.md#composer-home). * **cache-files-dir:** Defaults to `$cache-dir/files`. Stores the zip archives @@ -714,8 +790,14 @@ The following options are supported: the generated Composer autoloader. When null a random one will be generated. * **optimize-autoloader** Defaults to `false`. Always optimize when dumping the autoloader. +* **classmap-authoritative:** Defaults to `false`. If true, the composer + autoloader will not scan the filesystem for classes that are not found in + the class map. Implies 'optimize-autoloader'. * **github-domains:** Defaults to `["github.com"]`. A list of domains to use in github mode. This is used for GitHub Enterprise setups. +* **github-expose-hostname:** Defaults to `true`. If set to false, the OAuth + tokens created to access the github API will have a date instead of the + machine hostname. * **notify-on-install:** Defaults to `true`. Composer allows repositories to define a notification URL, so that they get notified whenever a package from that repository is installed. This option allows you to disable that behaviour. @@ -727,11 +809,18 @@ The following options are supported: Example: - { - "config": { - "bin-dir": "bin" - } +```json +{ + "config": { + "bin-dir": "bin" } +} +``` + +> **Note:** Authentication-related config options like `http-basic` and +> `github-oauth` can also be specified inside a `auth.json` file that goes +> besides your `composer.json`. That way you can gitignore it and every +> developer can place their own credentials in there. ### scripts (root-only) @@ -747,7 +836,9 @@ Arbitrary extra data for consumption by `scripts`. This can be virtually anything. To access it from within a script event handler, you can do: - $extra = $event->getComposer()->getPackage()->getExtra(); +```php +$extra = $event->getComposer()->getPackage()->getExtra(); +``` Optional. @@ -774,11 +865,13 @@ The following options are supported: Example: - { - "archive": { - "exclude": ["/foo/bar", "baz", "/*.test", "!/foo/bar/baz"] - } +```json +{ + "archive": { + "exclude": ["/foo/bar", "baz", "/*.test", "!/foo/bar/baz"] } +} +``` The example will include `/dir/foo/bar/file`, `/foo/bar/baz`, `/file.php`, `/foo/my.test` but it will exclude `/foo/bar/any`, `/foo/baz`, and `/my.test`. diff --git a/doc/05-repositories.md b/doc/05-repositories.md index ae947ea49..80d15c561 100644 --- a/doc/05-repositories.md +++ b/doc/05-repositories.md @@ -66,16 +66,18 @@ repository URL would be `example.org`. The only required field is `packages`. The JSON structure is as follows: - { - "packages": { - "vendor/package-name": { - "dev-master": { @composer.json }, - "1.0.x-dev": { @composer.json }, - "0.0.1": { @composer.json }, - "1.0.0": { @composer.json } - } +```json +{ + "packages": { + "vendor/package-name": { + "dev-master": { @composer.json }, + "1.0.x-dev": { @composer.json }, + "0.0.1": { @composer.json }, + "1.0.0": { @composer.json } } } +} +``` The `@composer.json` marker would be the contents of the `composer.json` from that package version including as a minimum: @@ -86,14 +88,16 @@ that package version including as a minimum: Here is a minimal package definition: - { - "name": "smarty/smarty", - "version": "3.1.7", - "dist": { - "url": "http://www.smarty.net/files/Smarty-3.1.7.zip", - "type": "zip" - } +```json +{ + "name": "smarty/smarty", + "version": "3.1.7", + "dist": { + "url": "http://www.smarty.net/files/Smarty-3.1.7.zip", + "type": "zip" } +} +``` It may include any of the other fields specified in the [schema](04-schema.md). @@ -105,19 +109,23 @@ every time a user installs a package. The URL can be either an absolute path An example value: - { - "notify-batch": "/downloads/" - } +```json +{ + "notify-batch": "/downloads/" +} +``` For `example.org/packages.json` containing a `monolog/monolog` package, this would send a `POST` request to `example.org/downloads/` with following JSON request body: - { - "downloads": [ - {"name": "monolog/monolog", "version": "1.2.1.0"}, - ] - } +```json +{ + "downloads": [ + {"name": "monolog/monolog", "version": "1.2.1.0"} + ] +} +``` The version field will contain the normalized representation of the version number. @@ -132,19 +140,21 @@ files. An example: - { - "includes": { - "packages-2011.json": { - "sha1": "525a85fb37edd1ad71040d429928c2c0edec9d17" - }, - "packages-2012-01.json": { - "sha1": "897cde726f8a3918faf27c803b336da223d400dd" - }, - "packages-2012-02.json": { - "sha1": "26f911ad717da26bbcac3f8f435280d13917efa5" - } +```json +{ + "includes": { + "packages-2011.json": { + "sha1": "525a85fb37edd1ad71040d429928c2c0edec9d17" + }, + "packages-2012-01.json": { + "sha1": "897cde726f8a3918faf27c803b336da223d400dd" + }, + "packages-2012-02.json": { + "sha1": "26f911ad717da26bbcac3f8f435280d13917efa5" } } +} +``` The SHA-1 sum of the file allows it to be cached and only re-requested if the hash changed. @@ -164,35 +174,39 @@ is an absolute path from the repository root. An example: - { - "provider-includes": { - "providers-a.json": { - "sha256": "f5b4bc0b354108ef08614e569c1ed01a2782e67641744864a74e788982886f4c" - }, - "providers-b.json": { - "sha256": "b38372163fac0573053536f5b8ef11b86f804ea8b016d239e706191203f6efac" - } +```json +{ + "provider-includes": { + "providers-a.json": { + "sha256": "f5b4bc0b354108ef08614e569c1ed01a2782e67641744864a74e788982886f4c" }, - "providers-url": "/p/%package%$%hash%.json" - } + "providers-b.json": { + "sha256": "b38372163fac0573053536f5b8ef11b86f804ea8b016d239e706191203f6efac" + } + }, + "providers-url": "/p/%package%$%hash%.json" +} +``` Those files contain lists of package names and hashes to verify the file integrity, for example: - { - "providers": { - "acme/foo": { - "sha256": "38968de1305c2e17f4de33aea164515bc787c42c7e2d6e25948539a14268bb82" - }, - "acme/bar": { - "sha256": "4dd24c930bd6e1103251306d6336ac813b563a220d9ca14f4743c032fb047233" - } +```json +{ + "providers": { + "acme/foo": { + "sha256": "38968de1305c2e17f4de33aea164515bc787c42c7e2d6e25948539a14268bb82" + }, + "acme/bar": { + "sha256": "4dd24c930bd6e1103251306d6336ac813b563a220d9ca14f4743c032fb047233" } } +} +``` The file above declares that acme/foo and acme/bar can be found in this repository, by loading the file referenced by `providers-url`, replacing -`%name%` by the package name and `%hash%` by the sha256 field. Those files +`%package%` by the package name and `%hash%` by the sha256 field. Those files themselves just contain package definitions as described [above](#packages). This field is optional. You probably don't need it for your own custom @@ -225,17 +239,19 @@ point to your custom branch. For version constraint naming conventions see Example assuming you patched monolog to fix a bug in the `bugfix` branch: - { - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/igorw/monolog" - } - ], - "require": { - "monolog/monolog": "dev-bugfix" +```json +{ + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/igorw/monolog" } + ], + "require": { + "monolog/monolog": "dev-bugfix" } +} +``` When you run `php composer.phar update`, you should get your modified version of `monolog/monolog` instead of the one from packagist. @@ -256,17 +272,19 @@ For more information [see the aliases article](articles/aliases.md). Exactly the same solution allows you to work with your private repositories at GitHub and BitBucket: - { - "require": { - "vendor/my-private-repo": "dev-master" - }, - "repositories": [ - { - "type": "vcs", - "url": "git@bitbucket.org:vendor/my-private-repo.git" - } - ] - } +```json +{ + "require": { + "vendor/my-private-repo": "dev-master" + }, + "repositories": [ + { + "type": "vcs", + "url": "git@bitbucket.org:vendor/my-private-repo.git" + } + ] +} +``` The only requirement is the installation of SSH keys for a git client. @@ -292,6 +310,11 @@ The VCS driver to be used is detected automatically based on the URL. However, should you need to specify one for whatever reason, you can use `git`, `svn` or `hg` as the repository type instead of `vcs`. +If you set the `no-api` key to `true` on a github repository it will clone the +repository as it would with any other git repository instead of using the +GitHub API. But unlike using the `git` driver directly, composer will still +attempt to use github's zip files. + #### Subversion Options Since Subversion has no native concept of branches and tags, Composer assumes @@ -300,17 +323,19 @@ by default that code is located in `$url/trunk`, `$url/branches` and values. For example if you used capitalized names you could configure the repository like this: - { - "repositories": [ - { - "type": "vcs", - "url": "http://svn.example.org/projectA/", - "trunk-path": "Trunk", - "branches-path": "Branches", - "tags-path": "Tags" - } - ] - } +```json +{ + "repositories": [ + { + "type": "vcs", + "url": "http://svn.example.org/projectA/", + "trunk-path": "Trunk", + "branches-path": "Branches", + "tags-path": "Tags" + } + ] +} +``` If you have no branches or tags directory you can disable them entirely by setting the `branches-path` or `tags-path` to `false`. @@ -320,6 +345,37 @@ If the package is in a sub-directory, e.g. `/trunk/foo/bar/composer.json` and setting the `"package-path"` option to the sub-directory, in this example it would be `"package-path": "foo/bar/"`. +If you have a private Subversion repository you can save credentials in the +http-basic section of your config (See [Schema](04-schema.md)): + +```json +{ + "http-basic": { + "svn.example.org": { + "username": "username", + "password": "password" + } + } +} +``` + +If your Subversion client is configured to store credentials by default these +credentials will be saved for the current user and existing saved credentials +for this server will be overwritten. To change this behavior by setting the +`"svn-cache-credentials"` option in your repository configuration: + +```json +{ + "repositories": [ + { + "type": "vcs", + "url": "http://svn.example.org/projectA/", + "svn-cache-credentials": false + } + ] +} +``` + ### PEAR It is possible to install packages from any PEAR channel by using the `pear` @@ -328,18 +384,20 @@ avoid conflicts. All packages are also aliased with prefix `pear-{channelAlias}/ Example using `pear2.php.net`: - { - "repositories": [ - { - "type": "pear", - "url": "http://pear2.php.net" - } - ], - "require": { - "pear-pear2.php.net/PEAR2_Text_Markdown": "*", - "pear-pear2/PEAR2_HTTP_Request": "*" +```json +{ + "repositories": [ + { + "type": "pear", + "url": "http://pear2.php.net" } + ], + "require": { + "pear-pear2.php.net/PEAR2_Text_Markdown": "*", + "pear-pear2/PEAR2_HTTP_Request": "*" } +} +``` In this case the short name of the channel is `pear2`, so the `PEAR2_HTTP_Request` package name becomes `pear-pear2/PEAR2_HTTP_Request`. @@ -382,23 +440,25 @@ To illustrate, the following example would get the `BasePackage`, `TopLevelPackage1`, and `TopLevelPackage2` packages from your PEAR repository and `IntermediatePackage` from a Github repository: - { - "repositories": [ - { - "type": "git", - "url": "https://github.com/foobar/intermediate.git" - }, - { - "type": "pear", - "url": "http://pear.foobar.repo", - "vendor-alias": "foobar" - } - ], - "require": { - "foobar/TopLevelPackage1": "*", - "foobar/TopLevelPackage2": "*" +```json +{ + "repositories": [ + { + "type": "git", + "url": "https://github.com/foobar/intermediate.git" + }, + { + "type": "pear", + "url": "http://pear.foobar.repo", + "vendor-alias": "foobar" } + ], + "require": { + "foobar/TopLevelPackage1": "*", + "foobar/TopLevelPackage2": "*" } +} +``` ### Package @@ -413,32 +473,34 @@ minimum required fields are `name`, `version`, and either of `dist` or Here is an example for the smarty template engine: - { - "repositories": [ - { - "type": "package", - "package": { - "name": "smarty/smarty", - "version": "3.1.7", - "dist": { - "url": "http://www.smarty.net/files/Smarty-3.1.7.zip", - "type": "zip" - }, - "source": { - "url": "http://smarty-php.googlecode.com/svn/", - "type": "svn", - "reference": "tags/Smarty_3_1_7/distribution/" - }, - "autoload": { - "classmap": ["libs/"] - } +```json +{ + "repositories": [ + { + "type": "package", + "package": { + "name": "smarty/smarty", + "version": "3.1.7", + "dist": { + "url": "http://www.smarty.net/files/Smarty-3.1.7.zip", + "type": "zip" + }, + "source": { + "url": "http://smarty-php.googlecode.com/svn/", + "type": "svn", + "reference": "tags/Smarty_3_1_7/distribution/" + }, + "autoload": { + "classmap": ["libs/"] } } - ], - "require": { - "smarty/smarty": "3.1.*" } + ], + "require": { + "smarty/smarty": "3.1.*" } +} +``` Typically you would leave the source part off, as you don't really need it. @@ -463,7 +525,7 @@ there are some use cases for hosting your own repository. might want to keep them separate to packagist. An example of this would be wordpress plugins. -For hosting your own packages, a native `composer` type of repository is +For hosting your own packages, a native `composer` type of repository is recommended, which provides the best performance. There are a few tools that can help you create a `composer` repository. @@ -507,25 +569,30 @@ of the times they are private. To simplify maintenance, one can simply use a repository of type `artifact` with a folder containing ZIP archives of those private packages: - { - "repositories": [ - { - "type": "artifact", - "url": "path/to/directory/with/zips/" - } - ], - "require": { - "private-vendor-one/core": "15.6.2", - "private-vendor-two/connectivity": "*", - "acme-corp/parser": "10.3.5" +```json +{ + "repositories": [ + { + "type": "artifact", + "url": "path/to/directory/with/zips/" } + ], + "require": { + "private-vendor-one/core": "15.6.2", + "private-vendor-two/connectivity": "*", + "acme-corp/parser": "10.3.5" } +} +``` Each zip artifact is just a ZIP archive with `composer.json` in root folder: - $ unzip -l acme-corp-parser-10.3.5.zip - composer.json - ... +```sh +unzip -l acme-corp-parser-10.3.5.zip + +composer.json +... +``` If there are two archives with different versions of a package, they are both imported. When an archive with a newer version is added in the artifact folder @@ -537,13 +604,14 @@ update to the latest version. You can disable the default Packagist repository by adding this to your `composer.json`: - { - "repositories": [ - { - "packagist": false - } - ] - } - +```json +{ + "repositories": [ + { + "packagist": false + } + ] +} +``` ← [Schema](04-schema.md) | [Community](06-community.md) → diff --git a/doc/articles/aliases.md b/doc/articles/aliases.md index 26a9c46ab..79c573d3d 100644 --- a/doc/articles/aliases.md +++ b/doc/articles/aliases.md @@ -7,7 +7,7 @@ ## Why aliases? When you are using a VCS repository, you will only get comparable versions for -branches that look like versions, such as `2.0`. For your `master` branch, you +branches that look like versions, such as `2.0` or `2.0.x`. For your `master` branch, you will get a `dev-master` version. For your `bugfix` branch, you will get a `dev-bugfix` version. @@ -28,18 +28,24 @@ someone will want the latest master dev version. Thus, Composer allows you to alias your `dev-master` branch to a `1.0.x-dev` version. It is done by specifying a `branch-alias` field under `extra` in `composer.json`: - { - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } +```json +{ + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" } } +} +``` -The branch version must begin with `dev-` (non-comparable version), the alias -must be a comparable dev version (i.e. start with numbers, and end with -`.x-dev`). The `branch-alias` must be present on the branch that it references. -For `dev-master`, you need to commit it on the `master` branch. +If you alias a non-comparible version (such as dev-develop) `dev-` must prefix the +branch name. You may also alias a comparible version (i.e. start with numbers, +and end with `.x-dev`), but only as a more specific version. +For example, 1.x-dev could be aliased as 1.2.x-dev. + +The alias must be a comparable dev version, and the `branch-alias` must be present on +the branch that it references. For `dev-master`, you need to commit it on the +`master` branch. As a result, anyone can now require `1.0.*` and it will happily install `dev-master`. @@ -68,18 +74,20 @@ You are using `symfony/monolog-bundle` which requires `monolog/monolog` version Just add this to your project's root `composer.json`: - { - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/you/monolog" - } - ], - "require": { - "symfony/monolog-bundle": "2.0", - "monolog/monolog": "dev-bugfix as 1.0.x-dev" +```json +{ + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/you/monolog" } + ], + "require": { + "symfony/monolog-bundle": "2.0", + "monolog/monolog": "dev-bugfix as 1.0.x-dev" } +} +``` That will fetch the `dev-bugfix` version of `monolog/monolog` from your GitHub and alias it to `1.0.x-dev`. diff --git a/doc/articles/custom-installers.md b/doc/articles/custom-installers.md index feeebe52c..98a9a2212 100644 --- a/doc/articles/custom-installers.md +++ b/doc/articles/custom-installers.md @@ -34,13 +34,15 @@ An example use-case would be: An example composer.json of such a template package would be: - { - "name": "phpdocumentor/template-responsive", - "type": "phpdocumentor-template", - "require": { - "phpdocumentor/template-installer-plugin": "*" - } +```json +{ + "name": "phpdocumentor/template-responsive", + "type": "phpdocumentor-template", + "require": { + "phpdocumentor/template-installer-plugin": "*" } +} +``` > **IMPORTANT**: to make sure that the template installer is present at the > time the template package is installed, template packages should require @@ -70,20 +72,22 @@ requirements: Example: - { - "name": "phpdocumentor/template-installer-plugin", - "type": "composer-plugin", - "license": "MIT", - "autoload": { - "psr-0": {"phpDocumentor\\Composer": "src/"} - }, - "extra": { - "class": "phpDocumentor\\Composer\\TemplateInstallerPlugin" - }, - "require": { - "composer-plugin-api": "1.0.0" - } +```json +{ + "name": "phpdocumentor/template-installer-plugin", + "type": "composer-plugin", + "license": "MIT", + "autoload": { + "psr-0": {"phpDocumentor\\Composer": "src/"} + }, + "extra": { + "class": "phpDocumentor\\Composer\\TemplateInstallerPlugin" + }, + "require": { + "composer-plugin-api": "1.0.0" } +} +``` ### The Plugin class @@ -96,20 +100,24 @@ autoloadable and matches the `extra.class` element in the package definition. Example: - namespace phpDocumentor\Composer; +```php +getInstallationManager()->addInstaller($installer); - } + $installer = new TemplateInstaller($io, $composer); + $composer->getInstallationManager()->addInstaller($installer); } +} +``` ### The Custom Installer class @@ -138,38 +146,42 @@ source for the exact signature): Example: - namespace phpDocumentor\Composer; +```php +getPrettyName(), 0, 23); - if ('phpdocumentor/template-' !== $prefix) { - throw new \InvalidArgumentException( - 'Unable to install template, phpdocumentor templates ' - .'should always start their package name with ' - .'"phpdocumentor/template-"' - ); - } - - return 'data/templates/'.substr($package->getPrettyName(), 23); + $prefix = substr($package->getPrettyName(), 0, 23); + if ('phpdocumentor/template-' !== $prefix) { + throw new \InvalidArgumentException( + 'Unable to install template, phpdocumentor templates ' + .'should always start their package name with ' + .'"phpdocumentor/template-"' + ); } - /** - * {@inheritDoc} - */ - public function supports($packageType) - { - return 'phpdocumentor-template' === $packageType; - } + return 'data/templates/'.substr($package->getPrettyName(), 23); + } + + /** + * {@inheritDoc} + */ + public function supports($packageType) + { + return 'phpdocumentor-template' === $packageType; } +} +``` The example demonstrates that it is quite simple to extend the [`Composer\Installer\LibraryInstaller`][5] class to strip a prefix diff --git a/doc/articles/handling-private-packages-with-satis.md b/doc/articles/handling-private-packages-with-satis.md index 0219f8108..5fce5377b 100644 --- a/doc/articles/handling-private-packages-with-satis.md +++ b/doc/articles/handling-private-packages-with-satis.md @@ -2,14 +2,22 @@ tagline: Host your own composer repository --> -# Handling private packages with Satis +# Handling private packages with Satis or Toran Proxy -Satis is a static `composer` repository generator. It is a bit like an ultra- -lightweight, static file-based version of packagist and can be used to host the -metadata of your company's private packages, or your own. It basically acts as -a micro-packagist. You can get it from -[GitHub](http://github.com/composer/satis) or install via CLI: -`composer.phar create-project composer/satis --stability=dev`. +# Toran Proxy + +[Toran Proxy](https://toranproxy.com/) is a commercial alternative to Satis offering professional support as well as a web UI to manage everything and a better integration with Composer. + +Toran's revenue is also used to pay for Composer and Packagist development and hosting so using it is a good way to support open source financially. You can find more information about how to set it up and use it on the [Toran Proxy](https://toranproxy.com/) website. + +# Satis + +Satis on the other hand is open source but only a static `composer` +repository generator. It is a bit like an ultra-lightweight, static file-based +version of packagist and can be used to host the metadata of your company's +private packages, or your own. You can get it from [GitHub](http://github.com/composer/satis) +or install via CLI: +`php composer.phar create-project composer/satis --stability=dev --keep-vcs`. ## Setup @@ -25,36 +33,40 @@ repositories you defined. The default file Satis looks for is `satis.json` in the root of the repository. - { - "name": "My Repository", - "homepage": "http://packages.example.org", - "repositories": [ - { "type": "vcs", "url": "http://github.com/mycompany/privaterepo" }, - { "type": "vcs", "url": "http://svn.example.org/private/repo" }, - { "type": "vcs", "url": "http://github.com/mycompany/privaterepo2" } - ], - "require-all": true - } +```json +{ + "name": "My Repository", + "homepage": "http://packages.example.org", + "repositories": [ + { "type": "vcs", "url": "http://github.com/mycompany/privaterepo" }, + { "type": "vcs", "url": "http://svn.example.org/private/repo" }, + { "type": "vcs", "url": "http://github.com/mycompany/privaterepo2" } + ], + "require-all": true +} +``` If you want to cherry pick which packages you want, you can list all the packages you want to have in your satis repository inside the classic composer `require` key, using a `"*"` constraint to make sure all versions are selected, or another constraint if you want really specific versions. - { - "repositories": [ - { "type": "vcs", "url": "http://github.com/mycompany/privaterepo" }, - { "type": "vcs", "url": "http://svn.example.org/private/repo" }, - { "type": "vcs", "url": "http://github.com/mycompany/privaterepo2" } - ], - "require": { - "company/package": "*", - "company/package2": "*", - "company/package3": "2.0.0" - } +```json +{ + "repositories": [ + { "type": "vcs", "url": "http://github.com/mycompany/privaterepo" }, + { "type": "vcs", "url": "http://svn.example.org/private/repo" }, + { "type": "vcs", "url": "http://github.com/mycompany/privaterepo2" } + ], + "require": { + "company/package": "*", + "company/package2": "*", + "company/package3": "2.0.0" } +} +``` -Once you did this, you just run `php bin/satis build `. +Once you've done this, you just run `php bin/satis build `. For example `php bin/satis build config.json web/` would read the `config.json` file and build a static repository inside the `web/` directory. @@ -80,14 +92,16 @@ everything should work smoothly. You don't need to copy all your repositories in every project anymore. Only that one unique repository that will update itself. - { - "repositories": [ { "type": "composer", "url": "http://packages.example.org/" } ], - "require": { - "company/package": "1.2.0", - "company/package2": "1.5.2", - "company/package3": "dev-master" - } +```json +{ + "repositories": [ { "type": "composer", "url": "http://packages.example.org/" } ], + "require": { + "company/package": "1.2.0", + "company/package2": "1.5.2", + "company/package3": "dev-master" } +} +``` ### Security @@ -97,39 +111,43 @@ connection options for the server. Example using a custom repository using SSH (requires the SSH2 PECL extension): - { - "repositories": [ - { - "type": "composer", - "url": "ssh2.sftp://example.org", - "options": { - "ssh2": { - "username": "composer", - "pubkey_file": "/home/composer/.ssh/id_rsa.pub", - "privkey_file": "/home/composer/.ssh/id_rsa" - } +```json +{ + "repositories": [ + { + "type": "composer", + "url": "ssh2.sftp://example.org", + "options": { + "ssh2": { + "username": "composer", + "pubkey_file": "/home/composer/.ssh/id_rsa.pub", + "privkey_file": "/home/composer/.ssh/id_rsa" } } - ] - } + } + ] +} +``` > **Tip:** See [ssh2 context options](http://www.php.net/manual/en/wrappers.ssh2.php#refsect1-wrappers.ssh2-options) for more information. Example using HTTP over SSL using a client certificate: - { - "repositories": [ - { - "type": "composer", - "url": "https://example.org", - "options": { - "ssl": { - "local_cert": "/home/composer/.ssl/composer.pem" - } +```json +{ + "repositories": [ + { + "type": "composer", + "url": "https://example.org", + "options": { + "ssl": { + "local_cert": "/home/composer/.ssl/composer.pem" } } - ] - } + } + ] +} +``` > **Tip:** See [ssl context options](http://www.php.net/manual/en/context.ssl.php) for more information. @@ -145,14 +163,16 @@ Subversion) will not have downloads available and thus installations usually tak To enable your satis installation to create downloads for all (Git, Mercurial and Subversion) your packages, add the following to your `satis.json`: - { - "archive": { - "directory": "dist", - "format": "tar", - "prefix-url": "https://amazing.cdn.example.org", - "skip-dev": true - } +```json +{ + "archive": { + "directory": "dist", + "format": "tar", + "prefix-url": "https://amazing.cdn.example.org", + "skip-dev": true } +} +``` #### Options explained @@ -178,11 +198,14 @@ It is possible to make satis automatically resolve and add all dependencies for with the Downloads functionality to have a complete local mirror of packages. Just add the following to your `satis.json`: -``` +```json { - "require-dependencies": true + "require-dependencies": true, + "require-dev-dependencies": true } ``` When searching for packages, satis will attempt to resolve all the required packages from the listed repositories. Therefore, if you are requiring a package from Packagist, you will need to define it in your `satis.json`. + +Dev dependencies are packaged only if the `require-dev-dependencies` parameter is set to true. diff --git a/doc/articles/http-basic-authentication.md b/doc/articles/http-basic-authentication.md new file mode 100644 index 000000000..1add2d7a6 --- /dev/null +++ b/doc/articles/http-basic-authentication.md @@ -0,0 +1,59 @@ + + +# HTTP basic authentication + +Your [Satis or Toran Proxy](handling-private-packages-with-satis.md) server +could be secured with http basic authentication. In order to allow your project +to have access to these packages you will have to tell composer how to +authenticate with your credentials. + +The simplest way to provide your credentials is providing your set +of credentials inline with the repository specification such as: + +```json +{ + "repositories": [ + { + "type": "composer", + "url": "http://extremely:secret@repo.example.org" + } + ] +} +``` + +This will basically teach composer how to authenticate automatically +when reading packages from the provided composer repository. + +This does not work for everybody especially when you don't want to +hard code your credentials into your composer.json. There is a second +way to provide these details and it is via interaction. If you don't +provide the authentication credentials composer will prompt you upon +connection to enter the username and password. + +The third way if you want to pre-configure it is via an `auth.json` file +located in your `COMPOSER_HOME` or besides your `composer.json`. + +The file should contain a set of hostnames followed each with their own +username/password pairs, for example: + +```json +{ + "basic-auth": { + "repo.example1.org": { + "username": "my-username1", + "password": "my-secret-password1" + }, + "repo.example2.org": { + "username": "my-username2", + "password": "my-secret-password2" + } + } +} +``` + +The main advantage of the auth.json file is that it can be gitignored so +that every developer in your team can place their own credentials in there, +which makes revokation of credentials much easier than if you all share the +same. diff --git a/doc/articles/plugins.md b/doc/articles/plugins.md index 75706f711..65884fd18 100644 --- a/doc/articles/plugins.md +++ b/doc/articles/plugins.md @@ -35,13 +35,15 @@ current composer plugin API version is 1.0.0. For example - { - "name": "my/plugin-package", - "type": "composer-plugin", - "require": { - "composer-plugin-api": "1.0.0" - } +```json +{ + "name": "my/plugin-package", + "type": "composer-plugin", + "require": { + "composer-plugin-api": "1.0.0" } +} +``` ### Plugin Class @@ -54,20 +56,24 @@ be read and all internal objects and state can be manipulated as desired. Example: - namespace phpDocumentor\Composer; +```php +getInstallationManager()->addInstaller($installer); - } + $installer = new TemplateInstaller($io, $composer); + $composer->getInstallationManager()->addInstaller($installer); } +} +``` ## Event Handler @@ -88,46 +94,50 @@ The events available for plugins are: Example: - namespace Naderman\Composer\AWS; +```php +composer = $composer; - $this->io = $io; - } +class AwsPlugin implements PluginInterface, EventSubscriberInterface +{ + protected $composer; + protected $io; - public static function getSubscribedEvents() - { - return array( - PluginEvents::PRE_FILE_DOWNLOAD => array( - array('onPreFileDownload', 0) - ), - ); - } + public function activate(Composer $composer, IOInterface $io) + { + $this->composer = $composer; + $this->io = $io; + } + + public static function getSubscribedEvents() + { + return array( + PluginEvents::PRE_FILE_DOWNLOAD => array( + array('onPreFileDownload', 0) + ), + ); + } - public function onPreFileDownload(PreFileDownloadEvent $event) - { - $protocol = parse_url($event->getProcessedUrl(), PHP_URL_SCHEME); + public function onPreFileDownload(PreFileDownloadEvent $event) + { + $protocol = parse_url($event->getProcessedUrl(), PHP_URL_SCHEME); - if ($protocol === 's3') { - $awsClient = new AwsClient($this->io, $this->composer->getConfig()); - $s3RemoteFilesystem = new S3RemoteFilesystem($this->io, $event->getRemoteFilesystem()->getOptions(), $awsClient); - $event->setRemoteFilesystem($s3RemoteFilesystem); - } + if ($protocol === 's3') { + $awsClient = new AwsClient($this->io, $this->composer->getConfig()); + $s3RemoteFilesystem = new S3RemoteFilesystem($this->io, $event->getRemoteFilesystem()->getOptions(), $awsClient); + $event->setRemoteFilesystem($s3RemoteFilesystem); } } +} +``` ## Using Plugins diff --git a/doc/articles/scripts.md b/doc/articles/scripts.md index 06e7c784d..3e6ef54cf 100644 --- a/doc/articles/scripts.md +++ b/doc/articles/scripts.md @@ -11,9 +11,9 @@ static method) or any command-line executable command. Scripts are useful for executing a package's custom code or package-specific commands during the Composer execution process. -**NOTE: Only scripts defined in the root package's `composer.json` are -executed. If a dependency of the root package specifies its own scripts, -Composer does not execute those additional scripts.** +> **Note:** Only scripts defined in the root package's `composer.json` are +> executed. If a dependency of the root package specifies its own scripts, +> Composer does not execute those additional scripts. ## Event names @@ -26,6 +26,8 @@ Composer fires the following named events during its execution process: - **post-update-cmd**: occurs after the `update` command is executed. - **pre-status-cmd**: occurs before the `status` command is executed. - **post-status-cmd**: occurs after the `status` command is executed. +- **pre-dependencies-solving**: occurs before the dependencies are resolved. +- **post-dependencies-solving**: occurs after the dependencies are resolved. - **pre-package-install**: occurs before a package is installed. - **post-package-install**: occurs after a package is installed. - **pre-package-update**: occurs before a package is updated. @@ -40,13 +42,15 @@ Composer fires the following named events during its execution process: installed, during the `create-project` command. - **post-create-project-cmd**: occurs after the `create-project` command is executed. +- **pre-archive-cmd**: occurs before the `archive` command is executed. +- **post-archive-cmd**: occurs after the `archive` command is executed. -**NOTE: Composer makes no assumptions about the state of your dependencies -prior to `install` or `update`. Therefore, you should not specify scripts that -require Composer-managed dependencies in the `pre-update-cmd` or -`pre-install-cmd` event hooks. If you need to execute scripts prior to -`install` or `update` please make sure they are self-contained within your -root package.** +> **Note:** Composer makes no assumptions about the state of your dependencies +> prior to `install` or `update`. Therefore, you should not specify scripts +> that require Composer-managed dependencies in the `pre-update-cmd` or +> `pre-install-cmd` event hooks. If you need to execute scripts prior to +> `install` or `update` please make sure they are self-contained within your +> root package. ## Defining scripts @@ -59,53 +63,60 @@ For any given event: - Scripts execute in the order defined when their corresponding event is fired. - An array of scripts wired to a single event can contain both PHP callbacks -and command-line executables commands. +and command-line executable commands. - PHP classes containing defined callbacks must be autoloadable via Composer's autoload functionality. Script definition example: - { - "scripts": { - "post-update-cmd": "MyVendor\\MyClass::postUpdate", - "post-package-install": [ - "MyVendor\\MyClass::postPackageInstall" - ], - "post-install-cmd": [ - "MyVendor\\MyClass::warmCache", - "phpunit -c app/" - ] - } +```json +{ + "scripts": { + "post-update-cmd": "MyVendor\\MyClass::postUpdate", + "post-package-install": [ + "MyVendor\\MyClass::postPackageInstall" + ], + "post-install-cmd": [ + "MyVendor\\MyClass::warmCache", + "phpunit -c app/" + ], + "post-create-project-cmd" : [ + "php -r \"copy('config/local-example.php', 'config/local.php');\"" + ] } +} +``` Using the previous definition example, here's the class `MyVendor\MyClass` that might be used to execute the PHP callbacks: - getComposer(); + // do stuff + } + + public static function postPackageInstall(Event $event) + { + $installedPackage = $event->getOperation()->getPackage(); + // do stuff + } - class MyClass + public static function warmCache(Event $event) { - public static function postUpdate(Event $event) - { - $composer = $event->getComposer(); - // do stuff - } - - public static function postPackageInstall(Event $event) - { - $installedPackage = $event->getOperation()->getPackage(); - // do stuff - } - - public static function warmCache(Event $event) - { - // make cache toasty - } + // make cache toasty } +} +``` When an event is fired, Composer's internal event handler receives a `Composer\Script\Event` object, which is passed as the first argument to your @@ -120,6 +131,33 @@ PHP callback. This `Event` object has getters for other contextual objects: If you would like to run the scripts for an event manually, the syntax is: - $ composer run-script [--dev] [--no-dev] script +```sh +composer run-script [--dev] [--no-dev] script +``` + +For example `composer run-script post-install-cmd` will run any +**post-install-cmd** scripts that have been defined. + +You can also give additional arguments to the script handler by appending `--` +followed by the handler arguments. e.g. +`composer run-script post-install-cmd -- --check` will pass`--check` along to +the script handler. Those arguments are received as CLI arg by CLI handlers, +and can be retrieved as an array via `$event->getArguments()` by PHP handlers. + +## Writing custom commands + +If you add custom scripts that do not fit one of the predefined event name +above, you can either run them with run-script or also run them as native +Composer commands. For example the handler defined below is executable by +simply running `composer test`: + +```json +{ + "scripts": { + "test": "phpunit" + } +} +``` -For example `composer run-script post-install-cmd` will run any **post-install-cmd** scripts that have been defined. +> **Note:** Composer's bin-dir is pushed on top of the PATH so that binaries +> of dependencies are easily accessible as CLI commands when writing scripts. diff --git a/doc/articles/troubleshooting.md b/doc/articles/troubleshooting.md index e30b08cf8..4de3ad19e 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -21,6 +21,8 @@ This is a list of common pitfalls on using Composer, and how to avoid them. possible interferences with existing vendor installations or `composer.lock` entries. +5. Try clearing Composer's cache by running `composer clear-cache`. + ## Package not found 1. Double-check you **don't have typos** in your `composer.json` or repository @@ -38,6 +40,9 @@ This is a list of common pitfalls on using Composer, and how to avoid them. your repository, especially when maintaining a third party fork and using `replace`. +5. If you are updating to a recently published version of a package, be aware that + Packagist has a delay of up to 1 minute before new packages are visible to Composer. + ## Package not found on travis-ci.org 1. Check the ["Package not found"](#package-not-found) item above. @@ -63,12 +68,14 @@ You can fix this by aliasing version 0.11 to 0.1: composer.json: - { - "require": { - "A": "0.2", - "B": "0.11 as 0.1" - } +```json +{ + "require": { + "A": "0.2", + "B": "0.11 as 0.1" } +} +``` See [aliases](aliases.md) for more information. @@ -76,7 +83,7 @@ See [aliases](aliases.md) for more information. If composer shows memory errors on some commands: - PHP Fatal error: Allowed memory size of XXXXXX bytes exhausted <...> +`PHP Fatal error: Allowed memory size of XXXXXX bytes exhausted <...>` The PHP `memory_limit` should be increased. @@ -86,35 +93,67 @@ The PHP `memory_limit` should be increased. To get the current `memory_limit` value, run: - php -r "echo ini_get('memory_limit').PHP_EOL;" +```sh +php -r "echo ini_get('memory_limit').PHP_EOL;" +``` Try increasing the limit in your `php.ini` file (ex. `/etc/php5/cli/php.ini` for Debian-like systems): - ; Use -1 for unlimited or define an explicit value like 512M - memory_limit = -1 +```ini +; Use -1 for unlimited or define an explicit value like 512M +memory_limit = -1 +``` Or, you can increase the limit with a command-line argument: - php -d memory_limit=-1 composer.phar <...> +```sh +php -d memory_limit=-1 composer.phar <...> +``` ## "The system cannot find the path specified" (Windows) 1. Open regedit. -2. Search for an ```AutoRun``` key inside ```HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor``` - or ```HKEY_CURRENT_USER\Software\Microsoft\Command Processor```. +2. Search for an `AutoRun` key inside `HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor`, + `HKEY_CURRENT_USER\Software\Microsoft\Command Processor` + or `HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Command Processor`. 3. Check if it contains any path to non-existent file, if it's the case, just remove them. -## API rate limit and two factor authentication +## API rate limit and OAuth tokens Because of GitHub's rate limits on their API it can happen that Composer prompts for authentication asking your username and password so it can go ahead with its work. -Unfortunately this will not work if you enabled two factor authentication on -your GitHub account and to solve this issue you need to: -1. [Create](https://github.com/settings/applications) an oauthtoken on GitHub. +If you would prefer not to provide your GitHub credentials to Composer you can +manually create a token using the following procedure: + +1. [Create](https://github.com/settings/applications) an OAuth token on GitHub. [Read more](https://github.com/blog/1509-personal-api-tokens) on this. 2. Add it to the configuration running `composer config -g github-oauth.github.com ` Now Composer should install/update without asking for authentication. + +## proc_open(): fork failed errors +If composer shows proc_open() fork failed on some commands: + +`PHP Fatal error: Uncaught exception 'ErrorException' with message 'proc_open(): fork failed - Cannot allocate memory' in phar` + +This could be happening because the VPS runs out of memory and has no Swap space enabled. + +```sh +free -m + +total used free shared buffers cached +Mem: 2048 357 1690 0 0 237 +-/+ buffers/cache: 119 1928 +Swap: 0 0 0 +``` + +To enable the swap you can use for example: + +```sh +/bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=1024 +/sbin/mkswap /var/swap.1 +/sbin/swapon /var/swap.1 +``` diff --git a/doc/articles/vendor-binaries.md b/doc/articles/vendor-binaries.md index b258dccb7..b65b6bcf4 100644 --- a/doc/articles/vendor-binaries.md +++ b/doc/articles/vendor-binaries.md @@ -20,10 +20,11 @@ It is defined by adding the `bin` key to a project's `composer.json`. It is specified as an array of files so multiple binaries can be added for any given project. - { - "bin": ["bin/my-script", "bin/my-other-script"] - } - +```json +{ + "bin": ["bin/my-script", "bin/my-other-script"] +} +``` ## What does defining a vendor binary in composer.json do? @@ -46,22 +47,26 @@ symlink is created from each dependency's binaries to `vendor/bin`. Say package `my-vendor/project-a` has binaries setup like this: - { - "name": "my-vendor/project-a", - "bin": ["bin/project-a-bin"] - } +```json +{ + "name": "my-vendor/project-a", + "bin": ["bin/project-a-bin"] +} +``` Running `composer install` for this `composer.json` will not do anything with `bin/project-a-bin`. Say project `my-vendor/project-b` has requirements setup like this: - { - "name": "my-vendor/project-b", - "require": { - "my-vendor/project-a": "*" - } +```json +{ + "name": "my-vendor/project-b", + "require": { + "my-vendor/project-a": "*" } +} +``` Running `composer install` for this `composer.json` will look at all of project-b's dependencies and install them to `vendor/bin`. @@ -95,12 +100,16 @@ Yes, there are two ways an alternate vendor binary location can be specified: An example of the former looks like this: - { - "config": { - "bin-dir": "scripts" - } +```json +{ + "config": { + "bin-dir": "scripts" } +} +``` Running `composer install` for this `composer.json` will result in all of the vendor binaries being installed in `scripts/` instead of `vendor/bin/`. + +You can set `bin-dir` to `./` to put binaries in your project root. diff --git a/doc/faqs/how-do-i-install-a-package-to-a-custom-path-for-my-framework.md b/doc/faqs/how-do-i-install-a-package-to-a-custom-path-for-my-framework.md index b5956ca19..bd38d1e40 100644 --- a/doc/faqs/how-do-i-install-a-package-to-a-custom-path-for-my-framework.md +++ b/doc/faqs/how-do-i-install-a-package-to-a-custom-path-for-my-framework.md @@ -11,13 +11,15 @@ This is common if your package is intended for a specific framework such as CakePHP, Drupal or WordPress. Here is an example composer.json file for a WordPress theme: - { - "name": "you/themename", - "type": "wordpress-theme", - "require": { - "composer/installers": "~1.0" - } +```json +{ + "name": "you/themename", + "type": "wordpress-theme", + "require": { + "composer/installers": "~1.0" } +} +``` Now when your theme is installed with Composer it will be placed into `wp-content/themes/themename/` folder. Check the @@ -30,13 +32,15 @@ useful example would be for a Drupal multisite setup where the package should be installed into your sites subdirectory. Here we are overriding the install path for a module that uses composer/installers: - { - "extra": { - "installer-paths": { - "sites/example.com/modules/{$name}": ["vendor/package"] - } +```json +{ + "extra": { + "installer-paths": { + "sites/example.com/modules/{$name}": ["vendor/package"] } } +} +``` Now the package would be installed to your folder location, rather than the default composer/installers determined location. diff --git a/doc/faqs/should-i-commit-the-dependencies-in-my-vendor-directory.md b/doc/faqs/should-i-commit-the-dependencies-in-my-vendor-directory.md index 8e50f7264..cdbd3141c 100644 --- a/doc/faqs/should-i-commit-the-dependencies-in-my-vendor-directory.md +++ b/doc/faqs/should-i-commit-the-dependencies-in-my-vendor-directory.md @@ -24,6 +24,7 @@ If you really feel like you must do this, you have a few options: [config](../04-schema.md#config). 3. Remove the `.git` directory of every dependency after the installation, then you can add them to your git repo. You can do that with `rm -rf vendor/**/.git` + in ZSH or `find vendor/ -type d -name ".git" -exec rm -rf {} \;` in Bash. but this means you will have to delete those dependencies from disk before running composer update. 4. Add a .gitignore rule (`vendor/.git`) to ignore all the vendor `.git` folders. diff --git a/doc/faqs/why-are-unbound-version-constraints-a-bad-idea.md b/doc/faqs/why-are-unbound-version-constraints-a-bad-idea.md new file mode 100644 index 000000000..183403948 --- /dev/null +++ b/doc/faqs/why-are-unbound-version-constraints-a-bad-idea.md @@ -0,0 +1,21 @@ +# Why are unbound version constraints a bad idea? + +A version constraint without an upper bound such as `*`, `>=3.4` or +`dev-master` will allow updates to any future version of the dependency. +This includes major versions breaking backward compatibility. + +Once a release of your package is tagged, you cannot tweak its dependencies +anymore in case a dependency breaks BC - you have to do a new release but the +previous one stays broken. + +The only good alternative is to define an upper bound on your constraints, +which you can increase in a new release after testing that your package is +compatible with the new major version of your dependency. + +For example instead of using `>=3.4` you should use `~3.4` which allows all +versions up to `3.999` but does not include `4.0` and above. The `~` operator +works very well with libraries follow [semantic versioning](http://semver.org). + +**Note:** As a package maintainer, you can make the life of your users easier +by providing an [alias version](../articles/aliases.md) for your development +branch to allow it to match bound constraints. diff --git a/doc/faqs/why-can't-composer-load-repositories-recursively.md b/doc/faqs/why-can't-composer-load-repositories-recursively.md index d81a0f066..0ab44c7d2 100644 --- a/doc/faqs/why-can't-composer-load-repositories-recursively.md +++ b/doc/faqs/why-can't-composer-load-repositories-recursively.md @@ -9,7 +9,7 @@ that the main use of custom VCS & package repositories is to temporarily try some things, or use a fork of a project until your pull request is merged, etc. You should not use them to keep track of private packages. For that you should look into [setting up Satis](../articles/handling-private-packages-with-satis.md) -for your company or even for yourself. +or getting a [Toran Proxy](https://toranproxy.com) license for your company. There are three ways the dependency solver could work with custom repositories: diff --git a/doc/fixtures/fixtures.md b/doc/fixtures/fixtures.md new file mode 100644 index 000000000..fd77f3c37 --- /dev/null +++ b/doc/fixtures/fixtures.md @@ -0,0 +1,20 @@ +`Composer` type repository fixtures +======================= + +This directory contains some examples of what `composer` type repositories can look like. They serve as illustrating examples accompanying the docs, but can also be used as (initial) fixtures for tests. + +* `repo-composer-plain` is a simple, plain `packages.json` file +* `repo-composer-with-includes` uses the `includes` mechanism +* `repo-composer-with-providers` uses the `providers` mechanism + +Sample Packages used in these fixtures +------- + +All these repositories contain the following packages. + +* `foo/bar` versions 1.0.0, 1.0.1 and 1.1.0; dev-default and 1.0.x-dev branches. On dev-default and in 1.1.0, `bar/baz` ~1.0 is required. +* `qux/quux` only has a dev-default branch. It `replace`s `gar/nix`. +* `gar/nix` has a 1.0.0 version and a dev-default branch. It is being replaced by `qux/quux`. +* `bar/baz` has a 1.0.0 version and 1.0.x-dev as well as dev-default branches. Additionally, 1.1.x-dev is a branch alias for dev-default. + + diff --git a/doc/fixtures/repo-composer-plain/packages.json b/doc/fixtures/repo-composer-plain/packages.json new file mode 100644 index 000000000..219051998 --- /dev/null +++ b/doc/fixtures/repo-composer-plain/packages.json @@ -0,0 +1,158 @@ +{ + "packages": { + "bar/baz": { + "1.0.0": { + "name": "bar/baz", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "35810817c14d" + }, + "time": "2014-10-13 12:04:55", + "type": "library" + }, + "1.0.x-dev": { + "name": "bar/baz", + "version": "1.0.x-dev", + "version_normalized": "1.0.9999999.9999999-dev", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "ffff9aae6ed5" + }, + "time": "2014-10-13 12:05:37", + "type": "library" + }, + "dev-default": { + "name": "bar/baz", + "version": "dev-default", + "version_normalized": "9999999-dev", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "f317e556f2e2" + }, + "time": "2014-10-13 12:06:45", + "type": "library", + "extra": { + "branch-alias": { + "dev-default": "1.1.x-dev" + } + } + } + }, + "foo/bar": { + "1.0.0": { + "name": "foo/bar", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "249dec95a52a" + }, + "time": "2014-10-11 15:42:00", + "type": "library" + }, + "1.0.1": { + "name": "foo/bar", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "21e3328295d4" + }, + "time": "2014-10-11 15:45:56", + "type": "library" + }, + "1.0.x-dev": { + "name": "foo/bar", + "version": "1.0.x-dev", + "version_normalized": "1.0.9999999.9999999-dev", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "14dc17c8e860" + }, + "time": "2014-10-11 15:45:59", + "type": "library" + }, + "1.1.0": { + "name": "foo/bar", + "version": "1.1.0", + "version_normalized": "1.1.0.0", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "d2fa3e69ad5b" + }, + "require": { + "bar/baz": "~1.0" + }, + "time": "2014-10-11 15:43:16", + "type": "library" + }, + "dev-default": { + "name": "foo/bar", + "version": "dev-default", + "version_normalized": "9999999-dev", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "8e5a5c224336" + }, + "require": { + "bar/baz": "~1.0" + }, + "time": "2014-10-11 15:43:18", + "type": "library" + } + }, + "gar/nix": { + "1.0.0": { + "name": "gar/nix", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "44977145d64e" + }, + "time": "2014-10-13 12:03:33", + "type": "library" + }, + "dev-default": { + "name": "gar/nix", + "version": "dev-default", + "version_normalized": "9999999-dev", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "51cca95a31c2" + }, + "time": "2014-10-13 12:03:35", + "type": "library" + } + }, + "qux/quux": { + "dev-default": { + "name": "qux/quux", + "version": "dev-default", + "version_normalized": "9999999-dev", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "4a10a567baa5" + }, + "replace": { + "gar/nix": "1.0.*" + }, + "time": "2014-10-11 15:48:15", + "type": "library" + } + } + } +} diff --git a/doc/fixtures/repo-composer-with-includes/include/all$5fa86b937f0502d92f776072cd49c002dca742b9.json b/doc/fixtures/repo-composer-with-includes/include/all$5fa86b937f0502d92f776072cd49c002dca742b9.json new file mode 100644 index 000000000..219051998 --- /dev/null +++ b/doc/fixtures/repo-composer-with-includes/include/all$5fa86b937f0502d92f776072cd49c002dca742b9.json @@ -0,0 +1,158 @@ +{ + "packages": { + "bar/baz": { + "1.0.0": { + "name": "bar/baz", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "35810817c14d" + }, + "time": "2014-10-13 12:04:55", + "type": "library" + }, + "1.0.x-dev": { + "name": "bar/baz", + "version": "1.0.x-dev", + "version_normalized": "1.0.9999999.9999999-dev", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "ffff9aae6ed5" + }, + "time": "2014-10-13 12:05:37", + "type": "library" + }, + "dev-default": { + "name": "bar/baz", + "version": "dev-default", + "version_normalized": "9999999-dev", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "f317e556f2e2" + }, + "time": "2014-10-13 12:06:45", + "type": "library", + "extra": { + "branch-alias": { + "dev-default": "1.1.x-dev" + } + } + } + }, + "foo/bar": { + "1.0.0": { + "name": "foo/bar", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "249dec95a52a" + }, + "time": "2014-10-11 15:42:00", + "type": "library" + }, + "1.0.1": { + "name": "foo/bar", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "21e3328295d4" + }, + "time": "2014-10-11 15:45:56", + "type": "library" + }, + "1.0.x-dev": { + "name": "foo/bar", + "version": "1.0.x-dev", + "version_normalized": "1.0.9999999.9999999-dev", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "14dc17c8e860" + }, + "time": "2014-10-11 15:45:59", + "type": "library" + }, + "1.1.0": { + "name": "foo/bar", + "version": "1.1.0", + "version_normalized": "1.1.0.0", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "d2fa3e69ad5b" + }, + "require": { + "bar/baz": "~1.0" + }, + "time": "2014-10-11 15:43:16", + "type": "library" + }, + "dev-default": { + "name": "foo/bar", + "version": "dev-default", + "version_normalized": "9999999-dev", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "8e5a5c224336" + }, + "require": { + "bar/baz": "~1.0" + }, + "time": "2014-10-11 15:43:18", + "type": "library" + } + }, + "gar/nix": { + "1.0.0": { + "name": "gar/nix", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "44977145d64e" + }, + "time": "2014-10-13 12:03:33", + "type": "library" + }, + "dev-default": { + "name": "gar/nix", + "version": "dev-default", + "version_normalized": "9999999-dev", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "51cca95a31c2" + }, + "time": "2014-10-13 12:03:35", + "type": "library" + } + }, + "qux/quux": { + "dev-default": { + "name": "qux/quux", + "version": "dev-default", + "version_normalized": "9999999-dev", + "source": { + "type": "hg", + "url": "http://some.where/over/the/rainbow/", + "reference": "4a10a567baa5" + }, + "replace": { + "gar/nix": "1.0.*" + }, + "time": "2014-10-11 15:48:15", + "type": "library" + } + } + } +} diff --git a/doc/fixtures/repo-composer-with-includes/packages.json b/doc/fixtures/repo-composer-with-includes/packages.json new file mode 100644 index 000000000..1ef97530a --- /dev/null +++ b/doc/fixtures/repo-composer-with-includes/packages.json @@ -0,0 +1,10 @@ +{ + "packages": [ + + ], + "includes": { + "include/all$5fa86b937f0502d92f776072cd49c002dca742b9.json": { + "sha1": "5fa86b937f0502d92f776072cd49c002dca742b9" + } + } +} diff --git a/doc/fixtures/repo-composer-with-providers/p/bar/baz$923363b3c22e73abb2e3fd891c8156dd4d0821a97fd3e428bc910833e3e46dbe.json b/doc/fixtures/repo-composer-with-providers/p/bar/baz$923363b3c22e73abb2e3fd891c8156dd4d0821a97fd3e428bc910833e3e46dbe.json new file mode 100644 index 000000000..94a43b019 --- /dev/null +++ b/doc/fixtures/repo-composer-with-providers/p/bar/baz$923363b3c22e73abb2e3fd891c8156dd4d0821a97fd3e428bc910833e3e46dbe.json @@ -0,0 +1,50 @@ +{ + "packages": { + "bar\/baz": { + "1.0.0": { + "name": "bar\/baz", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "hg", + "url": "http:\/\/some.where\/over\/the\/rainbow\/", + "reference": "35810817c14d" + }, + "time": "2014-10-13 12:04:55", + "type": "library", + "uid": 0 + }, + "1.0.x-dev": { + "name": "bar\/baz", + "version": "1.0.x-dev", + "version_normalized": "1.0.9999999.9999999-dev", + "source": { + "type": "hg", + "url": "http:\/\/some.where\/over\/the\/rainbow\/", + "reference": "ffff9aae6ed5" + }, + "time": "2014-10-13 12:05:37", + "type": "library", + "uid": 1 + }, + "dev-default": { + "name": "bar\/baz", + "version": "dev-default", + "version_normalized": "9999999-dev", + "source": { + "type": "hg", + "url": "http:\/\/some.where\/over\/the\/rainbow\/", + "reference": "f317e556f2e2" + }, + "time": "2014-10-13 12:06:45", + "type": "library", + "extra": { + "branch-alias": { + "dev-default": "1.1.x-dev" + } + }, + "uid": 2 + } + } + } +} \ No newline at end of file diff --git a/doc/fixtures/repo-composer-with-providers/p/foo/bar$4baabb3303afa3e34a4d3af18fb138e5f3b79029c1f8d9ab5b477ea15776ba0a.json b/doc/fixtures/repo-composer-with-providers/p/foo/bar$4baabb3303afa3e34a4d3af18fb138e5f3b79029c1f8d9ab5b477ea15776ba0a.json new file mode 100644 index 000000000..7dc7cc91e --- /dev/null +++ b/doc/fixtures/repo-composer-with-providers/p/foo/bar$4baabb3303afa3e34a4d3af18fb138e5f3b79029c1f8d9ab5b477ea15776ba0a.json @@ -0,0 +1,77 @@ +{ + "packages": { + "foo\/bar": { + "1.0.0": { + "name": "foo\/bar", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "hg", + "url": "http:\/\/some.where\/over\/the\/rainbow\/", + "reference": "249dec95a52a" + }, + "time": "2014-10-11 15:42:00", + "type": "library", + "uid": 3 + }, + "1.0.1": { + "name": "foo\/bar", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "hg", + "url": "http:\/\/some.where\/over\/the\/rainbow\/", + "reference": "21e3328295d4" + }, + "time": "2014-10-11 15:45:56", + "type": "library", + "uid": 4 + }, + "1.0.x-dev": { + "name": "foo\/bar", + "version": "1.0.x-dev", + "version_normalized": "1.0.9999999.9999999-dev", + "source": { + "type": "hg", + "url": "http:\/\/some.where\/over\/the\/rainbow\/", + "reference": "14dc17c8e860" + }, + "time": "2014-10-11 15:45:59", + "type": "library", + "uid": 5 + }, + "1.1.0": { + "name": "foo\/bar", + "version": "1.1.0", + "version_normalized": "1.1.0.0", + "source": { + "type": "hg", + "url": "http:\/\/some.where\/over\/the\/rainbow\/", + "reference": "d2fa3e69ad5b" + }, + "require": { + "bar\/baz": "~1.0" + }, + "time": "2014-10-11 15:43:16", + "type": "library", + "uid": 6 + }, + "dev-default": { + "name": "foo\/bar", + "version": "dev-default", + "version_normalized": "9999999-dev", + "source": { + "type": "hg", + "url": "http:\/\/some.where\/over\/the\/rainbow\/", + "reference": "8e5a5c224336" + }, + "require": { + "bar\/baz": "~1.0" + }, + "time": "2014-10-11 15:43:18", + "type": "library", + "uid": 7 + } + } + } +} \ No newline at end of file diff --git a/doc/fixtures/repo-composer-with-providers/p/gar/nix$5d210670cb46c8364c8e3fb449967b9bea558b971e5b082f330ae4f1d484c321.json b/doc/fixtures/repo-composer-with-providers/p/gar/nix$5d210670cb46c8364c8e3fb449967b9bea558b971e5b082f330ae4f1d484c321.json new file mode 100644 index 000000000..512b8d882 --- /dev/null +++ b/doc/fixtures/repo-composer-with-providers/p/gar/nix$5d210670cb46c8364c8e3fb449967b9bea558b971e5b082f330ae4f1d484c321.json @@ -0,0 +1,50 @@ +{ + "packages": { + "qux\/quux": { + "dev-default": { + "name": "qux\/quux", + "version": "dev-default", + "version_normalized": "9999999-dev", + "source": { + "type": "hg", + "url": "http:\/\/some.where\/over\/the\/rainbow\/", + "reference": "4a10a567baa5" + }, + "replace": { + "gar\/nix": "1.0.*" + }, + "time": "2014-10-11 15:48:15", + "type": "library", + "uid": 10 + } + }, + "gar\/nix": { + "1.0.0": { + "name": "gar\/nix", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "hg", + "url": "http:\/\/some.where\/over\/the\/rainbow\/", + "reference": "44977145d64e" + }, + "time": "2014-10-13 12:03:33", + "type": "library", + "uid": 8 + }, + "dev-default": { + "name": "gar\/nix", + "version": "dev-default", + "version_normalized": "9999999-dev", + "source": { + "type": "hg", + "url": "http:\/\/some.where\/over\/the\/rainbow\/", + "reference": "51cca95a31c2" + }, + "time": "2014-10-13 12:03:35", + "type": "library", + "uid": 9 + } + } + } +} \ No newline at end of file diff --git a/doc/fixtures/repo-composer-with-providers/p/provider-active$1893a061e579543822389ecd12d791c612db0c05e22d90e9286e233cacd86ed8.json b/doc/fixtures/repo-composer-with-providers/p/provider-active$1893a061e579543822389ecd12d791c612db0c05e22d90e9286e233cacd86ed8.json new file mode 100644 index 000000000..b82eb418e --- /dev/null +++ b/doc/fixtures/repo-composer-with-providers/p/provider-active$1893a061e579543822389ecd12d791c612db0c05e22d90e9286e233cacd86ed8.json @@ -0,0 +1,16 @@ +{ + "providers": { + "bar\/baz": { + "sha256": "923363b3c22e73abb2e3fd891c8156dd4d0821a97fd3e428bc910833e3e46dbe" + }, + "foo\/bar": { + "sha256": "4baabb3303afa3e34a4d3af18fb138e5f3b79029c1f8d9ab5b477ea15776ba0a" + }, + "gar\/nix": { + "sha256": "5d210670cb46c8364c8e3fb449967b9bea558b971e5b082f330ae4f1d484c321" + }, + "qux\/quux": { + "sha256": "c142d1a07ca354be46b613f59f1d601923a5a00ccc5fcce50a77ecdd461eb72d" + } + } +} \ No newline at end of file diff --git a/doc/fixtures/repo-composer-with-providers/p/qux/quux$c142d1a07ca354be46b613f59f1d601923a5a00ccc5fcce50a77ecdd461eb72d.json b/doc/fixtures/repo-composer-with-providers/p/qux/quux$c142d1a07ca354be46b613f59f1d601923a5a00ccc5fcce50a77ecdd461eb72d.json new file mode 100644 index 000000000..014187206 --- /dev/null +++ b/doc/fixtures/repo-composer-with-providers/p/qux/quux$c142d1a07ca354be46b613f59f1d601923a5a00ccc5fcce50a77ecdd461eb72d.json @@ -0,0 +1,22 @@ +{ + "packages": { + "qux\/quux": { + "dev-default": { + "name": "qux\/quux", + "version": "dev-default", + "version_normalized": "9999999-dev", + "source": { + "type": "hg", + "url": "http:\/\/some.where\/over\/the\/rainbow\/", + "reference": "4a10a567baa5" + }, + "replace": { + "gar\/nix": "1.0.*" + }, + "time": "2014-10-11 15:48:15", + "type": "library", + "uid": 10 + } + } + } +} \ No newline at end of file diff --git a/doc/fixtures/repo-composer-with-providers/packages.json b/doc/fixtures/repo-composer-with-providers/packages.json new file mode 100644 index 000000000..65968a861 --- /dev/null +++ b/doc/fixtures/repo-composer-with-providers/packages.json @@ -0,0 +1,9 @@ +{ + "packages": [], + "providers-url": "\/p\/%package%$%hash%.json", + "provider-includes": { + "p\/provider-active$1893a061e579543822389ecd12d791c612db0c05e22d90e9286e233cacd86ed8.json": { + "sha256": "1893a061e579543822389ecd12d791c612db0c05e22d90e9286e233cacd86ed8" + } + } +} \ No newline at end of file diff --git a/res/composer-schema.json b/res/composer-schema.json index 6687de280..749d2055b 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -1,12 +1,13 @@ { + "$schema": "http://json-schema.org/draft-04/schema#", "name": "Package", "type": "object", "additionalProperties": false, + "required": [ "name", "description" ], "properties": { "name": { "type": "string", - "description": "Package name, including 'vendor-name/' prefix.", - "required": true + "description": "Package name, including 'vendor-name/' prefix." }, "type": { "description": "Package type, either 'library' for common packages, 'composer-plugin' for plugins, 'metapackage' for empty packages, or a custom type ([a-z0-9-]+) defined by whatever project this package applies to.", @@ -18,8 +19,7 @@ }, "description": { "type": "string", - "description": "Short package description.", - "required": true + "description": "Short package description." }, "keywords": { "type": "array", @@ -39,7 +39,7 @@ }, "time": { "type": "string", - "description": "Package release date, in 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' format." + "description": "Package release date, in 'YYYY-MM-DD', 'YYYY-MM-DD HH:MM:SS' or 'YYYY-MM-DDTHH:MM:SSZ' format." }, "license": { "type": ["string", "array"], @@ -51,11 +51,11 @@ "items": { "type": "object", "additionalProperties": false, + "required": [ "name"], "properties": { "name": { "type": "string", - "description": "Full name of the author.", - "required": true + "description": "Full name of the author." }, "email": { "type": "string", @@ -136,6 +136,15 @@ "description": "A hash of domain name => github API oauth tokens, typically {\"github.com\":\"\"}.", "additionalProperties": true }, + "http-basic": { + "type": "object", + "description": "A hash of domain name => {\"username\": \"...\", \"password\": \"...\"}.", + "additionalProperties": true + }, + "store-auths": { + "type": ["string", "boolean"], + "description": "What to do after prompting for authentication, one of: true (store), false (do not store) or \"prompt\" (ask every time), defaults to prompt." + }, "vendor-dir": { "type": "string", "description": "The location where all packages are installed, defaults to \"vendor\"." @@ -182,18 +191,26 @@ }, "optimize-autoloader": { "type": "boolean", - "description": "Always optimize when dumping the autoloader" + "description": "Always optimize when dumping the autoloader." }, "prepend-autoloader": { "type": "boolean", "description": "If false, the composer autoloader will not be prepended to existing autoloaders, defaults to true." }, + "classmap-authoritative": { + "type": "boolean", + "description": "If true, the composer autoloader will not scan the filesystem for classes that are not found in the class map, defaults to false." + }, "github-domains": { "type": "array", "description": "A list of domains to use in github mode. This is used for GitHub Enterprise setups, defaults to [\"github.com\"].", "items": { "type": "string" } + }, + "github-expose-hostname": { + "type": "boolean", + "description": "Defaults to true. If set to false, the OAuth tokens created to access the github API will have a date instead of the machine hostname." } } }, @@ -230,6 +247,30 @@ } } }, + "autoload-dev": { + "type": "object", + "description": "Description of additional autoload rules for development purpose (eg. a test suite).", + "properties": { + "psr-0": { + "type": "object", + "description": "This is a hash of namespaces (keys) and the directories they can be found into (values, can be arrays of paths) by the autoloader.", + "additionalProperties": true + }, + "psr-4": { + "type": "object", + "description": "This is a hash of namespaces (keys) and the PSR-4 directories they can map to (values, can be arrays of paths) by the autoloader.", + "additionalProperties": true + }, + "classmap": { + "type": "array", + "description": "This is an array of directories that contain classes to be included in the class-map generation process." + }, + "files": { + "type": "array", + "description": "This is an array of files that are always required on every request." + } + } + }, "archive": { "type": ["object"], "description": "Options for creating package archives for distribution.", @@ -247,7 +288,8 @@ }, "minimum-stability": { "type": ["string"], - "description": "The minimum stability the packages must have to be install-able. Possible values are: dev, alpha, beta, RC, stable." + "description": "The minimum stability the packages must have to be install-able. Possible values are: dev, alpha, beta, RC, stable.", + "pattern": "^dev|alpha|beta|rc|RC|stable$" }, "prefer-stable": { "type": ["boolean"], diff --git a/res/spdx-identifier.json b/res/spdx-identifier.json index 8e472637e..b6d8dbc1f 100644 --- a/res/spdx-identifier.json +++ b/res/spdx-identifier.json @@ -1,42 +1,59 @@ [ - "AFL-1.1", "AFL-1.2", "AFL-2.0", "AFL-2.1", "AFL-3.0", "APL-1.0", "Aladdin", - "ANTLR-PD", "Apache-1.0", "Apache-1.1", "Apache-2.0", "APSL-1.0", - "APSL-1.1", "APSL-1.2", "APSL-2.0", "Artistic-1.0", "Artistic-2.0", "AAL", - "BitTorrent-1.0", "BitTorrent-1.1", "BSL-1.0", "BSD-3-Clause-Clear", - "BSD-2-Clause", "BSD-2-Clause-FreeBSD", "BSD-2-Clause-NetBSD", - "BSD-3-Clause", "BSD-4-Clause", "BSD-4-Clause-UC", "CECILL-1.0", - "CECILL-1.1", "CECILL-2.0", "CECILL-B", "CECILL-C", "ClArtistic", - "CNRI-Python", "CNRI-Python-GPL-Compatible", "CDDL-1.0", "CDDL-1.1", - "CPAL-1.0", "CPL-1.0", "CATOSL-1.1", "Condor-1.1", "CC-BY-1.0", "CC-BY-2.0", - "CC-BY-2.5", "CC-BY-3.0", "CC-BY-ND-1.0", "CC-BY-ND-2.0", "CC-BY-ND-2.5", - "CC-BY-ND-3.0", "CC-BY-NC-1.0", "CC-BY-NC-2.0", "CC-BY-NC-2.5", - "CC-BY-NC-3.0", "CC-BY-NC-ND-1.0", "CC-BY-NC-ND-2.0", "CC-BY-NC-ND-2.5", - "CC-BY-NC-ND-3.0", "CC-BY-NC-SA-1.0", "CC-BY-NC-SA-2.0", "CC-BY-NC-SA-2.5", - "CC-BY-NC-SA-3.0", "CC-BY-SA-1.0", "CC-BY-SA-2.0", "CC-BY-SA-2.5", - "CC-BY-SA-3.0", "CC0-1.0", "CUA-OPL-1.0", "WTFPL", "EPL-1.0", "eCos-2.0", - "ECL-1.0", "ECL-2.0", "EFL-1.0", "EFL-2.0", "Entessa", "ErlPL-1.1", - "EUDatagrid", "EUPL-1.0", "EUPL-1.1", "Fair", "Frameworx-1.0", "FTL", - "AGPL-3.0", "GFDL-1.1", "GFDL-1.2", "GFDL-1.3", "GPL-1.0", "GPL-1.0+", - "GPL-2.0", "GPL-2.0+", "GPL-2.0-with-autoconf-exception", + "Glide", "Abstyles", "AFL-1.1", "AFL-1.2", "AFL-2.0", "AFL-2.1", "AFL-3.0", + "AMPAS", "APL-1.0", "Adobe-Glyph", "APAFML", "Adobe-2006", "AGPL-1.0", + "Afmparse", "Aladdin", "ADSL", "AMDPLPA", "ANTLR-PD", "Apache-1.0", + "Apache-1.1", "Apache-2.0", "AML", "APSL-1.0", "APSL-1.1", "APSL-1.2", + "APSL-2.0", "Artistic-1.0", "Artistic-1.0-Perl", "Artistic-1.0-cl8", + "Artistic-2.0", "AAL", "Bahyph", "Barr", "Beerware", "BitTorrent-1.0", + "BitTorrent-1.1", "BSL-1.0", "Borceux", "BSD-2-Clause", + "BSD-2-Clause-FreeBSD", "BSD-2-Clause-NetBSD", "BSD-3-Clause", + "BSD-3-Clause-Clear", "BSD-4-Clause", "BSD-Protection", + "BSD-3-Clause-Attribution", "BSD-4-Clause-UC", "bzip2-1.0.5", "bzip2-1.0.6", + "Caldera", "CECILL-1.0", "CECILL-1.1", "CECILL-2.0", "CECILL-B", "CECILL-C", + "ClArtistic", "MIT-CMU", "CNRI-Python", "CNRI-Python-GPL-Compatible", + "CPOL-1.02", "CDDL-1.0", "CDDL-1.1", "CPAL-1.0", "CPL-1.0", "CATOSL-1.1", + "Condor-1.1", "CC-BY-1.0", "CC-BY-2.0", "CC-BY-2.5", "CC-BY-3.0", + "CC-BY-4.0", "CC-BY-ND-1.0", "CC-BY-ND-2.0", "CC-BY-ND-2.5", "CC-BY-ND-3.0", + "CC-BY-ND-4.0", "CC-BY-NC-1.0", "CC-BY-NC-2.0", "CC-BY-NC-2.5", + "CC-BY-NC-3.0", "CC-BY-NC-4.0", "CC-BY-NC-ND-1.0", "CC-BY-NC-ND-2.0", + "CC-BY-NC-ND-2.5", "CC-BY-NC-ND-3.0", "CC-BY-NC-ND-4.0", "CC-BY-NC-SA-1.0", + "CC-BY-NC-SA-2.0", "CC-BY-NC-SA-2.5", "CC-BY-NC-SA-3.0", "CC-BY-NC-SA-4.0", + "CC-BY-SA-1.0", "CC-BY-SA-2.0", "CC-BY-SA-2.5", "CC-BY-SA-3.0", + "CC-BY-SA-4.0", "CC0-1.0", "Crossword", "CUA-OPL-1.0", "Cube", "D-FSL-1.0", + "diffmark", "WTFPL", "DOC", "Dotseqn", "DSDP", "dvipdfm", "EPL-1.0", + "eCos-2.0", "ECL-1.0", "ECL-2.0", "eGenix", "EFL-1.0", "EFL-2.0", + "MIT-advertising", "MIT-enna", "Entessa", "ErlPL-1.1", "EUDatagrid", + "EUPL-1.0", "EUPL-1.1", "Eurosym", "Fair", "MIT-feh", "Frameworx-1.0", + "FTL", "FSFUL", "FSFULLR", "Giftware", "GL2PS", "Glulxe", "AGPL-3.0", + "GFDL-1.1", "GFDL-1.2", "GFDL-1.3", "GPL-1.0", "GPL-1.0+", "GPL-2.0", + "GPL-2.0+", "GPL-2.0-with-autoconf-exception", "GPL-2.0-with-bison-exception", "GPL-2.0-with-classpath-exception", "GPL-2.0-with-font-exception", "GPL-2.0-with-GCC-exception", "GPL-3.0", "GPL-3.0+", "GPL-3.0-with-autoconf-exception", "GPL-3.0-with-GCC-exception", "LGPL-2.1", "LGPL-2.1+", "LGPL-3.0", "LGPL-3.0+", "LGPL-2.0", "LGPL-2.0+", - "gSOAP-1.3b", "HPND", "IPL-1.0", "Imlib2", "IJG", "Intel", "IPA", "ISC", - "JSON", "LPPL-1.3a", "LPPL-1.0", "LPPL-1.1", "LPPL-1.2", "LPPL-1.3c", - "Libpng", "LPL-1.02", "LPL-1.0", "MS-PL", "MS-RL", "MirOS", "MIT", - "Motosoto", "MPL-1.0", "MPL-1.1", "MPL-2.0", - "MPL-2.0-no-copyleft-exception", "Multics", "NASA-1.3", "Naumen", - "NBPL-1.0", "NGPL", "NOSL", "NPL-1.0", "NPL-1.1", "Nokia", "NPOSL-3.0", - "NTP", "OCLC-2.0", "ODbL-1.0", "PDDL-1.0", "OGTSL", "OLDAP-2.2.2", + "gnuplot", "gSOAP-1.3b", "HaskellReport", "HPND", "IBM-pibs", "IPL-1.0", + "ImageMagick", "iMatix", "Imlib2", "IJG", "Intel-ACPI", "Intel", "IPA", + "ISC", "JasPer-2.0", "JSON", "LPPL-1.3a", "LPPL-1.0", "LPPL-1.1", + "LPPL-1.2", "LPPL-1.3c", "Latex2e", "BSD-3-Clause-LBNL", "Leptonica", + "Libpng", "libtiff", "LPL-1.02", "LPL-1.0", "MakeIndex", "MTLL", "MS-PL", + "MS-RL", "MirOS", "MITNFA", "MIT", "Motosoto", "MPL-1.0", "MPL-1.1", + "MPL-2.0", "MPL-2.0-no-copyleft-exception", "mpich2", "Multics", "Mup", + "NASA-1.3", "Naumen", "NBPL-1.0", "NetCDF", "NGPL", "NOSL", "NPL-1.0", + "NPL-1.1", "Newsletr", "NLPL", "Nokia", "NPOSL-3.0", "Noweb", "NRL", "NTP", + "Nunit", "OCLC-2.0", "ODbL-1.0", "PDDL-1.0", "OGTSL", "OLDAP-2.2.2", "OLDAP-1.1", "OLDAP-1.2", "OLDAP-1.3", "OLDAP-1.4", "OLDAP-2.0", "OLDAP-2.0.1", "OLDAP-2.1", "OLDAP-2.2", "OLDAP-2.2.1", "OLDAP-2.3", - "OLDAP-2.4", "OLDAP-2.5", "OLDAP-2.6", "OLDAP-2.7", "OPL-1.0", "OSL-1.0", - "OSL-2.0", "OSL-2.1", "OSL-3.0", "OLDAP-2.8", "OpenSSL", "PHP-3.0", - "PHP-3.01", "PostgreSQL", "Python-2.0", "QPL-1.0", "RPSL-1.0", "RPL-1.5", - "RHeCos-1.1", "RSCPL", "Ruby", "SAX-PD", "SGI-B-1.0", "SGI-B-1.1", - "SGI-B-2.0", "OFL-1.0", "OFL-1.1", "SimPL-2.0", "Sleepycat", "SMLNJ", - "SugarCRM-1.1.3", "SISSL", "SPL-1.0", "Watcom-1.0", "NCSA", "VSL-1.0", - "W3C", "WXwindows", "Xnet", "X11", "XFree86-1.1", "YPL-1.0", "YPL-1.1", - "Zimbra-1.3", "Zlib", "ZPL-1.1", "ZPL-2.0", "ZPL-2.1" -] \ No newline at end of file + "OLDAP-2.4", "OLDAP-2.5", "OLDAP-2.6", "OLDAP-2.7", "OML", "OPL-1.0", + "OSL-1.0", "OSL-1.1", "OSL-2.0", "OSL-2.1", "OSL-3.0", "OLDAP-2.8", + "OpenSSL", "PHP-3.0", "PHP-3.01", "Plexus", "PostgreSQL", "psfrag", + "psutils", "Python-2.0", "QPL-1.0", "Qhull", "Rdisc", "RPSL-1.0", "RPL-1.1", + "RPL-1.5", "RHeCos-1.1", "RSCPL", "Ruby", "SAX-PD", "Saxpath", "SCEA", + "SWL", "SGI-B-1.0", "SGI-B-1.1", "SGI-B-2.0", "OFL-1.0", "OFL-1.1", + "SimPL-2.0", "Sleepycat", "SNIA", "SMLNJ", "StandardML-NJ", + "SugarCRM-1.1.3", "SISSL", "SISSL-1.2", "SPL-1.0", "Watcom-1.0", "TCL", + "Unlicense", "TMate", "TORQUE-1.1", "TOSL", "Unicode-TOU", "NCSA", "Vim", + "VOSTROM", "VSL-1.0", "W3C", "Wsuipa", "WXwindows", "Xnet", "X11", "Xerox", + "XFree86-1.1", "xinetd", "xpp", "XSkat", "YPL-1.0", "YPL-1.1", "Zed", + "Zend-2.0", "Zimbra-1.3", "Zlib", "zlib-acknowledgement", "ZPL-1.1", + "ZPL-2.0", "ZPL-2.1" +] diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index c65980697..b0ce14481 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -15,6 +15,7 @@ namespace Composer\Autoload; use Composer\Config; use Composer\EventDispatcher\EventDispatcher; use Composer\Installer\InstallationManager; +use Composer\IO\IOInterface; use Composer\Package\AliasPackage; use Composer\Package\PackageInterface; use Composer\Repository\InstalledRepositoryInterface; @@ -34,14 +35,29 @@ class AutoloadGenerator */ private $eventDispatcher; - public function __construct(EventDispatcher $eventDispatcher) + /** + * @var IOInterface + */ + private $io; + + private $devMode = false; + + public function __construct(EventDispatcher $eventDispatcher, IOInterface $io = null) { $this->eventDispatcher = $eventDispatcher; + $this->io = $io; + } + + public function setDevMode($devMode = true) + { + $this->devMode = (boolean) $devMode; } public function dump(Config $config, InstalledRepositoryInterface $localRepo, PackageInterface $mainPackage, InstallationManager $installationManager, $targetDir, $scanPsr0Packages = false, $suffix = '') { - $this->eventDispatcher->dispatchScript(ScriptEvents::PRE_AUTOLOAD_DUMP); + $this->eventDispatcher->dispatchScript(ScriptEvents::PRE_AUTOLOAD_DUMP, $this->devMode, array(), array( + 'optimize' => (bool) $scanPsr0Packages, + )); $filesystem = new Filesystem(); $filesystem->ensureDirectoryExists($config->get('vendor-dir')); @@ -49,6 +65,7 @@ class AutoloadGenerator $vendorPath = $filesystem->normalizePath(realpath($config->get('vendor-dir'))); $useGlobalIncludePath = (bool) $config->get('use-include-path'); $prependAutoloader = $config->get('prepend-autoloader') === false ? 'false' : 'true'; + $classMapAuthoritative = $config->get('classmap-authoritative'); $targetDir = $vendorPath.'/'.$targetDir; $filesystem->ensureDirectoryExists($targetDir); @@ -172,7 +189,22 @@ EOF; if (!is_dir($dir)) { continue; } - foreach (ClassMapGenerator::createMap($dir, $blacklist) as $class => $path) { + $whitelist = sprintf( + '{%s/%s.+(?io, $namespaceFilter) as $class => $path) { + if (!isset($classMap[$class])) { + $path = $this->getPathCode($filesystem, $basePath, $vendorPath, $path); + $classMap[$class] = $path.",\n"; + } + } + /* + * RKERNER + * foreach (ClassMapGenerator::createMap($dir, $blacklist) as $class => $path) { if ('' === $namespace || 0 === strpos($class, $namespace)) { if (!isset($classMap[$class])) { $path = $this->getPathCode($filesystem, $basePath, $vendorPath, $path); @@ -180,14 +212,15 @@ EOF; } } } + */ } } } } - $autoloads['classmap'] = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($autoloads['classmap'])); foreach ($autoloads['classmap'] as $dir) { - foreach (ClassMapGenerator::createMap($dir, $blacklist) as $class => $path) { + foreach (ClassMapGenerator::createMap($dir, null, $this->io) as $class => $path) { + //REKERNER foreach (ClassMapGenerator::createMap($dir, $blacklist) as $class => $path) { $path = $this->getPathCode($filesystem, $basePath, $vendorPath, $path); $classMap[$class] = $path.",\n"; } @@ -213,7 +246,7 @@ EOF; file_put_contents($targetDir.'/autoload_files.php', $includeFilesFile); } file_put_contents($vendorPath.'/autoload.php', $this->getAutoloadFile($vendorPathToTargetDirCode, $suffix)); - file_put_contents($targetDir.'/autoload_real.php', $this->getAutoloadRealFile(true, (bool) $includePathFile, $targetDirLoader, (bool) $includeFilesFile, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader)); + file_put_contents($targetDir.'/autoload_real.php', $this->getAutoloadRealFile(true, (bool) $includePathFile, $targetDirLoader, (bool) $includeFilesFile, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader, $classMapAuthoritative)); // use stream_copy_to_stream instead of copy // to work around https://bugs.php.net/bug.php?id=64634 @@ -224,7 +257,9 @@ EOF; fclose($targetLoader); unset($sourceLoader, $targetLoader); - $this->eventDispatcher->dispatchScript(ScriptEvents::POST_AUTOLOAD_DUMP); + $this->eventDispatcher->dispatchScript(ScriptEvents::POST_AUTOLOAD_DUMP, $this->devMode, array(), array( + 'optimize' => (bool) $scanPsr0Packages, + )); } public function buildPackageMap(InstallationManager $installationManager, PackageInterface $mainPackage, array $packages) @@ -240,7 +275,7 @@ EOF; $packageMap[] = array( $package, - $installationManager->getInstallPath($package) + $installationManager->getInstallPath($package), ); } @@ -370,7 +405,6 @@ EOF; protected function getIncludeFilesFile(array $files, Filesystem $filesystem, $basePath, $vendorPath, $vendorPathCode, $appBaseDirCode) { $filesCode = ''; - $files = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($files)); foreach ($files as $functionFile) { $filesCode .= ' '.$this->getPathCode($filesystem, $basePath, $vendorPath, $functionFile).",\n"; } @@ -437,7 +471,7 @@ return ComposerAutoloaderInit$suffix::getLoader(); AUTOLOAD; } - protected function getAutoloadRealFile($useClassMap, $useIncludePath, $targetDirLoader, $useIncludeFiles, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader) + protected function getAutoloadRealFile($useClassMap, $useIncludePath, $targetDirLoader, $useIncludeFiles, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader, $classMapAuthoritative) { // TODO the class ComposerAutoloaderInit should be revert to a closure // when APC has been fixed: @@ -472,9 +506,6 @@ class ComposerAutoloaderInit$suffix self::\$loader = \$loader = new \\Composer\\Autoload\\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInit$suffix', 'loadClassLoader')); - \$vendorDir = $vendorPathCode; - \$baseDir = $appBaseDirCode; - HEADER; @@ -517,6 +548,13 @@ PSR4; CLASSMAP; } + if ($classMapAuthoritative) { + $file .= <<<'CLASSMAPAUTHORITATIVE' + $loader->setClassMapAuthoritative(true); + +CLASSMAPAUTHORITATIVE; + } + if ($useGlobalIncludePath) { $file .= <<<'INCLUDEPATH' $loader->setUseIncludePath(true); @@ -530,7 +568,6 @@ INCLUDEPATH; REGISTER_AUTOLOAD; - } $file .= <<getAutoload(); + if ($this->devMode && $package === $mainPackage) { + $autoload = array_merge_recursive($autoload, $package->getDevAutoload()); + } // skip misconfigured packages if (!isset($autoload[$type]) || !is_array($autoload[$type])) { @@ -585,24 +628,19 @@ FOOTER; foreach ($autoload[$type] as $namespace => $paths) { foreach ((array) $paths as $path) { - // remove target-dir from file paths of the root package - if ($type === 'files' && $package === $mainPackage && $package->getTargetDir() && !is_readable($installPath.'/'.$path)) { - $targetDir = str_replace('\\', '[\\\\/]', preg_quote(str_replace(array('/', '\\'), '', $package->getTargetDir()))); - $path = ltrim(preg_replace('{^'.$targetDir.'}', '', ltrim($path, '\\/')), '\\/'); - } - - // add target-dir from file paths that don't have it - if ($type === 'files' && $package !== $mainPackage && $package->getTargetDir() && !is_readable($installPath.'/'.$path)) { - $path = $package->getTargetDir() . '/' . $path; - } - - // remove target-dir from classmap entries of the root package - if ($type === 'classmap' && $package === $mainPackage && $package->getTargetDir() && !is_readable($installPath.'/'.$path)) { - $targetDir = str_replace('\\', '[\\\\/]', preg_quote(str_replace(array('/', '\\'), '', $package->getTargetDir()))); - $path = ltrim(preg_replace('{^'.$targetDir.'}', '', ltrim($path, '\\/')), '\\/'); + if (($type === 'files' || $type === 'classmap') && $package->getTargetDir() && !is_readable($installPath.'/'.$path)) { + // remove target-dir from file paths of the root package + if ($package === $mainPackage) { + $targetDir = str_replace('\\', '[\\\\/]', preg_quote(str_replace(array('/', '\\'), '', $package->getTargetDir()))); + $path = ltrim(preg_replace('{^'.$targetDir.'}', '', ltrim($path, '\\/')), '\\/'); + } else { + // add target-dir from file paths that don't have it + $path = $package->getTargetDir() . '/' . $path; + } } - - // add target-dir to classmap entries that don't have it + + /* RKERNER + * // add target-dir to classmap entries that don't have it if ($type === 'classmap' && $package !== $mainPackage && $package->getTargetDir() && !is_readable($installPath.'/'.$path)) { $path = $package->getTargetDir() . '/' . $path; } @@ -629,6 +667,16 @@ FOOTER; } else { $autoloads[$namespace][] = $installPath.'/'.$path; } + */ + + $relativePath = empty($installPath) ? (empty($path) ? '.' : $path) : $installPath.'/'.$path; + + if ($type === 'files' || $type === 'classmap') { + $autoloads[] = $relativePath; + continue; + } + + $autoloads[$namespace][] = $relativePath; } } } @@ -636,47 +684,93 @@ FOOTER; return $autoloads; } + /** + * Sorts packages by dependency weight + * + * Packages of equal weight retain the original order + * + * @param array $packageMap + * @return array + */ protected function sortPackageMap(array $packageMap) { - $positions = array(); - $names = array(); - $indexes = array(); - - foreach ($packageMap as $position => $item) { - $mainName = $item[0]->getName(); - $names = array_merge(array_fill_keys($item[0]->getNames(), $mainName), $names); - $names[$mainName] = $mainName; - $indexes[$mainName] = $positions[$mainName] = $position; - } + $packages = array(); + $paths = array(); + $usageList = array(); foreach ($packageMap as $item) { - $position = $positions[$item[0]->getName()]; - foreach (array_merge($item[0]->getRequires(), $item[0]->getDevRequires()) as $link) { + list($package, $path) = $item; + $name = $package->getName(); + $packages[$name] = $package; + $paths[$name] = $path; + + foreach (array_merge($package->getRequires(), $package->getDevRequires()) as $link) { $target = $link->getTarget(); - if (!isset($names[$target])) { - continue; - } + $usageList[$target][] = $name; + } + } - $target = $names[$target]; - if ($positions[$target] <= $position) { - continue; - } + $computing = array(); + $computed = array(); + $computeImportance = function ($name) use (&$computeImportance, &$computing, &$computed, $usageList) { + // reusing computed importance + if (isset($computed[$name])) { + return $computed[$name]; + } - foreach ($positions as $key => $value) { - if ($value >= $position) { - break; - } - $positions[$key]--; - } + // canceling circular dependency + if (isset($computing[$name])) { + return 0; + } - $positions[$target] = $position - 1; + $computing[$name] = true; + $weight = 0; + + if (isset($usageList[$name])) { + foreach ($usageList[$name] as $user) { + $weight -= 1 - $computeImportance($user); + } } - asort($positions); + + unset($computing[$name]); + $computed[$name] = $weight; + + return $weight; + }; + + $weightList = array(); + + foreach ($packages as $name => $package) { + $weight = $computeImportance($name); + $weightList[$name] = $weight; } + $stable_sort = function (&$array) { + static $transform, $restore; + + $i = 0; + + if (!$transform) { + $transform = function (&$v, $k) use (&$i) { + $v = array($v, ++$i, $k, $v); + }; + + $restore = function (&$v, $k) { + $v = $v[3]; + }; + } + + array_walk($array, $transform); + asort($array); + array_walk($array, $restore); + }; + + $stable_sort($weightList); + $sortedPackageMap = array(); - foreach (array_keys($positions) as $packageName) { - $sortedPackageMap[] = $packageMap[$indexes[$packageName]]; + + foreach (array_keys($weightList) as $name) { + $sortedPackageMap[] = array($packages[$name], $paths[$name]); } return $sortedPackageMap; diff --git a/src/Composer/Autoload/ClassLoader.php b/src/Composer/Autoload/ClassLoader.php index f438e319c..5e1469e83 100644 --- a/src/Composer/Autoload/ClassLoader.php +++ b/src/Composer/Autoload/ClassLoader.php @@ -54,9 +54,15 @@ class ClassLoader private $useIncludePath = false; private $classMap = array(); + private $classMapAuthoritative = false; + public function getPrefixes() { - return call_user_func_array('array_merge', $this->prefixesPsr0); + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); } public function getPrefixesPsr4() @@ -143,6 +149,8 @@ class ClassLoader * @param string $prefix The prefix/namespace, with trailing '\\' * @param array|string $paths The PSR-0 base directories * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException */ public function addPsr4($prefix, $paths, $prepend = false) { @@ -202,10 +210,13 @@ class ClassLoader * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param array|string $paths The PSR-4 base directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException */ - public function setPsr4($prefix, $paths) { + public function setPsr4($prefix, $paths) + { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { @@ -239,6 +250,27 @@ class ClassLoader return $this->useIncludePath; } + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + /** * Registers this instance as an autoloader. * @@ -266,7 +298,7 @@ class ClassLoader public function loadClass($class) { if ($file = $this->findFile($class)) { - include $file; + includeFile($file); return true; } @@ -290,9 +322,29 @@ class ClassLoader if (isset($this->classMap[$class])) { return $this->classMap[$class]; } + if ($this->classMapAuthoritative) { + return false; + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if ($file === null && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if ($file === null) { + // Remember that this class does not exist. + return $this->classMap[$class] = false; + } + + return $file; + } + private function findFileWithExtension($class, $ext) + { // PSR-4 lookup - $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . '.php'; + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { @@ -321,7 +373,7 @@ class ClassLoader . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name - $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . '.php'; + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { @@ -347,8 +399,15 @@ class ClassLoader if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } - - // Remember that this class does not exist. - return $this->classMap[$class] = false; } } + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/src/Composer/Autoload/ClassMapGenerator.php b/src/Composer/Autoload/ClassMapGenerator.php index 5d45eb482..80e1fe8b4 100644 --- a/src/Composer/Autoload/ClassMapGenerator.php +++ b/src/Composer/Autoload/ClassMapGenerator.php @@ -12,20 +12,23 @@ */ namespace Composer\Autoload; + use Symfony\Component\Finder\Finder; +use Composer\IO\IOInterface; /** * ClassMapGenerator * * @author Gyula Sallai + * @author Jordi Boggiano */ class ClassMapGenerator { /** * Generate a class map file * - * @param Traversable $dirs Directories or a single path to search in - * @param string $file The name of the class map file + * @param \Traversable $dirs Directories or a single path to search in + * @param string $file The name of the class map file */ public static function dump($dirs, $file) { @@ -41,20 +44,23 @@ class ClassMapGenerator /** * Iterate over all files in the given directory searching for classes * - * @param Iterator|string $path The path to search in or an iterator - * @param string $blacklist Regex that matches against the file path that exclude from the classmap. + * @param \Iterator|string $path The path to search in or an iterator + * @param string $whitelist Regex that matches against the file path + * @param IOInterface $io IO object + * @param string $namespace Optional namespace prefix to filter by * * @return array A class map array * * @throws \RuntimeException When the path is neither an existing file nor directory */ - public static function createMap($path, $blacklist = '') + public static function createMap($path, $whitelist = null, IOInterface $io = null, $namespace = null) + // RKERNER: public static function createMap($path, $blacklist = '') { if (is_string($path)) { if (is_file($path)) { $path = array(new \SplFileInfo($path)); } elseif (is_dir($path)) { - $path = Finder::create()->files()->followLinks()->name('/\.(php|inc)$/')->in($path); + $path = Finder::create()->files()->followLinks()->name('/\.(php|inc|hh)$/')->in($path); } else { throw new \RuntimeException( 'Could not scan for classes inside "'.$path. @@ -68,18 +74,31 @@ class ClassMapGenerator foreach ($path as $file) { $filePath = $file->getRealPath(); - if (!in_array(pathinfo($filePath, PATHINFO_EXTENSION), array('php', 'inc'))) { + if (!in_array(pathinfo($filePath, PATHINFO_EXTENSION), array('php', 'inc', 'hh'))) { continue; } - if ($blacklist && preg_match($blacklist, strtr($filePath, '\\', '/'))) { + if ($whitelist && !preg_match($whitelist, strtr($filePath, '\\', '/'))) { + // RKERNER: if ($blacklist && preg_match($blacklist, strtr($filePath, '\\', '/'))) { continue; } $classes = self::findClasses($filePath); foreach ($classes as $class) { - $map[$class] = $filePath; + // skip classes not within the given namespace prefix + if (null !== $namespace && 0 !== strpos($class, $namespace)) { + continue; + } + + if (!isset($map[$class])) { + $map[$class] = $filePath; + } elseif ($io && $map[$class] !== $filePath && !preg_match('{/(test|fixture|example)s?/}i', strtr($map[$class].' '.$filePath, '\\', '/'))) { + $io->write( + 'Warning: Ambiguous class resolution, "'.$class.'"'. + ' was found in both "'.$map[$class].'" and "'.$filePath.'", the first will be used.' + ); + } } } @@ -98,7 +117,15 @@ class ClassMapGenerator $traits = version_compare(PHP_VERSION, '5.4', '<') ? '' : '|trait'; try { - $contents = php_strip_whitespace($path); + $contents = @php_strip_whitespace($path); + if (!$contents) { + if (!file_exists($path)) { + throw new \Exception('File does not exist'); + } + if (!is_readable($path)) { + throw new \Exception('File is not readable'); + } + } } catch (\Exception $e) { throw new \RuntimeException('Could not scan for classes inside '.$path.": \n".$e->getMessage(), 0, $e); } @@ -109,12 +136,15 @@ class ClassMapGenerator } // strip heredocs/nowdocs - $contents = preg_replace('{<<<\'?(\w+)\'?(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\1(?=\r\n|\n|\r|;)}s', 'null', $contents); + $contents = preg_replace('{<<<\s*(\'?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\2(?=\r\n|\n|\r|;)}s', 'null', $contents); // strip strings $contents = preg_replace('{"[^"\\\\]*(\\\\.[^"\\\\]*)*"|\'[^\'\\\\]*(\\\\.[^\'\\\\]*)*\'}s', 'null', $contents); // strip leading non-php code if needed if (substr($contents, 0, 2) !== '.+<\?}s', '?>])(?Pclass|interface'.$traits.') \s+ (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*) + \b(?])(?Pclass|interface'.$traits.') \s+ (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*) | \b(?])(?Pnamespace) (?P\s+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\s*\\\\\s*[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*)? \s*[\{;] ) }ix', $contents, $matches); @@ -138,7 +168,12 @@ class ClassMapGenerator if (!empty($matches['ns'][$i])) { $namespace = str_replace(array(' ', "\t", "\r", "\n"), '', $matches['nsname'][$i]) . '\\'; } else { - $classes[] = ltrim($namespace . $matches['name'][$i], '\\'); + $name = $matches['name'][$i]; + if ($name[0] === ':') { + // This is an XHP class, https://github.com/facebook/xhp + $name = 'xhp'.substr(str_replace(array('-', ':'), array('_', '__'), $name), 1); + } + $classes[] = ltrim($namespace . $name, '\\'); } } diff --git a/src/Composer/Cache.php b/src/Composer/Cache.php index 6bdf43d5d..7341f61c2 100644 --- a/src/Composer/Cache.php +++ b/src/Composer/Cache.php @@ -83,7 +83,28 @@ class Cache $this->io->write('Writing '.$this->root . $file.' into cache'); } - return file_put_contents($this->root . $file, $contents); + try { + return file_put_contents($this->root . $file, $contents); + } catch (\ErrorException $e) { + if (preg_match('{^file_put_contents\(\): Only ([0-9]+) of ([0-9]+) bytes written}', $e->getMessage(), $m)) { + // Remove partial file. + unlink($this->root . $file); + + $message = sprintf( + 'Writing %1$s into cache failed after %2$u of %3$u bytes written, only %4$u bytes of free space available', + $this->root . $file, + $m[1], + $m[2], + @disk_free_space($this->root . dirname($file)) + ); + + $this->io->write($message); + + return false; + } + + throw $e; + } } return false; @@ -129,14 +150,14 @@ class Cache public function gcIsNecessary() { - return (!self::$cacheCollected && !mt_rand(0, 50)); + return (!self::$cacheCollected && !mt_rand(0, 50)); } public function remove($file) { $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); if ($this->enabled && file_exists($this->root . $file)) { - return unlink($this->root . $file); + return $this->filesystem->unlink($this->root . $file); } return false; @@ -144,28 +165,32 @@ class Cache public function gc($ttl, $maxSize) { - $expire = new \DateTime(); - $expire->modify('-'.$ttl.' seconds'); + if ($this->enabled) { + $expire = new \DateTime(); + $expire->modify('-'.$ttl.' seconds'); - $finder = $this->getFinder()->date('until '.$expire->format('Y-m-d H:i:s')); - foreach ($finder as $file) { - unlink($file->getRealPath()); - } + $finder = $this->getFinder()->date('until '.$expire->format('Y-m-d H:i:s')); + foreach ($finder as $file) { + $this->filesystem->unlink($file->getPathname()); + } - $totalSize = $this->filesystem->size($this->root); - if ($totalSize > $maxSize) { - $iterator = $this->getFinder()->sortByAccessedTime()->getIterator(); - while ($totalSize > $maxSize && $iterator->valid()) { - $filepath = $iterator->current()->getRealPath(); - $totalSize -= $this->filesystem->size($filepath); - unlink($filepath); - $iterator->next(); + $totalSize = $this->filesystem->size($this->root); + if ($totalSize > $maxSize) { + $iterator = $this->getFinder()->sortByAccessedTime()->getIterator(); + while ($totalSize > $maxSize && $iterator->valid()) { + $filepath = $iterator->current()->getPathname(); + $totalSize -= $this->filesystem->size($filepath); + $this->filesystem->unlink($filepath); + $iterator->next(); + } } - } - self::$cacheCollected = true; + self::$cacheCollected = true; + + return true; + } - return true; + return false; } public function sha1($file) diff --git a/src/Composer/Command/AboutCommand.php b/src/Composer/Command/AboutCommand.php index da0fb7fbe..ead7604df 100644 --- a/src/Composer/Command/AboutCommand.php +++ b/src/Composer/Command/AboutCommand.php @@ -40,6 +40,5 @@ EOT See http://getcomposer.org/ for more information. EOT ); - } } diff --git a/src/Composer/Command/ArchiveCommand.php b/src/Composer/Command/ArchiveCommand.php index 03e8600fa..913a56a42 100644 --- a/src/Composer/Command/ArchiveCommand.php +++ b/src/Composer/Command/ArchiveCommand.php @@ -15,8 +15,11 @@ namespace Composer\Command; use Composer\Factory; use Composer\IO\IOInterface; use Composer\DependencyResolver\Pool; -use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Repository\CompositeRepository; +use Composer\Script\ScriptEvents; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; +use Composer\Package\Version\VersionParser; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -37,7 +40,7 @@ class ArchiveCommand extends Command ->setDescription('Create an archive of this composer package') ->setDefinition(array( new InputArgument('package', InputArgument::OPTIONAL, 'The package to archive instead of the current project'), - new InputArgument('version', InputArgument::OPTIONAL, 'The package version to archive'), + new InputArgument('version', InputArgument::OPTIONAL, 'A version constraint to find the package to archive'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the resulting archive: tar or zip', 'tar'), new InputOption('dir', false, InputOption::VALUE_REQUIRED, 'Write the archive to this directory', '.'), )) @@ -55,13 +58,26 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { - return $this->archive( + $composer = $this->getComposer(false); + if ($composer) { + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'archive', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $composer->getEventDispatcher()->dispatchScript(ScriptEvents::PRE_ARCHIVE_CMD); + } + + $returnCode = $this->archive( $this->getIO(), $input->getArgument('package'), $input->getArgument('version'), $input->getOption('format'), $input->getOption('dir') ); + + if (0 === $returnCode && $composer) { + $composer->getEventDispatcher()->dispatchScript(ScriptEvents::POST_ARCHIVE_CMD); + } + + return $returnCode; } protected function archive(IOInterface $io, $packageName = null, $version = null, $format = 'tar', $dest = '.') @@ -103,16 +119,17 @@ EOT $pool = new Pool(); $pool->addRepository($repos); - $constraint = ($version) ? new VersionConstraint('>=', $version) : null; - $packages = $pool->whatProvides($packageName, $constraint); + $parser = new VersionParser(); + $constraint = ($version) ? $parser->parseConstraints($version) : null; + $packages = $pool->whatProvides($packageName, $constraint, true); if (count($packages) > 1) { - $package = $packages[0]; + $package = reset($packages); $io->write('Found multiple matches, selected '.$package->getPrettyString().'.'); $io->write('Alternatives were '.implode(', ', array_map(function ($p) { return $p->getPrettyString(); }, $packages)).'.'); $io->write('Please use a more specific constraint to pick a different package.'); } elseif ($packages) { - $package = $packages[0]; + $package = reset($packages); $io->write('Found an exact match '.$package->getPrettyString().'.'); } else { $io->write('Could not find a package matching '.$packageName.'.'); diff --git a/src/Composer/Command/ClearCacheCommand.php b/src/Composer/Command/ClearCacheCommand.php new file mode 100644 index 000000000..b1b9ecd9a --- /dev/null +++ b/src/Composer/Command/ClearCacheCommand.php @@ -0,0 +1,71 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Command; + +use Composer\Cache; +use Composer\Factory; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author David Neilsen + */ +class ClearCacheCommand extends Command +{ + protected function configure() + { + $this + ->setName('clear-cache') + ->setAliases(array('clearcache')) + ->setDescription('Clears composer\'s internal package cache.') + ->setHelp(<<clear-cache deletes all cached packages from composer's +cache directory. +EOT + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $config = Factory::createConfig(); + $io = $this->getIO(); + + $cachePaths = array( + 'cache-dir' => $config->get('cache-dir'), + 'cache-files-dir' => $config->get('cache-files-dir'), + 'cache-repo-dir' => $config->get('cache-repo-dir'), + 'cache-vcs-dir' => $config->get('cache-vcs-dir'), + ); + + foreach ($cachePaths as $key => $cachePath) { + $cachePath = realpath($cachePath); + if (!$cachePath) { + $io->write("Cache directory does not exist ($key): $cachePath"); + + return; + } + $cache = new Cache($io, $cachePath); + if (!$cache->isEnabled()) { + $io->write("Cache is not enabled ($key): $cachePath"); + + return; + } + + $io->write("Clearing cache ($key): $cachePath"); + $cache->gc(0, 0); + } + + $io->write('All caches cleared.'); + } +} diff --git a/src/Composer/Command/Command.php b/src/Composer/Command/Command.php index 862b54e58..6c5226c6a 100644 --- a/src/Composer/Command/Command.php +++ b/src/Composer/Command/Command.php @@ -16,6 +16,8 @@ use Composer\Composer; use Composer\Console\Application; use Composer\IO\IOInterface; use Composer\IO\NullIO; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Command\Command as BaseCommand; /** @@ -68,6 +70,15 @@ abstract class Command extends BaseCommand $this->composer = $composer; } + /** + * Removes the cached composer instance + */ + public function resetComposer() + { + $this->composer = null; + $this->getApplication()->resetComposer(); + } + /** * @return IOInterface */ @@ -93,4 +104,16 @@ abstract class Command extends BaseCommand { $this->io = $io; } + + /** + * {@inheritDoc} + */ + protected function initialize(InputInterface $input, OutputInterface $output) + { + if (true === $input->hasParameterOption(array('--no-ansi')) && $input->hasOption('no-progress')) { + $input->setOption('no-progress', true); + } + + parent::initialize($input, $output); + } } diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index bcd3beeab..e21c99c8a 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -53,9 +53,11 @@ class ConfigCommand extends Command ->setDefinition(array( new InputOption('global', 'g', InputOption::VALUE_NONE, 'Apply command to the global config file'), new InputOption('editor', 'e', InputOption::VALUE_NONE, 'Open editor'), + new InputOption('auth', 'a', InputOption::VALUE_NONE, 'Affect auth config file (only used for --editor)'), new InputOption('unset', null, InputOption::VALUE_NONE, 'Unset the given setting-key'), new InputOption('list', 'l', InputOption::VALUE_NONE, 'List configuration settings'), new InputOption('file', 'f', InputOption::VALUE_REQUIRED, 'If you want to choose a different composer.json or config.json', 'composer.json'), + new InputOption('absolute', null, InputOption::VALUE_NONE, 'Returns absolute paths when fetching *-dir config values instead of relative'), new InputArgument('setting-key', null, 'Setting key'), new InputArgument('setting-value', InputArgument::IS_ARRAY, 'Setting value'), )) @@ -98,11 +100,13 @@ EOT */ protected function initialize(InputInterface $input, OutputInterface $output) { + parent::initialize($input, $output); + if ($input->getOption('global') && 'composer.json' !== $input->getOption('file')) { throw new \RuntimeException('--file and --global can not be combined'); } - $this->config = Factory::createConfig(); + $this->config = Factory::createConfig($this->getIO()); // Get the local composer.json, global config.json, or if the user // passed in a file to use @@ -113,15 +117,27 @@ EOT $this->configFile = new JsonFile($configFile); $this->configSource = new JsonConfigSource($this->configFile); + $authConfigFile = $input->getOption('global') + ? ($this->config->get('home') . '/auth.json') + : dirname(realpath($input->getOption('file'))) . '/auth.json'; + + $this->authConfigFile = new JsonFile($authConfigFile); + $this->authConfigSource = new JsonConfigSource($this->authConfigFile, true); + // initialize the global file if it's not there if ($input->getOption('global') && !$this->configFile->exists()) { touch($this->configFile->getPath()); $this->configFile->write(array('config' => new \ArrayObject)); @chmod($this->configFile->getPath(), 0600); } + if ($input->getOption('global') && !$this->authConfigFile->exists()) { + touch($this->authConfigFile->getPath()); + $this->authConfigFile->write(array('http-basic' => new \ArrayObject, 'github-oauth' => new \ArrayObject)); + @chmod($this->authConfigFile->getPath(), 0600); + } if (!$this->configFile->exists()) { - throw new \RuntimeException('No composer.json found in the current directory'); + throw new \RuntimeException(sprintf('File "%s" cannot be found in the current directory', $configFile)); } } @@ -146,13 +162,15 @@ EOT } } - system($editor . ' ' . $this->configFile->getPath() . (defined('PHP_WINDOWS_VERSION_BUILD') ? '': ' > `tty`')); + $file = $input->getOption('auth') ? $this->authConfigFile->getPath() : $this->configFile->getPath(); + system($editor . ' ' . $file . (defined('PHP_WINDOWS_VERSION_BUILD') ? '' : ' > `tty`')); return 0; } if (!$input->getOption('global')) { $this->config->merge($this->configFile->read()); + $this->config->merge(array('config' => $this->authConfigFile->exists() ? $this->authConfigFile->read() : array())); } // List the configuration of the file settings @@ -203,7 +221,7 @@ EOT $value = $data; } elseif (isset($data['config'][$settingKey])) { - $value = $data['config'][$settingKey]; + $value = $this->config->get($settingKey, $input->getOption('absolute') ? 0 : Config::RELATIVE_PATHS); } else { throw new \RuntimeException($settingKey.' is not defined'); } @@ -236,16 +254,29 @@ EOT } // handle github-oauth - if (preg_match('/^github-oauth\.(.+)/', $settingKey, $matches)) { + if (preg_match('/^(github-oauth|http-basic)\.(.+)/', $settingKey, $matches)) { if ($input->getOption('unset')) { - return $this->configSource->removeConfigSetting('github-oauth.'.$matches[1]); + $this->authConfigSource->removeConfigSetting($matches[1].'.'.$matches[2]); + $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]); + + return; } - if (1 !== count($values)) { - throw new \RuntimeException('Too many arguments, expected only one token'); + if ($matches[1] === 'github-oauth') { + if (1 !== count($values)) { + throw new \RuntimeException('Too many arguments, expected only one token'); + } + $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]); + $this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], $values[0]); + } elseif ($matches[1] === 'http-basic') { + if (2 !== count($values)) { + throw new \RuntimeException('Expected two arguments (username, password), got '.count($values)); + } + $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]); + $this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], array('username' => $values[0], 'password' => $values[1])); } - return $this->configSource->addConfigSetting('github-oauth.'.$matches[1], $values[0]); + return; } $booleanValidator = function ($val) { return in_array($val, array('true', 'false', '1', '0'), true); }; @@ -259,6 +290,16 @@ EOT function ($val) { return in_array($val, array('auto', 'source', 'dist'), true); }, function ($val) { return $val; } ), + 'store-auths' => array( + function ($val) { return in_array($val, array('true', 'false', 'prompt'), true); }, + function ($val) { + if ('prompt' === $val) { + return 'prompt'; + } + + return $val !== 'false' && (bool) $val; + } + ), 'notify-on-install' => array($booleanValidator, $booleanNormalizer), 'vendor-dir' => array('is_string', function ($val) { return $val; }), 'bin-dir' => array('is_string', function ($val) { return $val; }), @@ -284,7 +325,9 @@ EOT ), 'autoloader-suffix' => array('is_string', function ($val) { return $val === 'null' ? null : $val; }), 'optimize-autoloader' => array($booleanValidator, $booleanNormalizer), + 'classmap-authoritative' => array($booleanValidator, $booleanNormalizer), 'prepend-autoloader' => array($booleanValidator, $booleanNormalizer), + 'github-expose-hostname' => array($booleanValidator, $booleanNormalizer), ); $multiConfigValues = array( 'github-protocols' => array( @@ -294,8 +337,8 @@ EOT } foreach ($vals as $val) { - if (!in_array($val, array('git', 'https'))) { - return 'valid protocols include: git, https'; + if (!in_array($val, array('git', 'https', 'ssh'))) { + return 'valid protocols include: git, https, ssh'; } } @@ -320,7 +363,7 @@ EOT ); foreach ($uniqueConfigValues as $name => $callbacks) { - if ($settingKey === $name) { + if ($settingKey === $name) { if ($input->getOption('unset')) { return $this->configSource->removeConfigSetting($settingKey); } diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 5aab270ce..0242afcd3 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -19,9 +19,9 @@ use Composer\Installer\ProjectInstaller; use Composer\Installer\InstallationManager; use Composer\IO\IOInterface; use Composer\Package\BasePackage; -use Composer\Package\LinkConstraint\VersionConstraint; use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\Package\Version\VersionSelector; use Composer\Repository\ComposerRepository; use Composer\Repository\CompositeRepository; use Composer\Repository\FilesystemRepository; @@ -56,7 +56,7 @@ class CreateProjectCommand extends Command ->setDefinition(array( new InputArgument('package', InputArgument::OPTIONAL, 'Package name to be installed'), new InputArgument('directory', InputArgument::OPTIONAL, 'Directory where the files should be created'), - new InputArgument('version', InputArgument::OPTIONAL, 'Version, will defaults to latest'), + new InputArgument('version', InputArgument::OPTIONAL, 'Version, will default to latest'), new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum-stability allowed (unless a version is specified).'), 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.'), @@ -69,6 +69,7 @@ class CreateProjectCommand extends Command new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('keep-vcs', null, InputOption::VALUE_NONE, 'Whether to prevent deletion vcs folder.'), new InputOption('no-install', null, InputOption::VALUE_NONE, 'Whether to skip installation of the package dependencies.'), + new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'), )) ->setHelp(<<create-project command creates a new project from a given @@ -102,22 +103,7 @@ EOT $preferSource = false; $preferDist = false; - switch ($config->get('preferred-install')) { - case 'source': - $preferSource = true; - break; - case 'dist': - $preferDist = true; - break; - case 'auto': - default: - // noop - break; - } - if ($input->getOption('prefer-source') || $input->getOption('prefer-dist')) { - $preferSource = $input->getOption('prefer-source'); - $preferDist = $input->getOption('prefer-dist'); - } + $this->updatePreferredOptions($config, $input, $preferSource, $preferDist); if ($input->getOption('no-custom-installers')) { $output->writeln('You are using the deprecated option "no-custom-installers". Use "no-plugins" instead.'); @@ -139,14 +125,19 @@ EOT $input->getOption('no-scripts'), $input->getOption('keep-vcs'), $input->getOption('no-progress'), - $input->getOption('no-install') + $input->getOption('no-install'), + $input->getOption('ignore-platform-reqs'), + $input ); } - public function installProject(IOInterface $io, $config, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repositoryUrl = null, $disablePlugins = false, $noScripts = false, $keepVcs = false, $noProgress = false, $noInstall = false) + public function installProject(IOInterface $io, Config $config, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repositoryUrl = null, $disablePlugins = false, $noScripts = false, $keepVcs = false, $noProgress = false, $noInstall = false, $ignorePlatformReqs = false, InputInterface $input) { $oldCwd = getcwd(); + // we need to manually load the configuration to pass the auth credentials to the io interface! + $io->loadConfiguration($config); + if ($packageName !== null) { $installedFromVcs = $this->installRootPackage($io, $config, $packageName, $directory, $packageVersion, $stability, $preferSource, $preferDist, $installDevPackages, $repositoryUrl, $disablePlugins, $noScripts, $keepVcs, $noProgress); } else { @@ -161,13 +152,17 @@ EOT $composer->getEventDispatcher()->dispatchCommandEvent(ScriptEvents::POST_ROOT_PACKAGE_INSTALL, $installDevPackages); } + $rootPackageConfig = $composer->getConfig(); + $this->updatePreferredOptions($rootPackageConfig, $input, $preferSource, $preferDist); + // install dependencies of the created project if ($noInstall === false) { $installer = Installer::create($io, $composer); $installer->setPreferSource($preferSource) ->setPreferDist($preferDist) ->setDevMode($installDevPackages) - ->setRunScripts( ! $noScripts); + ->setRunScripts(!$noScripts) + ->setIgnorePlatformRequirements($ignorePlatformReqs); if ($disablePlugins) { $installer->disablePlugins(); @@ -238,12 +233,18 @@ EOT return 0; } - protected function installRootPackage(IOInterface $io, $config, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repositoryUrl = null, $disablePlugins = false, $noScripts = false, $keepVcs = false, $noProgress = false) + protected function installRootPackage(IOInterface $io, Config $config, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repositoryUrl = null, $disablePlugins = false, $noScripts = false, $keepVcs = false, $noProgress = false) { if (null === $repositoryUrl) { $sourceRepo = new CompositeRepository(Factory::createDefaultRepositories($io, $config)); - } elseif ("json" === pathinfo($repositoryUrl, PATHINFO_EXTENSION)) { - $sourceRepo = new FilesystemRepository(new JsonFile($repositoryUrl, new RemoteFilesystem($io))); + } elseif ("json" === pathinfo($repositoryUrl, PATHINFO_EXTENSION) && file_exists($repositoryUrl)) { + $json = new JsonFile($repositoryUrl, new RemoteFilesystem($io, $config)); + $data = $json->read(); + if (!empty($data['packages']) || !empty($data['includes']) || !empty($data['provider-includes'])) { + $sourceRepo = new ComposerRepository(array('url' => 'file://' . strtr(realpath($repositoryUrl), '\\', '/')), $io, $config); + } else { + $sourceRepo = new FilesystemRepository($json); + } } elseif (0 === strpos($repositoryUrl, 'http')) { $sourceRepo = new ComposerRepository(array('url' => $repositoryUrl), $io, $config); } else { @@ -251,7 +252,6 @@ EOT } $parser = new VersionParser(); - $candidates = array(); $requirements = $parser->parseNameVersionPairs(array($packageName)); $name = strtolower($requirements[0]['name']); if (!$packageVersion && isset($requirements[0]['version'])) { @@ -275,15 +275,11 @@ EOT $pool = new Pool($stability); $pool->addRepository($sourceRepo); - $constraint = $packageVersion ? $parser->parseConstraints($packageVersion) : null; - $candidates = $pool->whatProvides($name, $constraint); - foreach ($candidates as $key => $candidate) { - if ($candidate->getName() !== $name) { - unset($candidates[$key]); - } - } + // find the latest version if there are multiple + $versionSelector = new VersionSelector($pool); + $package = $versionSelector->findBestCandidate($name, $packageVersion); - if (!$candidates) { + if (!$package) { throw new \InvalidArgumentException("Could not find package $name" . ($packageVersion ? " with version $packageVersion." : " with stability $stability.")); } @@ -292,15 +288,6 @@ EOT $directory = getcwd() . DIRECTORY_SEPARATOR . array_pop($parts); } - // select highest version if we have many - $package = reset($candidates); - foreach ($candidates as $candidate) { - if (version_compare($package->getVersion(), $candidate->getVersion(), '<')) { - $package = $candidate; - } - } - unset($candidates); - $io->write('Installing ' . $package->getName() . ' (' . VersionParser::formatVersion($package, false) . ')'); if ($disablePlugins) { @@ -343,4 +330,34 @@ EOT { return new InstallationManager(); } + + /** + * Updated preferSource or preferDist based on the preferredInstall config option + * @param Config $config + * @param InputInterface $input + * @param boolean $preferSource + * @param boolean $preferDist + */ + protected function updatePreferredOptions(Config $config, InputInterface $input, &$preferSource, &$preferDist) + { + switch ($config->get('preferred-install')) { + case 'source': + $preferSource = true; + $preferDist = false; + break; + case 'dist': + $preferSource = false; + $preferDist = true; + break; + case 'auto': + default: + // noop + break; + } + + if ($input->getOption('prefer-source') || $input->getOption('prefer-dist')) { + $preferSource = $input->getOption('prefer-source'); + $preferDist = $input->getOption('prefer-dist'); + } + } } diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 445bcc1aa..b1fcc8dbe 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -29,8 +29,13 @@ use Symfony\Component\Console\Output\OutputInterface; */ class DiagnoseCommand extends Command { + /** @var RemoteFileSystem */ protected $rfs; + + /** @var ProcessExecutor */ protected $process; + + /** @var int */ protected $failures = 0; protected function configure() @@ -48,7 +53,23 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { - $this->rfs = new RemoteFilesystem($this->getIO()); + $composer = $this->getComposer(false); + + if ($composer) { + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'diagnose', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + + $output->write('Checking composer.json: '); + $this->outputResult($output, $this->checkComposerSchema()); + } + + if ($composer) { + $config = $composer->getConfig(); + } else { + $config = Factory::createConfig(); + } + + $this->rfs = new RemoteFilesystem($this->getIO(), $config); $this->process = new ProcessExecutor($this->getIO()); $output->write('Checking platform settings: '); @@ -70,26 +91,29 @@ EOT $this->outputResult($output, $this->checkHttpsProxyFullUriRequestParam()); } - $composer = $this->getComposer(false); - if ($composer) { - $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'diagnose', $input, $output); - $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); - - $output->write('Checking composer.json: '); - $this->outputResult($output, $this->checkComposerSchema()); - } - - if ($composer) { - $config = $composer->getConfig(); - } else { - $config = Factory::createConfig(); - } - if ($oauth = $config->get('github-oauth')) { foreach ($oauth as $domain => $token) { $output->write('Checking '.$domain.' oauth access: '); $this->outputResult($output, $this->checkGithubOauth($domain, $token)); } + } else { + $output->write('Checking github.com rate limit: '); + $rate = $this->getGithubRateLimit('github.com'); + + if (10 > $rate['remaining']) { + $output->writeln('WARNING'); + $output->writeln(sprintf( + 'Github has a rate limit on their API. ' + . 'You currently have %u ' + . 'out of %u requests left.' . PHP_EOL + . 'See https://developer.github.com/v3/#rate-limiting and also' . PHP_EOL + . ' https://getcomposer.org/doc/articles/troubleshooting.md#api-rate-limit-and-oauth-tokens', + $rate['remaining'], + $rate['limit'] + )); + } else { + $output->writeln('OK'); + } } $output->write('Checking disk free space: '); @@ -129,7 +153,7 @@ EOT { $this->process->execute('git config color.ui', $output); if (strtolower(trim($output)) === 'always') { - return 'Your git color.ui setting is set to always, this is known to create issues. Use "git config --global color.ui true" to set it correctly.'; + return 'Your git color.ui setting is set to always, this is known to create issues. Use "git config --global color.ui true" to set it correctly.'; } return true; @@ -139,7 +163,7 @@ EOT { $protocol = extension_loaded('openssl') ? 'https' : 'http'; try { - $json = $this->rfs->getContents('packagist.org', $protocol . '://packagist.org/packages.json', false); + $this->rfs->getContents('packagist.org', $protocol . '://packagist.org/packages.json', false); } catch (\Exception $e) { return $e; } @@ -183,7 +207,7 @@ EOT try { $this->rfs->getContents('packagist.org', $url, false, array('http' => array('request_fulluri' => false))); } catch (TransportException $e) { - return 'Unable to assert the situation, maybe packagist.org is down ('.$e->getMessage().')'; + return 'Unable to assess the situation, maybe packagist.org is down ('.$e->getMessage().')'; } return 'It seems there is a problem with your proxy server, try setting the "HTTP_PROXY_REQUEST_FULLURI" and "HTTPS_PROXY_REQUEST_FULLURI" environment variables to "false"'; @@ -207,12 +231,12 @@ EOT $url = 'https://api.github.com/repos/Seldaek/jsonlint/zipball/1.0.0'; try { - $rfcResult = $this->rfs->getContents('api.github.com', $url, false); + $this->rfs->getContents('github.com', $url, false); } catch (TransportException $e) { try { - $this->rfs->getContents('api.github.com', $url, false, array('http' => array('request_fulluri' => false))); + $this->rfs->getContents('github.com', $url, false, array('http' => array('request_fulluri' => false))); } catch (TransportException $e) { - return 'Unable to assert the situation, maybe github is down ('.$e->getMessage().')'; + return 'Unable to assess the situation, maybe github is down ('.$e->getMessage().')'; } return 'It seems there is a problem with your proxy server, try setting the "HTTPS_PROXY_REQUEST_FULLURI" environment variable to "false"'; @@ -227,10 +251,33 @@ EOT try { $url = $domain === 'github.com' ? 'https://api.'.$domain.'/user/repos' : 'https://'.$domain.'/api/v3/user/repos'; - return $this->rfs->getContents($domain, $url, false) ? true : 'Unexpected error'; + return $this->rfs->getContents($domain, $url, false, array( + 'retry-auth-failure' => false + )) ? true : 'Unexpected error'; + } catch (\Exception $e) { + if ($e instanceof TransportException && $e->getCode() === 401) { + return 'The oauth token for '.$domain.' seems invalid, run "composer config --global --unset github-oauth.'.$domain.'" to remove it'; + } + + return $e; + } + } + + private function getGithubRateLimit($domain, $token = null) + { + if ($token) { + $this->getIO()->setAuthentication($domain, $token, 'x-oauth-basic'); + } + + try { + $url = $domain === 'github.com' ? 'https://api.'.$domain.'/rate_limit' : 'https://'.$domain.'/api/rate_limit'; + $json = $this->rfs->getContents($domain, $url, false, array('retry-auth-failure' => false)); + $data = json_decode($json, true); + + return $data['resources']['core']; } catch (\Exception $e) { if ($e instanceof TransportException && $e->getCode() === 401) { - return 'The oauth token for '.$domain.' seems invalid, run "composer config --global --unset github-oauth.'.$domain.'" to remove it'; + return 'The oauth token for '.$domain.' seems invalid, run "composer config --global --unset github-oauth.'.$domain.'" to remove it'; } return $e; @@ -255,7 +302,7 @@ EOT $latest = trim($this->rfs->getContents('getcomposer.org', $protocol . '://getcomposer.org/version', false)); if (Composer::VERSION !== $latest && Composer::VERSION !== '@package_version@') { - return 'Your are not running the latest version'; + return 'You are not running the latest version'; } return true; @@ -271,7 +318,7 @@ EOT if ($result instanceof \Exception) { $output->writeln('['.get_class($result).'] '.$result->getMessage()); } elseif ($result) { - $output->writeln($result); + $output->writeln(trim($result)); } } } @@ -280,7 +327,7 @@ EOT { $output = ''; $out = function ($msg, $style) use (&$output) { - $output .= '<'.$style.'>'.$msg.''; + $output .= '<'.$style.'>'.$msg.''.PHP_EOL; }; // code below taken from getcomposer.org/installer, any changes should be made there and replicated here @@ -312,7 +359,7 @@ EOT $warnings['openssl'] = true; } - if (ini_get('apc.enable_cli')) { + if (!defined('HHVM_VERSION') && !extension_loaded('apcu') && ini_get('apc.enable_cli')) { $warnings['apc_cli'] = true; } @@ -341,13 +388,13 @@ EOT foreach ($errors as $error => $current) { switch ($error) { case 'php': - $text = PHP_EOL."Your PHP ({$current}) is too old, you must upgrade to PHP 5.3.2 or higher."; + $text = "Your PHP ({$current}) is too old, you must upgrade to PHP 5.3.2 or higher."; break; case 'allow_url_fopen': - $text = PHP_EOL."The allow_url_fopen setting is incorrect.".PHP_EOL; + $text = "The allow_url_fopen setting is incorrect.".PHP_EOL; $text .= "Add the following to the end of your `php.ini`:".PHP_EOL; - $text .= " allow_url_fopen = On"; + $text .= " allow_url_fopen = On"; $displayIniMessage = true; break; } @@ -361,51 +408,51 @@ EOT foreach ($warnings as $warning => $current) { switch ($warning) { case 'apc_cli': - $text = PHP_EOL."The apc.enable_cli setting is incorrect.".PHP_EOL; + $text = "The apc.enable_cli setting is incorrect.".PHP_EOL; $text .= "Add the following to the end of your `php.ini`:".PHP_EOL; - $text .= " apc.enable_cli = Off"; + $text .= " apc.enable_cli = Off"; $displayIniMessage = true; break; case 'sigchild': - $text = PHP_EOL."PHP was compiled with --enable-sigchild which can cause issues on some platforms.".PHP_EOL; + $text = "PHP was compiled with --enable-sigchild which can cause issues on some platforms.".PHP_EOL; $text .= "Recompile it without this flag if possible, see also:".PHP_EOL; - $text .= " https://bugs.php.net/bug.php?id=22999"; + $text .= " https://bugs.php.net/bug.php?id=22999"; break; case 'curlwrappers': - $text = PHP_EOL."PHP was compiled with --with-curlwrappers which will cause issues with HTTP authentication and GitHub.".PHP_EOL; - $text .= "Recompile it without this flag if possible"; + $text = "PHP was compiled with --with-curlwrappers which will cause issues with HTTP authentication and GitHub.".PHP_EOL; + $text .= " Recompile it without this flag if possible"; break; case 'openssl': - $text = PHP_EOL."The openssl extension is missing, which will reduce the security and stability of Composer.".PHP_EOL; - $text .= "If possible you should enable it or recompile php with --with-openssl"; + $text = "The openssl extension is missing, which will reduce the security and stability of Composer.".PHP_EOL; + $text .= " If possible you should enable it or recompile php with --with-openssl"; break; case 'php': - $text = PHP_EOL."Your PHP ({$current}) is quite old, upgrading to PHP 5.3.4 or higher is recommended.".PHP_EOL; - $text .= "Composer works with 5.3.2+ for most people, but there might be edge case issues."; + $text = "Your PHP ({$current}) is quite old, upgrading to PHP 5.3.4 or higher is recommended.".PHP_EOL; + $text .= " Composer works with 5.3.2+ for most people, but there might be edge case issues."; break; case 'xdebug_loaded': - $text = PHP_EOL."The xdebug extension is loaded, this can slow down Composer a little.".PHP_EOL; - $text .= "Disabling it when using Composer is recommended, but should not cause issues beyond slowness."; + $text = "The xdebug extension is loaded, this can slow down Composer a little.".PHP_EOL; + $text .= " Disabling it when using Composer is recommended."; break; case 'xdebug_profile': - $text = PHP_EOL."The xdebug.profiler_enabled setting is enabled, this can slow down Composer a lot.".PHP_EOL; + $text = "The xdebug.profiler_enabled setting is enabled, this can slow down Composer a lot.".PHP_EOL; $text .= "Add the following to the end of your `php.ini` to disable it:".PHP_EOL; - $text .= " xdebug.profiler_enabled = 0"; + $text .= " xdebug.profiler_enabled = 0"; $displayIniMessage = true; break; } - $out($text, 'warning'); + $out($text, 'comment'); } } if ($displayIniMessage) { - $out($iniMessage, 'warning'); + $out($iniMessage, 'comment'); } return !$warnings && !$errors ? true : $output; diff --git a/src/Composer/Command/DumpAutoloadCommand.php b/src/Composer/Command/DumpAutoloadCommand.php index d228fb150..adcc7adfd 100644 --- a/src/Composer/Command/DumpAutoloadCommand.php +++ b/src/Composer/Command/DumpAutoloadCommand.php @@ -30,7 +30,8 @@ class DumpAutoloadCommand extends Command ->setAliases(array('dumpautoload')) ->setDescription('Dumps the autoloader') ->setDefinition(array( - new InputOption('optimize', 'o', InputOption::VALUE_NONE, 'Optimizes PSR0 packages to be loaded with classmaps too, good for production.'), + new InputOption('optimize', 'o', InputOption::VALUE_NONE, 'Optimizes PSR0 and PSR4 packages to be loaded with classmaps too, good for production.'), + new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables autoload-dev rules.'), )) ->setHelp(<<php composer.phar dump-autoload @@ -51,7 +52,7 @@ EOT $package = $composer->getPackage(); $config = $composer->getConfig(); - $optimize = $input->getOption('optimize') || $config->get('optimize-autoloader'); + $optimize = $input->getOption('optimize') || $config->get('optimize-autoloader') || $config->get('classmap-authoritative'); if ($optimize) { $output->writeln('Generating optimized autoload files'); @@ -59,6 +60,8 @@ EOT $output->writeln('Generating autoload files'); } - $composer->getAutoloadGenerator()->dump($config, $localRepo, $package, $installationManager, 'composer', $optimize); + $generator = $composer->getAutoloadGenerator(); + $generator->setDevMode(!$input->getOption('no-dev')); + $generator->dump($config, $localRepo, $package, $installationManager, 'composer', $optimize); } } diff --git a/src/Composer/Command/HomeCommand.php b/src/Composer/Command/HomeCommand.php new file mode 100644 index 000000000..7dd8113cb --- /dev/null +++ b/src/Composer/Command/HomeCommand.php @@ -0,0 +1,164 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Command; + +use Composer\DependencyResolver\Pool; +use Composer\Factory; +use Composer\Package\CompletePackageInterface; +use Composer\Repository\CompositeRepository; +use Composer\Repository\RepositoryInterface; +use Composer\Util\ProcessExecutor; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Robert Schönthal + */ +class HomeCommand extends Command +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + $this + ->setName('browse') + ->setAliases(array('home')) + ->setDescription('Opens the package\'s repository URL or homepage in your browser.') + ->setDefinition(array( + new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Package(s) to browse to.'), + new InputOption('homepage', 'H', InputOption::VALUE_NONE, 'Open the homepage instead of the repository URL.'), + new InputOption('show', 's', InputOption::VALUE_NONE, 'Only show the homepage or repository URL.'), + )) + ->setHelp(<<initializeRepo(); + $return = 0; + + foreach ($input->getArgument('packages') as $packageName) { + $package = $this->getPackage($repo, $packageName); + + if (!$package instanceof CompletePackageInterface) { + $return = 1; + $output->writeln('Package '.$packageName.' not found'); + + continue; + } + + $support = $package->getSupport(); + $url = isset($support['source']) ? $support['source'] : $package->getSourceUrl(); + if (!$url || $input->getOption('homepage')) { + $url = $package->getHomepage(); + } + + if (!filter_var($url, FILTER_VALIDATE_URL)) { + $return = 1; + $output->writeln(''.($input->getOption('homepage') ? 'Invalid or missing homepage' : 'Invalid or missing repository URL').' for '.$packageName.''); + + continue; + } + + if ($input->getOption('show')) { + $output->writeln(sprintf('%s', $url)); + } else { + $this->openBrowser($url); + } + } + + return $return; + } + + /** + * finds a package by name + * + * @param RepositoryInterface $repos + * @param string $name + * @return CompletePackageInterface + */ + protected function getPackage(RepositoryInterface $repos, $name) + { + $name = strtolower($name); + $pool = new Pool('dev'); + $pool->addRepository($repos); + $matches = $pool->whatProvides($name); + + foreach ($matches as $index => $package) { + // skip providers/replacers + if ($package->getName() !== $name) { + unset($matches[$index]); + continue; + } + + return $package; + } + } + + /** + * opens a url in your system default browser + * + * @param string $url + */ + private function openBrowser($url) + { + $url = ProcessExecutor::escape($url); + + if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + return passthru('start "web" explorer "' . $url . '"'); + } + + passthru('which xdg-open', $linux); + passthru('which open', $osx); + + if (0 === $linux) { + passthru('xdg-open ' . $url); + } elseif (0 === $osx) { + passthru('open ' . $url); + } else { + $this->getIO()->write('no suitable browser opening command found, open yourself: ' . $url); + } + } + + /** + * Initializes the repo + * + * @return CompositeRepository + */ + private function initializeRepo() + { + $composer = $this->getComposer(false); + + if ($composer) { + $repo = new CompositeRepository($composer->getRepositoryManager()->getRepositories()); + } else { + $defaultRepos = Factory::createDefaultRepositories($this->getIO()); + $repo = new CompositeRepository($defaultRepos); + } + + return $repo; + } +} diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index a44546b73..38dc2c4a5 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -12,12 +12,15 @@ namespace Composer\Command; +use Composer\DependencyResolver\Pool; use Composer\Json\JsonFile; use Composer\Factory; use Composer\Package\BasePackage; +use Composer\Package\Version\VersionSelector; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Package\Version\VersionParser; +use Composer\Util\ProcessExecutor; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -30,8 +33,10 @@ use Symfony\Component\Process\ExecutableFinder; */ class InitCommand extends Command { + protected $repos; + private $gitConfig; - private $repos; + private $pool; public function parseAuthorString($author) { @@ -101,7 +106,7 @@ EOT } if (isset($options['require-dev'])) { - $options['require-dev'] = $this->formatRequirements($options['require-dev']) ; + $options['require-dev'] = $this->formatRequirements($options['require-dev']); if (array() === $options['require-dev']) { $options['require-dev'] = new \stdClass; } @@ -224,10 +229,7 @@ EOT $output, $dialog->getQuestion('Author', $author), function ($value) use ($self, $author) { - if (null === $value) { - return $author; - } - + $value = $value ?: $author; $author = $self->parseAuthorString($value); return sprintf('%s <%s>', $author['name'], $author['email']); @@ -284,9 +286,11 @@ EOT protected function findPackages($name) { - $packages = array(); + return $this->getRepos()->search($name); + } - // init repos + protected function getRepos() + { if (!$this->repos) { $this->repos = new CompositeRepository(array_merge( array(new PlatformRepository), @@ -294,7 +298,7 @@ EOT )); } - return $this->repos->search($name); + return $this->repos; } protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array()) @@ -306,15 +310,17 @@ EOT $requires = $this->normalizeRequirements($requires); $result = array(); - foreach ($requires as $key => $requirement) { - if (!isset($requirement['version']) && $input->isInteractive()) { - $question = $dialog->getQuestion('Please provide a version constraint for the '.$requirement['name'].' requirement'); - if ($constraint = $dialog->ask($output, $question)) { - $requirement['version'] = $constraint; - } - } + foreach ($requires as $requirement) { if (!isset($requirement['version'])) { - throw new \InvalidArgumentException('The requirement '.$requirement['name'].' must contain a version constraint'); + // determine the best version automatically + $version = $this->findBestVersionForPackage($input, $requirement['name']); + $requirement['version'] = $version; + + $output->writeln(sprintf( + 'Using version %s for %s', + $requirement['version'], + $requirement['name'] + )); } $result[] = $requirement['name'] . ' ' . $requirement['version']; @@ -327,17 +333,11 @@ EOT $matches = $this->findPackages($package); if (count($matches)) { - $output->writeln(array( - '', - sprintf('Found %s packages matching %s', count($matches), $package), - '' - )); - $exactMatch = null; $choices = array(); - foreach ($matches as $position => $package) { - $choices[] = sprintf(' %5s %s', "[$position]", $package['name']); - if ($package['name'] === $package) { + foreach ($matches as $position => $foundPackage) { + $choices[] = sprintf(' %5s %s', "[$position]", $foundPackage['name']); + if ($foundPackage['name'] === $package) { $exactMatch = true; break; } @@ -345,6 +345,12 @@ EOT // no match, prompt which to pick if (!$exactMatch) { + $output->writeln(array( + '', + sprintf('Found %s packages matching %s', count($matches), $package), + '' + )); + $output->writeln($choices); $output->writeln(''); @@ -369,7 +375,7 @@ EOT $package = $dialog->askAndValidate($output, $dialog->getQuestion('Enter package # to add, or the complete package name if it is not listed', false, ':'), $validator, 3); } - // no constraint yet, prompt user + // no constraint yet, determine the best version automatically if (false !== $package && false === strpos($package, ' ')) { $validator = function ($input) { $input = trim($input); @@ -377,9 +383,20 @@ EOT return $input ?: false; }; - $constraint = $dialog->askAndValidate($output, $dialog->getQuestion('Enter the version constraint to require', false, ':'), $validator, 3); + $constraint = $dialog->askAndValidate( + $output, + $dialog->getQuestion('Enter the version constraint to require (or leave blank to use the latest version)', false, ':'), + $validator, + 3) + ; if (false === $constraint) { - continue; + $constraint = $this->findBestVersionForPackage($input, $package); + + $output->writeln(sprintf( + 'Using version %s for %s', + $constraint, + $package + )); } $package .= ' '.$constraint; @@ -419,7 +436,7 @@ EOT $finder = new ExecutableFinder(); $gitBin = $finder->find('git'); - $cmd = new Process(sprintf('%s config -l', escapeshellarg($gitBin))); + $cmd = new Process(sprintf('%s config -l', ProcessExecutor::escape($gitBin))); $cmd->run(); if ($cmd->isSuccessful()) { @@ -504,4 +521,57 @@ EOT return false !== filter_var($email, FILTER_VALIDATE_EMAIL); } + + private function getPool(InputInterface $input) + { + if (!$this->pool) { + $this->pool = new Pool($this->getMinimumStability($input)); + $this->pool->addRepository($this->getRepos()); + } + + return $this->pool; + } + + private function getMinimumStability(InputInterface $input) + { + if ($input->hasOption('stability')) { + return $input->getOption('stability') ?: 'stable'; + } + + $file = Factory::getComposerFile(); + if (is_file($file) && is_readable($file) && is_array($composer = json_decode(file_get_contents($file), true))) { + if (!empty($composer['minimum-stability'])) { + return $composer['minimum-stability']; + } + } + + return 'stable'; + } + + /** + * Given a package name, this determines the best version to use in the require key. + * + * This returns a version with the ~ operator prefixed when possible. + * + * @param InputInterface $input + * @param string $name + * @return string + * @throws \InvalidArgumentException + */ + private function findBestVersionForPackage(InputInterface $input, $name) + { + // find the latest version allowed in this pool + $versionSelector = new VersionSelector($this->getPool($input)); + $package = $versionSelector->findBestCandidate($name); + + if (!$package) { + throw new \InvalidArgumentException(sprintf( + 'Could not find package %s at any version for your minimum-stability (%s). Check the package spelling or your minimum-stability', + $name, + $this->getMinimumStability($input) + )); + } + + return $versionSelector->findRecommendedRequireVersion($package); + } } diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index 955607c85..115e1d6af 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -17,6 +17,7 @@ use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; /** @@ -40,10 +41,13 @@ class InstallCommand extends Command new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'), new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Disables all plugins.'), new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'DEPRECATED: Use no-plugins instead.'), + new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('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('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'), + new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Should not be provided, use composer require instead to add a given package to composer.json.'), )) ->setHelp(<<install command reads the composer.lock file from @@ -60,11 +64,21 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { + if ($args = $input->getArgument('packages')) { + $output->writeln('Invalid argument '.implode(' ', $args).'. Use "composer require '.implode(' ', $args).'" instead to add packages to your composer.json.'); + + return 1; + } + if ($input->getOption('no-custom-installers')) { $output->writeln('You are using the deprecated option "no-custom-installers". Use "no-plugins" instead.'); $input->setOption('no-plugins', true); } + if ($input->getOption('dev')) { + $output->writeln('You are using the deprecated option "dev". Dev packages are installed by default now.'); + } + $composer = $this->getComposer(true, $input->getOption('no-plugins')); $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $io = $this->getIO(); @@ -96,7 +110,7 @@ EOT $preferDist = $input->getOption('prefer-dist'); } - $optimize = $input->getOption('optimize-autoloader') || $config->get('optimize-autoloader'); + $optimize = $input->getOption('optimize-autoloader') || $config->get('optimize-autoloader') || $config->get('classmap-authoritative'); $install ->setDryRun($input->getOption('dry-run')) @@ -104,8 +118,10 @@ EOT ->setPreferSource($preferSource) ->setPreferDist($preferDist) ->setDevMode(!$input->getOption('no-dev')) + ->setDumpAutoloader(!$input->getOption('no-autoloader')) ->setRunScripts(!$input->getOption('no-scripts')) ->setOptimizeAutoloader($optimize) + ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ; if ($input->getOption('no-plugins')) { diff --git a/src/Composer/Command/LicensesCommand.php b/src/Composer/Command/LicensesCommand.php index 5d05ef74f..8ab9d94b4 100644 --- a/src/Composer/Command/LicensesCommand.php +++ b/src/Composer/Command/LicensesCommand.php @@ -16,6 +16,8 @@ use Composer\Json\JsonFile; use Composer\Package\Version\VersionParser; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; +use Composer\Package\PackageInterface; +use Composer\Repository\RepositoryInterface; use Symfony\Component\Console\Helper\TableHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -33,6 +35,7 @@ class LicensesCommand extends Command ->setDescription('Show information about licenses of dependencies') ->setDefinition(array( new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), + new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'), )) ->setHelp(<<getPackages() as $package) { - $packages[$package->getName()] = $package; + if ($input->getOption('no-dev')) { + $packages = $this->filterRequiredPackages($repo, $root); + } else { + $packages = $this->appendPackages($repo->getPackages(), array()); } ksort($packages); @@ -102,4 +106,47 @@ EOT throw new \RuntimeException(sprintf('Unsupported format "%s". See help for supported formats.', $format)); } } + + /** + * Find package requires and child requires + * + * @param RepositoryInterface $repo + * @param PackageInterface $package + */ + private function filterRequiredPackages(RepositoryInterface $repo, PackageInterface $package, $bucket = array()) + { + $requires = array_keys($package->getRequires()); + + $packageListNames = array_keys($bucket); + $packages = array_filter( + $repo->getPackages(), + function ($package) use ($requires, $packageListNames) { + return in_array($package->getName(), $requires) && !in_array($package->getName(), $packageListNames); + } + ); + + $bucket = $this->appendPackages($packages, $bucket); + + foreach ($packages as $package) { + $bucket = $this->filterRequiredPackages($repo, $package, $bucket); + } + + return $bucket; + } + + /** + * Adds packages to the package list + * + * @param array $packages the list of packages to add + * @param array $bucket the list to add packages to + * @return array + */ + public function appendPackages(array $packages, array $bucket) + { + foreach ($packages as $package) { + $bucket[$package->getName()] = $package; + } + + return $bucket; + } } diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php new file mode 100644 index 000000000..c292a2812 --- /dev/null +++ b/src/Composer/Command/RemoveCommand.php @@ -0,0 +1,120 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Command; + +use Composer\Config\JsonConfigSource; +use Composer\Installer; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; +use Composer\Json\JsonFile; +use Composer\Factory; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Pierre du Plessis + * @author Jordi Boggiano + */ +class RemoveCommand extends Command +{ + protected function configure() + { + $this + ->setName('remove') + ->setDescription('Removes a package from the require or require-dev') + ->setDefinition(array( + new InputArgument('packages', InputArgument::IS_ARRAY, 'Packages that should be removed.'), + new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'), + 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('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 with explicit dependencies.'), + new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'), + )) + ->setHelp(<<remove command removes a package from the current +list of installed packages + +php composer.phar remove + +EOT + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $packages = $input->getArgument('packages'); + + $file = Factory::getComposerFile(); + + $jsonFile = new JsonFile($file); + $composer = $jsonFile->read(); + $composerBackup = file_get_contents($jsonFile->getPath()); + + $json = new JsonConfigSource($jsonFile); + + $type = $input->getOption('dev') ? 'require-dev' : 'require'; + $altType = !$input->getOption('dev') ? 'require-dev' : 'require'; + + foreach ($packages as $package) { + if (isset($composer[$type][$package])) { + $json->removeLink($type, $package); + } elseif (isset($composer[$altType][$package])) { + $output->writeln(''.$package.' could not be found in '.$type.' but it is present in '.$altType.''); + $dialog = $this->getHelperSet()->get('dialog'); + if ($this->getIO()->isInteractive()) { + if ($dialog->askConfirmation($output, $dialog->getQuestion('Do you want to remove it from '.$altType, 'yes', '?'), true)) { + $json->removeLink($altType, $package); + } + } + } else { + $output->writeln(''.$package.' is not required in your composer.json and has not been removed'); + } + } + + if ($input->getOption('no-update')) { + return 0; + } + + // Update packages + $composer = $this->getComposer(); + $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); + $io = $this->getIO(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + + $install = Installer::create($io, $composer); + + $updateDevMode = !$input->getOption('update-no-dev'); + $install + ->setVerbose($input->getOption('verbose')) + ->setDevMode($updateDevMode) + ->setUpdate(true) + ->setUpdateWhitelist($packages) + ->setWhitelistDependencies($input->getOption('update-with-dependencies')) + ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) + ; + + $status = $install->run(); + if ($status !== 0) { + $output->writeln("\n".'Removal failed, reverting '.$file.' to its original content.'); + file_put_contents($jsonFile->getPath(), $composerBackup); + } + + return $status; + } +} diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index f33c2fd00..ea972aaf0 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -23,6 +23,8 @@ use Composer\Json\JsonManipulator; use Composer\Package\Version\VersionParser; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; +use Composer\Repository\CompositeRepository; +use Composer\Repository\PlatformRepository; /** * @author Jérémy Romey @@ -36,15 +38,21 @@ class RequireCommand extends InitCommand ->setName('require') ->setDescription('Adds required packages to your composer.json and installs them') ->setDefinition(array( - new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Required package with a version constraint, e.g. 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, 'Required package name optionally including 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('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('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('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 with explicit dependencies.'), + new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'), + new InputOption('sort-packages', null, InputOption::VALUE_NONE, 'Sorts packages when adding/updating a new dependency'), )) ->setHelp(<<writeln(''.$file.' could not be created.'); @@ -74,13 +83,22 @@ EOT } $json = new JsonFile($file); - $composer = $json->read(); + $composerDefinition = $json->read(); $composerBackup = file_get_contents($json->getPath()); + $composer = $this->getComposer(); + $repos = $composer->getRepositoryManager()->getRepositories(); + + $this->repos = new CompositeRepository(array_merge( + array(new PlatformRepository), + $repos + )); + $requirements = $this->determineRequirements($input, $output, $input->getArgument('packages')); $requireKey = $input->getOption('dev') ? 'require-dev' : 'require'; - $baseRequirements = array_key_exists($requireKey, $composer) ? $composer[$requireKey] : array(); + $removeKey = $input->getOption('dev') ? 'require' : 'require-dev'; + $baseRequirements = array_key_exists($requireKey, $composerDefinition) ? $composerDefinition[$requireKey] : array(); $requirements = $this->formatRequirements($requirements); // validate requirements format @@ -89,22 +107,30 @@ EOT $versionParser->parseConstraints($constraint); } - if (!$this->updateFileCleanly($json, $baseRequirements, $requirements, $requireKey)) { + $sortPackages = $input->getOption('sort-packages'); + + if (!$this->updateFileCleanly($json, $baseRequirements, $requirements, $requireKey, $removeKey, $sortPackages)) { foreach ($requirements as $package => $version) { $baseRequirements[$package] = $version; + + if (isset($composerDefinition[$removeKey][$package])) { + unset($composerDefinition[$removeKey][$package]); + } } - $composer[$requireKey] = $baseRequirements; - $json->write($composer); + $composerDefinition[$requireKey] = $baseRequirements; + $json->write($composerDefinition); } - $output->writeln(''.$file.' has been updated'); + $output->writeln(''.$file.' has been '.($newlyCreated ? 'created' : 'updated').''); if ($input->getOption('no-update')) { return 0; } + $updateDevMode = !$input->getOption('update-no-dev'); // Update packages + $this->resetComposer(); $composer = $this->getComposer(); $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $io = $this->getIO(); @@ -118,28 +144,38 @@ EOT ->setVerbose($input->getOption('verbose')) ->setPreferSource($input->getOption('prefer-source')) ->setPreferDist($input->getOption('prefer-dist')) - ->setDevMode(true) + ->setDevMode($updateDevMode) ->setUpdate(true) - ->setUpdateWhitelist(array_keys($requirements)); + ->setUpdateWhitelist(array_keys($requirements)) + ->setWhitelistDependencies($input->getOption('update-with-dependencies')) + ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ; $status = $install->run(); if ($status !== 0) { - $output->writeln("\n".'Installation failed, reverting '.$file.' to its original content.'); - file_put_contents($json->getPath(), $composerBackup); + if ($newlyCreated) { + $output->writeln("\n".'Installation failed, deleting '.$file.'.'); + unlink($json->getPath()); + } else { + $output->writeln("\n".'Installation failed, reverting '.$file.' to its original content.'); + file_put_contents($json->getPath(), $composerBackup); + } } return $status; } - private function updateFileCleanly($json, array $base, array $new, $requireKey) + private function updateFileCleanly($json, array $base, array $new, $requireKey, $removeKey, $sortPackages) { $contents = file_get_contents($json->getPath()); $manipulator = new JsonManipulator($contents); foreach ($new as $package => $constraint) { - if (!$manipulator->addLink($requireKey, $package, $constraint)) { + if (!$manipulator->addLink($requireKey, $package, $constraint, $sortPackages)) { + return false; + } + if (!$manipulator->removeSubNode($removeKey, $package)) { return false; } } diff --git a/src/Composer/Command/RunScriptCommand.php b/src/Composer/Command/RunScriptCommand.php index c4a3a3563..f01a5febe 100644 --- a/src/Composer/Command/RunScriptCommand.php +++ b/src/Composer/Command/RunScriptCommand.php @@ -12,6 +12,7 @@ namespace Composer\Command; +use Composer\Script\CommandEvent; use Composer\Script\ScriptEvents; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -23,6 +24,30 @@ use Symfony\Component\Console\Output\OutputInterface; */ class RunScriptCommand extends Command { + /** + * @var array Array with command events + */ + protected $commandEvents = array( + ScriptEvents::PRE_INSTALL_CMD, + ScriptEvents::POST_INSTALL_CMD, + ScriptEvents::PRE_UPDATE_CMD, + ScriptEvents::POST_UPDATE_CMD, + ScriptEvents::PRE_STATUS_CMD, + ScriptEvents::POST_STATUS_CMD, + ScriptEvents::POST_ROOT_PACKAGE_INSTALL, + ScriptEvents::POST_CREATE_PROJECT_CMD + ); + + /** + * @var array Array with script events + */ + protected $scriptEvents = array( + ScriptEvents::PRE_ARCHIVE_CMD, + ScriptEvents::POST_ARCHIVE_CMD, + ScriptEvents::PRE_AUTOLOAD_DUMP, + ScriptEvents::POST_AUTOLOAD_DUMP + ); + protected function configure() { $this @@ -30,6 +55,7 @@ class RunScriptCommand extends Command ->setDescription('Run the scripts defined in composer.json.') ->setDefinition(array( new InputArgument('script', InputArgument::REQUIRED, 'Script name to run.'), + new InputArgument('args', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, ''), new InputOption('dev', null, InputOption::VALUE_NONE, 'Sets the dev mode.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables the dev mode.'), )) @@ -45,21 +71,30 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { $script = $input->getArgument('script'); - if (!in_array($script, array( - ScriptEvents::PRE_INSTALL_CMD, - ScriptEvents::POST_INSTALL_CMD, - ScriptEvents::PRE_UPDATE_CMD, - ScriptEvents::POST_UPDATE_CMD, - ScriptEvents::PRE_STATUS_CMD, - ScriptEvents::POST_STATUS_CMD, - ))) { + if (!in_array($script, $this->commandEvents) && !in_array($script, $this->scriptEvents)) { if (defined('Composer\Script\ScriptEvents::'.str_replace('-', '_', strtoupper($script)))) { throw new \InvalidArgumentException(sprintf('Script "%s" cannot be run with this command', $script)); } + } + + $composer = $this->getComposer(); + $hasListeners = $composer->getEventDispatcher()->hasEventListeners(new CommandEvent($script, $composer, $this->getIO())); + if (!$hasListeners) { + throw new \InvalidArgumentException(sprintf('Script "%s" is not defined in this package', $script)); + } + + // add the bin dir to the PATH to make local binaries of deps usable in scripts + $binDir = $composer->getConfig()->get('bin-dir'); + if (is_dir($binDir)) { + putenv('PATH='.realpath($binDir).PATH_SEPARATOR.getenv('PATH')); + } + + $args = $input->getArgument('args'); - throw new \InvalidArgumentException(sprintf('Script "%s" does not exist', $script)); + if (in_array($script, $this->commandEvents)) { + return $composer->getEventDispatcher()->dispatchCommandEvent($script, $input->getOption('dev') || !$input->getOption('no-dev'), $args); } - $this->getComposer()->getEventDispatcher()->dispatchCommandEvent($script, $input->getOption('dev') || !$input->getOption('no-dev')); + return $composer->getEventDispatcher()->dispatchScript($script, $input->getOption('dev') || !$input->getOption('no-dev'), $args); } } diff --git a/src/Composer/Command/ScriptAliasCommand.php b/src/Composer/Command/ScriptAliasCommand.php new file mode 100644 index 000000000..958678068 --- /dev/null +++ b/src/Composer/Command/ScriptAliasCommand.php @@ -0,0 +1,67 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Command; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Jordi Boggiano + */ +class ScriptAliasCommand extends Command +{ + private $script; + + public function __construct($script) + { + $this->script = $script; + + parent::__construct(); + } + + protected function configure() + { + $this + ->setName($this->script) + ->setDescription('Run the '.$this->script.' script as defined in composer.json.') + ->setDefinition(array( + new InputOption('dev', null, InputOption::VALUE_NONE, 'Sets the dev mode.'), + new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables the dev mode.'), + new InputArgument('args', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, ''), + )) + ->setHelp(<<run-script command runs scripts defined in composer.json: + +php composer.phar run-script post-update-cmd +EOT + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $composer = $this->getComposer(); + + // add the bin dir to the PATH to make local binaries of deps usable in scripts + $binDir = $composer->getConfig()->get('bin-dir'); + if (is_dir($binDir)) { + putenv('PATH='.realpath($binDir).PATH_SEPARATOR.getenv('PATH')); + } + + $args = $input->getArguments(); + + return $composer->getEventDispatcher()->dispatchScript($this->script, $input->getOption('dev') || !$input->getOption('no-dev'), $args['args']); + } +} diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 3782a5c62..eb0de083e 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -21,6 +21,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Finder\Finder; /** * @author Igor Wiedler @@ -42,6 +43,7 @@ class SelfUpdateCommand extends Command new InputOption('rollback', 'r', InputOption::VALUE_NONE, 'Revert to an older installation of composer'), new InputOption('clean-backups', null, InputOption::VALUE_NONE, 'Delete old backups during an update. This makes the current version of composer the only backup available after the update'), new InputArgument('version', InputArgument::OPTIONAL, 'The version to update to'), + new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), )) ->setHelp(<<self-update command checks getcomposer.org for newer @@ -57,8 +59,8 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { $baseUrl = (extension_loaded('openssl') ? 'https' : 'http') . '://' . self::HOMEPAGE; - $remoteFilesystem = new RemoteFilesystem($this->getIO()); $config = Factory::createConfig(); + $remoteFilesystem = new RemoteFilesystem($this->getIO(), $config); $cacheDir = $config->get('cache-dir'); $rollbackDir = $config->get('home'); $localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; @@ -104,7 +106,7 @@ EOT $output->writeln(sprintf("Updating to version %s.", $updateVersion)); $remoteFilename = $baseUrl . (preg_match('{^[0-9a-f]{40}$}', $updateVersion) ? '/composer.phar' : "/download/{$updateVersion}/composer.phar"); - $remoteFilesystem->copy(self::HOMEPAGE, $remoteFilename, $tempFilename); + $remoteFilesystem->copy(self::HOMEPAGE, $remoteFilename, $tempFilename, !$input->getOption('no-progress')); if (!file_exists($tempFilename)) { $output->writeln('The download of the new composer version failed for an unexpected reason'); @@ -113,15 +115,13 @@ EOT // remove saved installations of composer if ($input->getOption('clean-backups')) { - $files = $this->getOldInstallationFiles($rollbackDir); + $finder = $this->getOldInstallationFinder($rollbackDir); - if (!empty($files)) { - $fs = new Filesystem; - - foreach ($files as $file) { - $output->writeln('Removing: '.$file.''); - $fs->remove($file); - } + $fs = new Filesystem; + foreach ($finder as $file) { + $file = (string) $file; + $output->writeln('Removing: '.$file.''); + $fs->remove($file); } } @@ -173,18 +173,19 @@ EOT protected function setLocalPhar($localFilename, $newFilename, $backupTarget = null) { try { - @chmod($newFilename, 0777 & ~umask()); - // test the phar validity - $phar = new \Phar($newFilename); - // free the variable to unlock the file - unset($phar); + @chmod($newFilename, fileperms($localFilename)); + if (!ini_get('phar.readonly')) { + // test the phar validity + $phar = new \Phar($newFilename); + // free the variable to unlock the file + unset($phar); + } // copy current file into installations dir if ($backupTarget && file_exists($localFilename)) { @copy($localFilename, $backupTarget); } - unset($phar); rename($newFilename, $localFilename); } catch (\Exception $e) { if ($backupTarget) { @@ -200,18 +201,25 @@ EOT protected function getLastBackupVersion($rollbackDir) { - $files = $this->getOldInstallationFiles($rollbackDir); - if (empty($files)) { - return false; - } + $finder = $this->getOldInstallationFinder($rollbackDir); + $finder->sortByName(); + $files = iterator_to_array($finder); - sort($files); + if (count($files)) { + return basename(end($files), self::OLD_INSTALL_EXT); + } - return basename(end($files), self::OLD_INSTALL_EXT); + return false; } - protected function getOldInstallationFiles($rollbackDir) + protected function getOldInstallationFinder($rollbackDir) { - return glob($rollbackDir . '/*' . self::OLD_INSTALL_EXT) ?: array(); + $finder = Finder::create() + ->depth(0) + ->files() + ->name('*' . self::OLD_INSTALL_EXT) + ->in($rollbackDir); + + return $finder; } } diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 4b5b6ee9c..907e99f90 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -41,6 +41,7 @@ class ShowCommand extends Command { $this ->setName('show') + ->setAliases(array('info')) ->setDescription('Show information about packages') ->setDefinition(array( new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect'), @@ -50,6 +51,7 @@ class ShowCommand extends Command new InputOption('available', 'a', InputOption::VALUE_NONE, 'List available packages only'), new InputOption('self', 's', InputOption::VALUE_NONE, 'Show the root package information'), new InputOption('name-only', 'N', InputOption::VALUE_NONE, 'List package names only'), + new InputOption('path', 'P', InputOption::VALUE_NONE, 'Show package paths'), )) ->setHelp(<<getOption('name-only') && $showVersion && ($nameLength + $versionLength + 3 <= $width); - $writeDescription = !$input->getOption('name-only') && ($nameLength + ($showVersion ? $versionLength : 0) + 24 <= $width); + $writePath = !$input->getOption('name-only') && $input->getOption('path'); + $writeVersion = !$input->getOption('name-only') && !$input->getOption('path') && $showVersion && ($nameLength + $versionLength + 3 <= $width); + $writeDescription = !$input->getOption('name-only') && !$input->getOption('path') && ($nameLength + ($showVersion ? $versionLength : 0) + 24 <= $width); foreach ($packages[$type] as $package) { if (is_object($package)) { $output->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false); @@ -211,6 +214,11 @@ EOT } $output->write(' ' . $description); } + + if ($writePath) { + $path = strtok(realpath($composer->getInstallationManager()->getInstallPath($package)), "\r\n"); + $output->write(' ' . $path); + } } else { $output->write($indent . $package); } @@ -287,6 +295,16 @@ EOT $output->writeln('dist : ' . sprintf('[%s] %s %s', $package->getDistType(), $package->getDistUrl(), $package->getDistReference())); $output->writeln('names : ' . implode(', ', $package->getNames())); + if ($package->isAbandoned()) { + $replacement = ($package->getReplacementPackage() !== null) + ? ' The author suggests using the ' . $package->getReplacementPackage(). ' package instead.' + : null; + + $output->writeln( + sprintf('Attention: This package is abandoned and no longer maintained.%s', $replacement) + ); + } + if ($package->getSupport()) { $output->writeln("\nsupport"); foreach ($package->getSupport() as $type => $value) { diff --git a/src/Composer/Command/StatusCommand.php b/src/Composer/Command/StatusCommand.php index a125bdd3c..65662c048 100644 --- a/src/Composer/Command/StatusCommand.php +++ b/src/Composer/Command/StatusCommand.php @@ -84,7 +84,7 @@ EOT foreach ($errors as $path => $changes) { if ($input->getOption('verbose')) { $indentedChanges = implode("\n", array_map(function ($line) { - return ' ' . $line; + return ' ' . ltrim($line); }, explode("\n", $changes))); $output->writeln(''.$path.':'); $output->writeln($indentedChanges); diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index c3c90b94d..460075f0f 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -41,11 +41,15 @@ class UpdateCommand extends Command new InputOption('lock', null, InputOption::VALUE_NONE, 'Only updates the lock file hash to suppress warning about the lock file being out of date.'), new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Disables all plugins.'), new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'DEPRECATED: Use no-plugins instead.'), + new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Add also all dependencies of whitelisted packages to the whitelist.'), 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('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'), + new InputOption('prefer-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies.'), + new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest versions of dependencies.'), )) ->setHelp(<<update command reads the composer.json file from the @@ -58,6 +62,11 @@ To limit the update operation to a few packages, you can list the package(s) you want to update as such: php composer.phar update vendor/package1 foo/mypackage [...] + +You may also use an asterisk (*) pattern to limit the update operation to package(s) +from a specific vendor: + +php composer.phar update vendor/package1 foo/* [...] EOT ) ; @@ -70,6 +79,10 @@ EOT $input->setOption('no-plugins', true); } + if ($input->getOption('dev')) { + $output->writeln('You are using the deprecated option "dev". Dev packages are installed by default now.'); + } + $composer = $this->getComposer(true, $input->getOption('no-plugins')); $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $io = $this->getIO(); @@ -101,7 +114,7 @@ EOT $preferDist = $input->getOption('prefer-dist'); } - $optimize = $input->getOption('optimize-autoloader') || $config->get('optimize-autoloader'); + $optimize = $input->getOption('optimize-autoloader') || $config->get('optimize-autoloader') || $config->get('classmap-authoritative'); $install ->setDryRun($input->getOption('dry-run')) @@ -109,11 +122,15 @@ EOT ->setPreferSource($preferSource) ->setPreferDist($preferDist) ->setDevMode(!$input->getOption('no-dev')) + ->setDumpAutoloader(!$input->getOption('no-autoloader')) ->setRunScripts(!$input->getOption('no-scripts')) ->setOptimizeAutoloader($optimize) ->setUpdate(true) ->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $input->getArgument('packages')) ->setWhitelistDependencies($input->getOption('with-dependencies')) + ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) + ->setPreferStable($input->getOption('prefer-stable')) + ->setPreferLowest($input->getOption('prefer-lowest')) ; if ($input->getOption('no-plugins')) { diff --git a/src/Composer/Command/ValidateCommand.php b/src/Composer/Command/ValidateCommand.php index 38a524125..e7e0860e1 100644 --- a/src/Composer/Command/ValidateCommand.php +++ b/src/Composer/Command/ValidateCommand.php @@ -12,9 +12,11 @@ namespace Composer\Command; +use Composer\Package\Loader\ValidatingArrayLoader; use Composer\Util\ConfigValidator; -use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -34,6 +36,7 @@ class ValidateCommand extends Command ->setName('validate') ->setDescription('Validates a composer.json') ->setDefinition(array( + new InputOption('no-check-all', null, InputOption::VALUE_NONE, 'Do not make a complete validation'), new InputArgument('file', InputArgument::OPTIONAL, 'path to composer.json file', './composer.json') )) ->setHelp(<<getIO()); - list($errors, $publishErrors, $warnings) = $validator->validate($file); + $checkAll = $input->getOption('no-check-all') ? 0 : ValidatingArrayLoader::CHECK_ALL; + list($errors, $publishErrors, $warnings) = $validator->validate($file, $checkAll); // output errors/warnings if (!$errors && !$publishErrors && !$warnings) { diff --git a/src/Composer/Compiler.php b/src/Composer/Compiler.php index 41c30d6d1..8697d8caf 100644 --- a/src/Composer/Compiler.php +++ b/src/Composer/Compiler.php @@ -12,6 +12,7 @@ namespace Composer; +use Composer\Json\JsonFile; use Symfony\Component\Finder\Finder; use Symfony\Component\Process\Process; @@ -24,6 +25,7 @@ use Symfony\Component\Process\Process; class Compiler { private $version; + private $branchAliasVersion = ''; private $versionDate; /** @@ -48,13 +50,22 @@ class Compiler if ($process->run() != 0) { throw new \RuntimeException('Can\'t run git log. You must ensure to run compile from composer git repository clone and that git binary is available.'); } + $date = new \DateTime(trim($process->getOutput())); $date->setTimezone(new \DateTimeZone('UTC')); $this->versionDate = $date->format('Y-m-d H:i:s'); - $process = new Process('git describe --tags HEAD'); + $process = new Process('git describe --tags --exact-match HEAD'); if ($process->run() == 0) { $this->version = trim($process->getOutput()); + } else { + // get branch-alias defined in composer.json for dev-master (if any) + $localConfig = __DIR__.'/../../composer.json'; + $file = new JsonFile($localConfig); + $localConfig = $file->read(); + if (isset($localConfig['extra']['branch-alias']['dev-master'])) { + $this->branchAliasVersion = $localConfig['extra']['branch-alias']['dev-master']; + } } $phar = new \Phar($pharFile, 0, 'composer.phar'); @@ -138,6 +149,7 @@ class Compiler if ($path === 'src/Composer/Composer.php') { $content = str_replace('@package_version@', $this->version, $content); + $content = str_replace('@package_branch_alias_version@', $this->branchAliasVersion, $content); $content = str_replace('@release_date@', $this->versionDate, $content); } @@ -200,6 +212,16 @@ class Compiler * the license that is located at the bottom of this file. */ +// Avoid APC causing random fatal errors per https://github.com/composer/composer/issues/264 +if (extension_loaded('apc') && ini_get('apc.enable_cli') && ini_get('apc.cache_by_default')) { + if (version_compare(phpversion('apc'), '3.0.12', '>=')) { + ini_set('apc.cache_by_default', 0); + } else { + fwrite(STDERR, 'Warning: APC <= 3.0.12 may cause fatal errors when running composer commands.'.PHP_EOL); + fwrite(STDERR, 'Update APC, or set apc.enable_cli or apc.cache_by_default to 0 in your php.ini.'.PHP_EOL); + } +} + Phar::mapPhar('composer.phar'); EOF; diff --git a/src/Composer/Composer.php b/src/Composer/Composer.php index 0d1e0aa89..c874a0796 100644 --- a/src/Composer/Composer.php +++ b/src/Composer/Composer.php @@ -29,6 +29,7 @@ use Composer\Autoload\AutoloadGenerator; class Composer { const VERSION = '@package_version@'; + const BRANCH_ALIAS_VERSION = '@package_branch_alias_version@'; const RELEASE_DATE = '@release_date@'; /** @@ -67,7 +68,7 @@ class Composer private $config; /** - * @var EventDispatcher\EventDispatcher + * @var EventDispatcher */ private $eventDispatcher; @@ -190,7 +191,7 @@ class Composer } /** - * @param EventDispatcher\EventDispatcher $eventDispatcher + * @param EventDispatcher $eventDispatcher */ public function setEventDispatcher(EventDispatcher $eventDispatcher) { @@ -198,7 +199,7 @@ class Composer } /** - * @return EventDispatcher\EventDispatcher + * @return EventDispatcher */ public function getEventDispatcher() { diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 087949ef8..b6d942c51 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -19,12 +19,14 @@ use Composer\Config\ConfigSourceInterface; */ class Config { + const RELATIVE_PATHS = 1; + public static $defaultConfig = array( 'process-timeout' => 300, 'use-include-path' => false, 'preferred-install' => 'auto', 'notify-on-install' => true, - 'github-protocols' => array('git', 'https'), + 'github-protocols' => array('git', 'https', 'ssh'), 'vendor-dir' => 'vendor', 'bin-dir' => '{$vendor-dir}/bin', 'cache-dir' => '{$home}/cache', @@ -37,8 +39,14 @@ class Config 'discard-changes' => false, 'autoloader-suffix' => null, 'optimize-autoloader' => false, + 'classmap-authoritative' => false, 'prepend-autoloader' => true, 'github-domains' => array('github.com'), + 'github-expose-hostname' => true, + 'store-auths' => 'prompt', + // valid keys without defaults (auth config stuff): + // github-oauth + // http-basic ); public static $defaultRepositories = array( @@ -50,14 +58,22 @@ class Config ); private $config; + private $baseDir; private $repositories; private $configSource; + private $authConfigSource; + private $useEnvironment; - public function __construct() + /** + * @param boolean $useEnvironment Use COMPOSER_ environment variables to replace config settings + */ + public function __construct($useEnvironment = true, $baseDir = null) { // load defaults $this->config = static::$defaultConfig; $this->repositories = static::$defaultRepositories; + $this->useEnvironment = (bool) $useEnvironment; + $this->baseDir = $baseDir; } public function setConfigSource(ConfigSourceInterface $source) @@ -70,17 +86,27 @@ class Config return $this->configSource; } + public function setAuthConfigSource(ConfigSourceInterface $source) + { + $this->authConfigSource = $source; + } + + public function getAuthConfigSource() + { + return $this->authConfigSource; + } + /** * Merges new config values with the existing ones (overriding) * * @param array $config */ - public function merge(array $config) + public function merge($config) { // override defaults with given config if (!empty($config['config']) && is_array($config['config'])) { foreach ($config['config'] as $key => $val) { - if (in_array($key, array('github-oauth')) && isset($this->config[$key])) { + if (in_array($key, array('github-oauth', 'http-basic')) && isset($this->config[$key])) { $this->config[$key] = array_merge($this->config[$key], $val); } else { $this->config[$key] = $val; @@ -99,7 +125,7 @@ class Config } // disable a repository with an anonymous {"name": false} repo - if (1 === count($repository) && false === current($repository)) { + if (is_array($repository) && 1 === count($repository) && false === current($repository)) { unset($this->repositories[key($repository)]); continue; } @@ -127,10 +153,11 @@ class Config * Returns a setting * * @param string $key + * @param int $flags Options (see class constants) * @throws \RuntimeException * @return mixed */ - public function get($key) + public function get($key, $flags = 0) { switch ($key) { case 'vendor-dir': @@ -143,7 +170,14 @@ class Config // convert foo-bar to COMPOSER_FOO_BAR and check if it exists since it overrides the local config $env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_')); - return rtrim($this->process(getenv($env) ?: $this->config[$key]), '/\\'); + $val = rtrim($this->process($this->getComposerEnv($env) ?: $this->config[$key], $flags), '/\\'); + $val = preg_replace('#^(\$HOME|~)(/|$)#', rtrim(getenv('HOME') ?: getenv('USERPROFILE'), '/\\') . '/', $val); + + if (substr($key, -4) !== '-dir') { + return $val; + } + + return ($flags & self::RELATIVE_PATHS == 1) ? $val : $this->realpath($val); case 'cache-ttl': return (int) $this->config[$key]; @@ -179,10 +213,10 @@ class Config return (int) $this->config['cache-ttl']; case 'home': - return rtrim($this->process($this->config[$key]), '/\\'); + return rtrim($this->process($this->config[$key], $flags), '/\\'); case 'discard-changes': - if ($env = getenv('COMPOSER_DISCARD_CHANGES')) { + if ($env = $this->getComposerEnv('COMPOSER_DISCARD_CHANGES')) { if (!in_array($env, array('stash', 'true', 'false', '1', '0'), true)) { throw new \RuntimeException( "Invalid value for COMPOSER_DISCARD_CHANGES: {$env}. Expected 1, 0, true, false or stash" @@ -206,7 +240,7 @@ class Config case 'github-protocols': if (reset($this->config['github-protocols']) === 'http') { - throw new \RuntimeException('The http protocol for github is not available anymore, update your config\'s github-protocols to use "https" or "git"'); + throw new \RuntimeException('The http protocol for github is not available anymore, update your config\'s github-protocols to use "https", "git" or "ssh"'); } return $this->config[$key]; @@ -216,17 +250,17 @@ class Config return null; } - return $this->process($this->config[$key]); + return $this->process($this->config[$key], $flags); } } - public function all() + public function all($flags = 0) { $all = array( 'repositories' => $this->getRepositories(), ); foreach (array_keys($this->config) as $key) { - $all['config'][$key] = $this->get($key); + $all['config'][$key] = $this->get($key, $flags); } return $all; @@ -254,10 +288,11 @@ class Config /** * Replaces {$refs} inside a config string * - * @param string a config string that can contain {$refs-to-other-config} + * @param string $value a config string that can contain {$refs-to-other-config} + * @param int $flags Options (see class constants) * @return string */ - private function process($value) + private function process($value, $flags) { $config = $this; @@ -265,8 +300,43 @@ class Config return $value; } - return preg_replace_callback('#\{\$(.+)\}#', function ($match) use ($config) { - return $config->get($match[1]); + return preg_replace_callback('#\{\$(.+)\}#', function ($match) use ($config, $flags) { + return $config->get($match[1], $flags); }, $value); } + + /** + * Turns relative paths in absolute paths without realpath() + * + * Since the dirs might not exist yet we can not call realpath or it will fail. + * + * @param string $path + * @return string + */ + private function realpath($path) + { + if (substr($path, 0, 1) === '/' || substr($path, 1, 1) === ':') { + return $path; + } + + return $this->baseDir . '/' . $path; + } + + /** + * Reads the value of a Composer environment variable + * + * This should be used to read COMPOSER_ environment variables + * that overload config values. + * + * @param string $var + * @return string|boolean + */ + private function getComposerEnv($var) + { + if ($this->useEnvironment) { + return getenv($var); + } + + return false; + } } diff --git a/src/Composer/Config/ConfigSourceInterface.php b/src/Composer/Config/ConfigSourceInterface.php index e1478dbbb..edd3dff8a 100644 --- a/src/Composer/Config/ConfigSourceInterface.php +++ b/src/Composer/Config/ConfigSourceInterface.php @@ -66,4 +66,11 @@ interface ConfigSourceInterface * @param string $name Name */ public function removeLink($type, $name); + + /** + * Gives a user-friendly name to this source (file path or so) + * + * @return string + */ + public function getName(); } diff --git a/src/Composer/Config/JsonConfigSource.php b/src/Composer/Config/JsonConfigSource.php index 5223eb5d2..4fba2943c 100644 --- a/src/Composer/Config/JsonConfigSource.php +++ b/src/Composer/Config/JsonConfigSource.php @@ -23,17 +23,34 @@ use Composer\Json\JsonManipulator; */ class JsonConfigSource implements ConfigSourceInterface { + /** + * @var \Composer\Json\JsonFile + */ private $file; - private $manipulator; + + /** + * @var bool + */ + private $authConfig; /** * Constructor * * @param JsonFile $file + * @param bool $authConfig */ - public function __construct(JsonFile $file) + public function __construct(JsonFile $file, $authConfig = false) { $this->file = $file; + $this->authConfig = $authConfig; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->file->getPath(); } /** @@ -62,7 +79,16 @@ class JsonConfigSource implements ConfigSourceInterface public function addConfigSetting($name, $value) { $this->manipulateJson('addConfigSetting', $name, $value, function (&$config, $key, $val) { - $config['config'][$key] = $val; + if ($key === 'github-oauth' || $key === 'http-basic') { + list($key, $host) = explode('.', $key, 2); + if ($this->authConfig) { + $config[$key][$host] = $val; + } else { + $config['config'][$key][$host] = $val; + } + } else { + $config['config'][$key] = $val; + } }); } @@ -72,7 +98,16 @@ class JsonConfigSource implements ConfigSourceInterface public function removeConfigSetting($name) { $this->manipulateJson('removeConfigSetting', $name, function (&$config, $key) { - unset($config['config'][$key]); + if ($key === 'github-oauth' || $key === 'http-basic') { + list($key, $host) = explode('.', $key, 2); + if ($this->authConfig) { + unset($config[$key][$host]); + } else { + unset($config['config'][$key][$host]); + } + } else { + unset($config['config'][$key]); + } }); } @@ -105,20 +140,34 @@ class JsonConfigSource implements ConfigSourceInterface if ($this->file->exists()) { $contents = file_get_contents($this->file->getPath()); + } elseif ($this->authConfig) { + $contents = "{\n}\n"; } else { $contents = "{\n \"config\": {\n }\n}\n"; } + $manipulator = new JsonManipulator($contents); $newFile = !$this->file->exists(); + // override manipulator method for auth config files + if ($this->authConfig && $method === 'addConfigSetting') { + $method = 'addSubNode'; + list($mainNode, $name) = explode('.', $args[0], 2); + $args = array($mainNode, $name, $args[1]); + } elseif ($this->authConfig && $method === 'removeConfigSetting') { + $method = 'removeSubNode'; + list($mainNode, $name) = explode('.', $args[0], 2); + $args = array($mainNode, $name); + } + // try to update cleanly if (call_user_func_array(array($manipulator, $method), $args)) { file_put_contents($this->file->getPath(), $manipulator->getContents()); } else { // on failed clean update, call the fallback and rewrite the whole file $config = $this->file->read(); - array_unshift($args, $config); + $this->arrayUnshiftRef($args, $config); call_user_func_array($fallback, $args); $this->file->write($config); } @@ -127,4 +176,19 @@ class JsonConfigSource implements ConfigSourceInterface @chmod($this->file->getPath(), 0600); } } + + /** + * Prepend a reference to an element to the beginning of an array. + * + * @param array $array + * @param mixed $value + * @return array + */ + private function arrayUnshiftRef(&$array, &$value) + { + $return = array_unshift($array, ''); + $array[0] = &$value; + + return $return; + } } diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 9d622cf67..1c8246f88 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -56,11 +56,11 @@ class Application extends BaseApplication public function __construct() { - if (function_exists('ini_set')) { + if (function_exists('ini_set') && extension_loaded('xdebug')) { ini_set('xdebug.show_exception_trace', false); ini_set('xdebug.scream', false); - } + if (function_exists('date_default_timezone_set') && function_exists('date_default_timezone_get')) { date_default_timezone_set(@date_default_timezone_get()); } @@ -94,9 +94,18 @@ class Application extends BaseApplication $output->writeln('Composer only officially supports PHP 5.3.2 and above, you will most likely encounter problems with your PHP '.PHP_VERSION.', upgrading is strongly recommended.'); } - if (defined('COMPOSER_DEV_WARNING_TIME') && $this->getCommandName($input) !== 'self-update' && $this->getCommandName($input) !== 'selfupdate') { - if (time() > COMPOSER_DEV_WARNING_TIME) { - $output->writeln(sprintf('Warning: This development build of composer is over 30 days old. It is recommended to update it by running "%s self-update" to get the latest version.', $_SERVER['PHP_SELF'])); + if (defined('COMPOSER_DEV_WARNING_TIME')) { + $commandName = ''; + if ($name = $this->getCommandName($input)) { + try { + $commandName = $this->find($name)->getName(); + } catch (\InvalidArgumentException $e) { + } + } + if ($commandName !== 'self-update' && $commandName !== 'selfupdate') { + if (time() > COMPOSER_DEV_WARNING_TIME) { + $output->writeln(sprintf('Warning: This development build of composer is over 30 days old. It is recommended to update it by running "%s self-update" to get the latest version.', $_SERVER['PHP_SELF'])); + } } } @@ -104,14 +113,34 @@ class Application extends BaseApplication $input->setInteractive(false); } - if ($input->hasParameterOption('--profile')) { - $startTime = microtime(true); - $this->io->enableDebugging($startTime); - } - + // switch working dir if ($newWorkDir = $this->getNewWorkingDir($input)) { $oldWorkingDir = getcwd(); chdir($newWorkDir); + if ($output->getVerbosity() >= 4) { + $output->writeln('Changed CWD to ' . getcwd()); + } + } + + // add non-standard scripts as own commands + $file = Factory::getComposerFile(); + if (is_file($file) && is_readable($file) && is_array($composer = json_decode(file_get_contents($file), true))) { + if (isset($composer['scripts']) && is_array($composer['scripts'])) { + foreach ($composer['scripts'] as $script => $dummy) { + if (!defined('Composer\Script\ScriptEvents::'.str_replace('-', '_', strtoupper($script)))) { + if ($this->has($script)) { + $output->writeln('A script named '.$script.' would override a native Composer function and has been skipped'); + } else { + $this->add(new Command\ScriptAliasCommand($script)); + } + } + } + } + } + + if ($input->hasParameterOption('--profile')) { + $startTime = microtime(true); + $this->io->enableDebugging($startTime); } $result = parent::doRun($input, $output); @@ -129,6 +158,7 @@ class Application extends BaseApplication /** * @param InputInterface $input + * @return string * @throws \RuntimeException */ private function getNewWorkingDir(InputInterface $input) @@ -147,18 +177,30 @@ class Application extends BaseApplication public function renderException($exception, $output) { try { - $composer = $this->getComposer(false); + $composer = $this->getComposer(false, true); if ($composer) { $config = $composer->getConfig(); $minSpaceFree = 1024*1024; if ((($df = @disk_free_space($dir = $config->get('home'))) !== false && $df < $minSpaceFree) || (($df = @disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree) + || (($df = @disk_free_space($dir = sys_get_temp_dir())) !== false && $df < $minSpaceFree) ) { $output->writeln('The disk hosting '.$dir.' is full, this may be the cause of the following exception'); } } - } catch (\Exception $e) {} + } catch (\Exception $e) { + } + + if (defined('PHP_WINDOWS_VERSION_BUILD') && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) { + $output->writeln('The following exception may be caused by a stale entry in your cmd.exe AutoRun'); + $output->writeln('Check https://getcomposer.org/doc/articles/troubleshooting.md#-the-system-cannot-find-the-path-specified-windows- for details'); + } + + if (false !== strpos($exception->getMessage(), 'fork failed - Cannot allocate memory')) { + $output->writeln('The following exception is caused by a lack of memory and not having swap configured'); + $output->writeln('Check https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details'); + } return parent::renderException($exception, $output); } @@ -184,12 +226,19 @@ class Application extends BaseApplication $message = $e->getMessage() . ':' . PHP_EOL . $errors; throw new JsonValidationException($message); } - } return $this->composer; } + /** + * Removes the cached composer instance + */ + public function resetComposer() + { + $this->composer = null; + } + /** * @return IOInterface */ @@ -227,6 +276,9 @@ class Application extends BaseApplication $commands[] = new Command\RunScriptCommand(); $commands[] = new Command\LicensesCommand(); $commands[] = new Command\GlobalCommand(); + $commands[] = new Command\ClearCacheCommand(); + $commands[] = new Command\RemoveCommand(); + $commands[] = new Command\HomeCommand(); if ('phar:' === substr(__FILE__, 0, 5)) { $commands[] = new Command\SelfUpdateCommand(); @@ -240,6 +292,16 @@ class Application extends BaseApplication */ public function getLongVersion() { + if (Composer::BRANCH_ALIAS_VERSION) { + return sprintf( + '%s version %s (%s) %s', + $this->getName(), + Composer::BRANCH_ALIAS_VERSION, + $this->getVersion(), + Composer::RELEASE_DATE + ); + } + return parent::getLongVersion() . ' ' . Composer::RELEASE_DATE; } diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index 190829213..5ae6b2c31 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -24,10 +24,12 @@ use Composer\Package\LinkConstraint\VersionConstraint; class DefaultPolicy implements PolicyInterface { private $preferStable; + private $preferLowest; - public function __construct($preferStable = false) + public function __construct($preferStable = false, $preferLowest = false) { $this->preferStable = $preferStable; + $this->preferLowest = $preferLowest; } public function versionCompare(PackageInterface $a, PackageInterface $b, $operator) @@ -42,11 +44,11 @@ class DefaultPolicy implements PolicyInterface return $constraint->matchSpecific($version, true); } - public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package) + public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package, $mustMatchName = false) { $packages = array(); - foreach ($pool->whatProvides($package->getName()) as $candidate) { + foreach ($pool->whatProvides($package->getName(), null, $mustMatchName) as $candidate) { if ($candidate !== $package) { $packages[] = $candidate; } @@ -151,18 +153,18 @@ class DefaultPolicy implements PolicyInterface } // priority equal, sort by package id to make reproducible - if ($a->getId() === $b->getId()) { + if ($a->id === $b->id) { return 0; } - return ($a->getId() < $b->getId()) ? -1 : 1; + return ($a->id < $b->id) ? -1 : 1; } - if (isset($installedMap[$a->getId()])) { + if (isset($installedMap[$a->id])) { return -1; } - if (isset($installedMap[$b->getId()])) { + if (isset($installedMap[$b->id])) { return 1; } @@ -195,6 +197,7 @@ class DefaultPolicy implements PolicyInterface protected function pruneToBestVersion(Pool $pool, $literals) { + $operator = $this->preferLowest ? '<' : '>'; $bestLiterals = array($literals[0]); $bestPackage = $pool->literalToPackage($literals[0]); foreach ($literals as $i => $literal) { @@ -204,7 +207,7 @@ class DefaultPolicy implements PolicyInterface $package = $pool->literalToPackage($literal); - if ($this->versionCompare($package, $bestPackage, '>')) { + if ($this->versionCompare($package, $bestPackage, $operator)) { $bestPackage = $package; $bestLiterals = array($literal); } elseif ($this->versionCompare($package, $bestPackage, '==')) { @@ -215,26 +218,6 @@ class DefaultPolicy implements PolicyInterface return $bestLiterals; } - protected function selectNewestPackages(array $installedMap, array $literals) - { - $maxLiterals = array($literals[0]); - $maxPackage = $literals[0]->getPackage(); - foreach ($literals as $i => $literal) { - if (0 === $i) { - continue; - } - - if ($this->versionCompare($literal->getPackage(), $maxPackage, '>')) { - $maxPackage = $literal->getPackage(); - $maxLiterals = array($literal); - } elseif ($this->versionCompare($literal->getPackage(), $maxPackage, '==')) { - $maxLiterals[] = $literal; - } - } - - return $maxLiterals; - } - /** * Assumes that installed packages come first and then all highest priority packages */ @@ -247,7 +230,7 @@ class DefaultPolicy implements PolicyInterface foreach ($literals as $literal) { $package = $pool->literalToPackage($literal); - if (isset($installedMap[$package->getId()])) { + if (isset($installedMap[$package->id])) { $selected[] = $literal; continue; } diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index a1bba4f3a..e0d2d8a13 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -15,7 +15,6 @@ namespace Composer\DependencyResolver; use Composer\Package\BasePackage; use Composer\Package\AliasPackage; use Composer\Package\Version\VersionParser; -use Composer\Package\Link; use Composer\Package\LinkConstraint\LinkConstraintInterface; use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\LinkConstraint\EmptyConstraint; @@ -23,8 +22,8 @@ use Composer\Repository\RepositoryInterface; use Composer\Repository\CompositeRepository; use Composer\Repository\ComposerRepository; use Composer\Repository\InstalledRepositoryInterface; -use Composer\Repository\StreamableRepositoryInterface; use Composer\Repository\PlatformRepository; +use Composer\Package\PackageInterface; /** * A package pool contains repositories that provide packages. @@ -45,11 +44,13 @@ class Pool protected $providerRepos = array(); protected $packages = array(); protected $packageByName = array(); + protected $packageByExactName = array(); protected $acceptableStabilities; protected $stabilityFlags; protected $versionParser; protected $providerCache = array(); protected $filterRequires; + protected $whitelist = null; protected $id = 1; public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) @@ -66,6 +67,12 @@ class Pool $this->filterRequires = $filterRequires; } + public function setWhitelist($whitelist) + { + $this->whitelist = $whitelist; + $this->providerCache = array(); + } + /** * Adds a repository and its packages to this package pool * @@ -89,76 +96,6 @@ class Pool $this->providerRepos[] = $repo; $repo->setRootAliases($rootAliases); $repo->resetPackageIds(); - } elseif ($repo instanceof StreamableRepositoryInterface) { - foreach ($repo->getMinimalPackages() as $package) { - $name = $package['name']; - $version = $package['version']; - $stability = VersionParser::parseStability($version); - - // collect names - $names = array( - $name => true, - ); - if (isset($package['provide'])) { - foreach ($package['provide'] as $target => $constraint) { - $names[$target] = true; - } - } - if (isset($package['replace'])) { - foreach ($package['replace'] as $target => $constraint) { - $names[$target] = true; - } - } - $names = array_keys($names); - - if ($exempt || $this->isPackageAcceptable($names, $stability)) { - $package['id'] = $this->id++; - $package['stability'] = $stability; - $this->packages[] = $package; - - foreach ($names as $provided) { - $this->packageByName[$provided][$package['id']] = $this->packages[$this->id - 2]; - } - - // handle root package aliases - unset($rootAliasData); - if (isset($rootAliases[$name][$version])) { - $rootAliasData = $rootAliases[$name][$version]; - } elseif (isset($package['alias_normalized']) && isset($rootAliases[$name][$package['alias_normalized']])) { - $rootAliasData = $rootAliases[$name][$package['alias_normalized']]; - } - - if (isset($rootAliasData)) { - $alias = $package; - unset($alias['raw']); - $alias['version'] = $rootAliasData['alias_normalized']; - $alias['alias'] = $rootAliasData['alias']; - $alias['alias_of'] = $package['id']; - $alias['id'] = $this->id++; - $alias['root_alias'] = true; - $this->packages[] = $alias; - - foreach ($names as $provided) { - $this->packageByName[$provided][$alias['id']] = $this->packages[$this->id - 2]; - } - } - - // handle normal package aliases - if (isset($package['alias'])) { - $alias = $package; - unset($alias['raw']); - $alias['version'] = $package['alias_normalized']; - $alias['alias'] = $package['alias']; - $alias['alias_of'] = $package['id']; - $alias['id'] = $this->id++; - $this->packages[] = $alias; - - foreach ($names as $provided) { - $this->packageByName[$provided][$alias['id']] = $this->packages[$this->id - 2]; - } - } - } - } } else { foreach ($repo->getPackages() as $package) { $names = $package->getNames(); @@ -166,6 +103,7 @@ class Pool 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; @@ -184,6 +122,7 @@ class Pool $package->getRepository()->addPackage($aliasPackage); $this->packages[] = $aliasPackage; + $this->packageByExactName[$aliasPackage->getName()][$aliasPackage->id] = $aliasPackage; foreach ($aliasPackage->getNames() as $name) { $this->packageByName[$name][] = $aliasPackage; @@ -214,44 +153,54 @@ class Pool */ public function packageById($id) { - return $this->ensurePackageIsLoaded($this->packages[$id - 1]); + return $this->packages[$id - 1]; } /** * Searches all packages providing the given package name and match the constraint * - * @param string $name The package name to be searched for - * @param LinkConstraintInterface $constraint A constraint that all returned - * packages must match or null to return all - * @return array A set of packages + * @param string $name The package name to be searched for + * @param LinkConstraintInterface $constraint A constraint that all returned + * packages must match or null to return all + * @param bool $mustMatchName Whether the name of returned packages + * must match the given name + * @return PackageInterface[] A set of packages */ - public function whatProvides($name, LinkConstraintInterface $constraint = null) + public function whatProvides($name, LinkConstraintInterface $constraint = null, $mustMatchName = false) { - if (isset($this->providerCache[$name][(string) $constraint])) { - return $this->providerCache[$name][(string) $constraint]; + $key = ((int) $mustMatchName).$constraint; + if (isset($this->providerCache[$name][$key])) { + return $this->providerCache[$name][$key]; } - return $this->providerCache[$name][(string) $constraint] = $this->computeWhatProvides($name, $constraint); + return $this->providerCache[$name][$key] = $this->computeWhatProvides($name, $constraint, $mustMatchName); } /** * @see whatProvides */ - private function computeWhatProvides($name, $constraint) + private function computeWhatProvides($name, $constraint, $mustMatchName = false) { $candidates = array(); foreach ($this->providerRepos as $repo) { foreach ($repo->whatProvides($this, $name) as $candidate) { $candidates[] = $candidate; - if ($candidate->getId() < 1) { + if ($candidate->id < 1) { $candidate->setId($this->id++); $this->packages[$this->id - 2] = $candidate; } } } - if (isset($this->packageByName[$name])) { + if ($mustMatchName) { + $candidates = array_filter($candidates, function ($candidate) use ($name) { + return $candidate->getName() == $name; + }); + if (isset($this->packageByExactName[$name])) { + $candidates = array_merge($candidates, $this->packageByExactName[$name]); + } + } elseif (isset($this->packageByName[$name])) { $candidates = array_merge($candidates, $this->packageByName[$name]); } @@ -259,6 +208,20 @@ class Pool $nameMatch = false; foreach ($candidates as $candidate) { + $aliasOfCandidate = null; + + // alias packages are not white listed, make sure that the package + // being aliased is white listed + if ($candidate instanceof AliasPackage) { + $aliasOfCandidate = $candidate->getAliasOf(); + } + + if ($this->whitelist !== null && ( + (!($candidate instanceof AliasPackage) && !isset($this->whitelist[$candidate->id])) || + ($candidate instanceof AliasPackage && !isset($this->whitelist[$aliasOfCandidate->id])) + )) { + continue; + } switch ($this->match($candidate, $name, $constraint)) { case self::MATCH_NONE: break; @@ -269,15 +232,15 @@ class Pool case self::MATCH: $nameMatch = true; - $matches[] = $this->ensurePackageIsLoaded($candidate); + $matches[] = $candidate; break; case self::MATCH_PROVIDE: - $provideMatches[] = $this->ensurePackageIsLoaded($candidate); + $provideMatches[] = $candidate; break; case self::MATCH_REPLACE: - $matches[] = $this->ensurePackageIsLoaded($candidate); + $matches[] = $candidate; break; case self::MATCH_FILTERED: @@ -312,7 +275,7 @@ class Pool { $package = $this->literalToPackage($literal); - if (isset($installedMap[$package->getId()])) { + if (isset($installedMap[$package->id])) { $prefix = ($literal > 0 ? 'keep' : 'remove'); } else { $prefix = ($literal > 0 ? 'install' : 'don\'t install'); @@ -338,28 +301,6 @@ class Pool return false; } - private function ensurePackageIsLoaded($data) - { - if (is_array($data)) { - if (isset($data['alias_of'])) { - $aliasOf = $this->packageById($data['alias_of']); - $package = $this->packages[$data['id'] - 1] = $data['repo']->loadAliasPackage($data, $aliasOf); - $package->setRootPackageAlias(!empty($data['root_alias'])); - } else { - $package = $this->packages[$data['id'] - 1] = $data['repo']->loadPackage($data); - } - - foreach ($package->getNames() as $name) { - $this->packageByName[$name][$data['id']] = $package; - } - $package->setId($data['id']); - - return $package; - } - - return $data; - } - /** * Checks if the package matches the given constraint directly or through * provided or replaced packages @@ -371,19 +312,10 @@ class Pool */ private function match($candidate, $name, LinkConstraintInterface $constraint = null) { - // handle array packages - if (is_array($candidate)) { - $candidateName = $candidate['name']; - $candidateVersion = $candidate['version']; - $isDev = $candidate['stability'] === 'dev'; - $isAlias = isset($candidate['alias_of']); - } else { - // handle object packages - $candidateName = $candidate->getName(); - $candidateVersion = $candidate->getVersion(); - $isDev = $candidate->getStability() === 'dev'; - $isAlias = $candidate instanceof AliasPackage; - } + $candidateName = $candidate->getName(); + $candidateVersion = $candidate->getVersion(); + $isDev = $candidate->getStability() === 'dev'; + $isAlias = $candidate instanceof AliasPackage; if (!$isDev && !$isAlias && isset($this->filterRequires[$name])) { $requireFilter = $this->filterRequires[$name]; @@ -401,17 +333,8 @@ class Pool return self::MATCH_NAME; } - if (is_array($candidate)) { - $provides = isset($candidate['provide']) - ? $this->versionParser->parseLinks($candidateName, $candidateVersion, 'provides', $candidate['provide']) - : array(); - $replaces = isset($candidate['replace']) - ? $this->versionParser->parseLinks($candidateName, $candidateVersion, 'replaces', $candidate['replace']) - : array(); - } else { - $provides = $candidate->getProvides(); - $replaces = $candidate->getReplaces(); - } + $provides = $candidate->getProvides(); + $replaces = $candidate->getReplaces(); // aliases create multiple replaces/provides for one target so they can not use the shortcut below if (isset($replaces[0]) || isset($provides[0])) { diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 56ac867c4..9373a7b7b 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -80,7 +80,13 @@ class Problem $rule = $reason['rule']; $job = $reason['job']; - if ($job && $job['cmd'] === 'install' && empty($job['packages'])) { + if (isset($job['constraint'])) { + $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); + } else { + $packages = array(); + } + + if ($job && $job['cmd'] === 'install' && empty($packages)) { // handle php extensions if (0 === stripos($job['packageName'], 'ext-')) { $ext = substr($job['packageName'], 4); @@ -124,7 +130,7 @@ class Problem $messages[] = $this->jobToText($job); } elseif ($rule) { if ($rule instanceof Rule) { - $messages[] = $rule->getPrettyString($installedMap); + $messages[] = $rule->getPrettyString($this->pool, $installedMap); } } } @@ -161,18 +167,25 @@ class Problem { switch ($job['cmd']) { case 'install': - if (!$job['packages']) { + $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); + if (!$packages) { return 'No package found to satisfy install request for '.$job['packageName'].$this->constraintToText($job['constraint']); } - return 'Installation request for '.$job['packageName'].$this->constraintToText($job['constraint']).' -> satisfiable by '.$this->getPackageList($job['packages']).'.'; + return 'Installation request for '.$job['packageName'].$this->constraintToText($job['constraint']).' -> satisfiable by '.$this->getPackageList($packages).'.'; case 'update': return 'Update request for '.$job['packageName'].$this->constraintToText($job['constraint']).'.'; case 'remove': return 'Removal request for '.$job['packageName'].$this->constraintToText($job['constraint']).''; } - return 'Job(cmd='.$job['cmd'].', target='.$job['packageName'].', packages=['.$this->getPackageList($job['packages']).'])'; + if (isset($job['constraint'])) { + $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); + } else { + $packages = array(); + } + + return 'Job(cmd='.$job['cmd'].', target='.$job['packageName'].', packages=['.$this->getPackageList($packages).'])'; } protected function getPackageList($packages) diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 92c8aa175..79904a3a1 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -43,22 +43,31 @@ class Request $this->addJob($packageName, 'remove', $constraint); } - protected function addJob($packageName, $cmd, LinkConstraintInterface $constraint = null) + /** + * Mark an existing package as being installed and having to remain installed + * + * These jobs will not be tempered with by the solver + */ + public function fix($packageName, LinkConstraintInterface $constraint = null) + { + $this->addJob($packageName, 'install', $constraint, true); + } + + protected function addJob($packageName, $cmd, LinkConstraintInterface $constraint = null, $fixed = false) { $packageName = strtolower($packageName); - $packages = $this->pool->whatProvides($packageName, $constraint); $this->jobs[] = array( - 'packages' => $packages, 'cmd' => $cmd, 'packageName' => $packageName, 'constraint' => $constraint, + 'fixed' => $fixed ); } public function updateAll() { - $this->jobs[] = array('cmd' => 'update-all', 'packages' => array()); + $this->jobs[] = array('cmd' => 'update-all'); } public function getJobs() diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index a47038431..cba9400ef 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -29,10 +29,13 @@ class Rule const RULE_LEARNED = 12; const RULE_PACKAGE_ALIAS = 13; - protected $pool; + /** + * READ-ONLY: The literals this rule consists of. + * @var array + */ + public $literals; protected $disabled; - protected $literals; protected $type; protected $id; protected $reason; @@ -42,10 +45,8 @@ class Rule protected $ruleHash; - public function __construct(Pool $pool, array $literals, $reason, $reasonData, $job = null) + public function __construct(array $literals, $reason, $reasonData, $job = null) { - $this->pool = $pool; - // sort all packages ascending by id sort($literals); @@ -160,6 +161,9 @@ class Rule return !$this->disabled; } + /** + * @deprecated Use public literals member + */ public function getLiterals() { return $this->literals; @@ -170,14 +174,14 @@ class Rule return 1 === count($this->literals); } - public function getPrettyString(array $installedMap = array()) + public function getPrettyString(Pool $pool, array $installedMap = array()) { $ruleText = ''; foreach ($this->literals as $i => $literal) { if ($i != 0) { $ruleText .= '|'; } - $ruleText .= $this->pool->literalToPrettyString($literal, $installedMap); + $ruleText .= $pool->literalToPrettyString($literal, $installedMap); } switch ($this->reason) { @@ -191,24 +195,24 @@ class Rule return "Remove command rule ($ruleText)"; case self::RULE_PACKAGE_CONFLICT: - $package1 = $this->pool->literalToPackage($this->literals[0]); - $package2 = $this->pool->literalToPackage($this->literals[1]); + $package1 = $pool->literalToPackage($this->literals[0]); + $package2 = $pool->literalToPackage($this->literals[1]); - return $package1->getPrettyString().' conflicts with '.$this->formatPackagesUnique(array($package2)).'.'; + return $package1->getPrettyString().' conflicts with '.$this->formatPackagesUnique($pool, array($package2)).'.'; case self::RULE_PACKAGE_REQUIRES: $literals = $this->literals; $sourceLiteral = array_shift($literals); - $sourcePackage = $this->pool->literalToPackage($sourceLiteral); + $sourcePackage = $pool->literalToPackage($sourceLiteral); $requires = array(); foreach ($literals as $literal) { - $requires[] = $this->pool->literalToPackage($literal); + $requires[] = $pool->literalToPackage($literal); } $text = $this->reasonData->getPrettyString($sourcePackage); if ($requires) { - $text .= ' -> satisfiable by ' . $this->formatPackagesUnique($requires) . '.'; + $text .= ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $requires) . '.'; } else { $targetName = $this->reasonData->getTarget(); @@ -235,22 +239,24 @@ class Rule case self::RULE_INSTALLED_PACKAGE_OBSOLETES: return $ruleText; case self::RULE_PACKAGE_SAME_NAME: - return 'Can only install one of: ' . $this->formatPackagesUnique($this->literals) . '.'; + return 'Can only install one of: ' . $this->formatPackagesUnique($pool, $this->literals) . '.'; case self::RULE_PACKAGE_IMPLICIT_OBSOLETES: return $ruleText; case self::RULE_LEARNED: return 'Conclusion: '.$ruleText; case self::RULE_PACKAGE_ALIAS: return $ruleText; + default: + return '('.$ruleText.')'; } } - protected function formatPackagesUnique(array $packages) + protected function formatPackagesUnique($pool, array $packages) { $prepared = array(); foreach ($packages as $package) { if (!is_object($package)) { - $package = $this->pool->literalToPackage($package); + $package = $pool->literalToPackage($package); } $prepared[$package->getName()]['name'] = $package->getPrettyName(); $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion(); @@ -275,7 +281,7 @@ class Rule if ($i != 0) { $result .= '|'; } - $result .= $this->pool->literalToString($literal); + $result .= $literal; } $result .= ')'; diff --git a/src/Composer/DependencyResolver/RuleSet.php b/src/Composer/DependencyResolver/RuleSet.php index 05ab780ac..b9545123f 100644 --- a/src/Composer/DependencyResolver/RuleSet.php +++ b/src/Composer/DependencyResolver/RuleSet.php @@ -22,6 +22,13 @@ class RuleSet implements \IteratorAggregate, \Countable const TYPE_JOB = 1; const TYPE_LEARNED = 4; + /** + * READ-ONLY: Lookup table for rule id to rule object + * + * @var Rule[] + */ + public $ruleById; + protected static $types = array( -1 => 'UNKNOWN', self::TYPE_PACKAGE => 'PACKAGE', @@ -30,7 +37,6 @@ class RuleSet implements \IteratorAggregate, \Countable ); protected $rules; - protected $ruleById; protected $nextRuleId; protected $rulesByHash; @@ -144,17 +150,22 @@ class RuleSet implements \IteratorAggregate, \Countable return false; } - public function __toString() + public function getPrettyString(Pool $pool = null) { $string = "\n"; foreach ($this->rules as $type => $rules) { $string .= str_pad(self::$types[$type], 8, ' ') . ": "; foreach ($rules as $rule) { - $string .= $rule."\n"; + $string .= ($pool ? $rule->getPrettyString($pool) : $rule)."\n"; } $string .= "\n\n"; } return $string; } + + public function __toString() + { + return $this->getPrettyString(null); + } } diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index b40ce1a60..8ea24e742 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -14,6 +14,7 @@ namespace Composer\DependencyResolver; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; +use Composer\Repository\PlatformRepository; /** * @author Nils Adermann @@ -25,6 +26,8 @@ class RuleSetGenerator protected $rules; protected $jobs; protected $installedMap; + protected $whitelistedMap; + protected $addedMap; public function __construct(PolicyInterface $policy, Pool $pool) { @@ -38,27 +41,27 @@ class RuleSetGenerator * This rule is of the form (-A|B|C), where B and C are the providers of * one requirement of the package A. * - * @param PackageInterface $package The package with a requirement - * @param array $providers The providers of the requirement - * @param int $reason A RULE_* constant describing the - * reason for generating this rule - * @param mixed $reasonData Any data, e.g. the requirement name, - * that goes with the reason - * @return Rule The generated rule or null if tautological + * @param PackageInterface $package The package with a requirement + * @param array $providers The providers of the requirement + * @param int $reason A RULE_* constant describing the + * reason for generating this rule + * @param mixed $reasonData Any data, e.g. the requirement name, + * that goes with the reason + * @return Rule The generated rule or null if tautological */ protected function createRequireRule(PackageInterface $package, array $providers, $reason, $reasonData = null) { - $literals = array(-$package->getId()); + $literals = array(-$package->id); foreach ($providers as $provider) { // self fulfilling rule? if ($provider === $package) { return null; } - $literals[] = $provider->getId(); + $literals[] = $provider->id; } - return new Rule($this->pool, $literals, $reason, $reasonData); + return new Rule($literals, $reason, $reasonData); } /** @@ -67,20 +70,20 @@ class RuleSetGenerator * The rule is (A|B|C) with A, B and C different packages. If the given * set of packages is empty an impossible rule is generated. * - * @param array $packages The set of packages to choose from - * @param int $reason A RULE_* constant describing the reason for - * generating this rule - * @param array $job The job this rule was created from + * @param array $packages The set of packages to choose from + * @param int $reason A RULE_* constant describing the reason for + * generating this rule + * @param array $job The job this rule was created from * @return Rule The generated rule */ protected function createInstallOneOfRule(array $packages, $reason, $job) { $literals = array(); foreach ($packages as $package) { - $literals[] = $package->getId(); + $literals[] = $package->id; } - return new Rule($this->pool, $literals, $reason, $job['packageName'], $job); + return new Rule($literals, $reason, $job['packageName'], $job); } /** @@ -88,15 +91,15 @@ class RuleSetGenerator * * 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 + * @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 Rule($this->pool, array(-$package->getId()), $reason, $job['packageName'], $job); + return new Rule(array(-$package->id), $reason, $job['packageName'], $job); } /** @@ -105,13 +108,13 @@ class RuleSetGenerator * The rule for conflicting packages A and B is (-A|-B). A is called the issuer * and B the provider. * - * @param PackageInterface $issuer The package declaring the conflict - * @param PackageInterface $provider The package causing the conflict - * @param int $reason A RULE_* constant describing the - * reason for generating this rule - * @param mixed $reasonData Any data, e.g. the package name, that - * goes with the reason - * @return Rule The generated rule + * @param PackageInterface $issuer The package declaring the conflict + * @param PackageInterface $provider The package causing the conflict + * @param int $reason A RULE_* constant describing the + * reason for generating this rule + * @param mixed $reasonData Any data, e.g. the package name, that + * goes with the reason + * @return Rule The generated rule */ protected function createConflictRule(PackageInterface $issuer, PackageInterface $provider, $reason, $reasonData = null) { @@ -120,7 +123,7 @@ class RuleSetGenerator return null; } - return new Rule($this->pool, array(-$issuer->getId(), -$provider->getId()), $reason, $reasonData); + return new Rule(array(-$issuer->id, -$provider->id), $reason, $reasonData); } /** @@ -141,20 +144,59 @@ class RuleSetGenerator $this->rules->add($newRule, $type); } - protected function addRulesForPackage(PackageInterface $package) + protected function whitelistFromPackage(PackageInterface $package) + { + $workQueue = new \SplQueue; + $workQueue->enqueue($package); + + while (!$workQueue->isEmpty()) { + $package = $workQueue->dequeue(); + if (isset($this->whitelistedMap[$package->id])) { + continue; + } + + $this->whitelistedMap[$package->id] = true; + + foreach ($package->getRequires() as $link) { + $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint(), true); + + foreach ($possibleRequires as $require) { + $workQueue->enqueue($require); + } + } + + $obsoleteProviders = $this->pool->whatProvides($package->getName(), null, true); + + foreach ($obsoleteProviders as $provider) { + if ($provider === $package) { + continue; + } + + if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) { + $workQueue->enqueue($provider); + } + } + } + } + + protected function addRulesForPackage(PackageInterface $package, $ignorePlatformReqs) { $workQueue = new \SplQueue; $workQueue->enqueue($package); while (!$workQueue->isEmpty()) { $package = $workQueue->dequeue(); - if (isset($this->addedMap[$package->getId()])) { + if (isset($this->addedMap[$package->id])) { continue; } - $this->addedMap[$package->getId()] = true; + $this->addedMap[$package->id] = true; foreach ($package->getRequires() as $link) { + if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $link->getTarget())) { + continue; + } + $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, $possibleRequires, Rule::RULE_PACKAGE_REQUIRES, $link)); @@ -173,7 +215,7 @@ class RuleSetGenerator } // check obsoletes and implicit obsoletes of a package - $isInstalled = (isset($this->installedMap[$package->getId()])); + $isInstalled = (isset($this->installedMap[$package->id])); foreach ($package->getReplaces() as $link) { $obsoleteProviders = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); @@ -221,41 +263,46 @@ class RuleSetGenerator return $impossible; } - /** - * Adds all rules for all update packages of a given package - * - * @param PackageInterface $package Rules for this package's updates are to - * be added - */ - private function addRulesForUpdatePackages(PackageInterface $package) + protected function whitelistFromJobs() { - $updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package); - - foreach ($updates as $update) { - $this->addRulesForPackage($update); + foreach ($this->jobs as $job) { + switch ($job['cmd']) { + case 'install': + $packages = $this->pool->whatProvides($job['packageName'], $job['constraint'], true); + foreach ($packages as $package) { + $this->whitelistFromPackage($package); + } + break; + } } } - protected function addRulesForJobs() + protected function addRulesForJobs($ignorePlatformReqs) { foreach ($this->jobs as $job) { switch ($job['cmd']) { case 'install': - if ($job['packages']) { - foreach ($job['packages'] as $package) { - if (!isset($this->installedMap[$package->getId()])) { - $this->addRulesForPackage($package); + if (!$job['fixed'] && $ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) { + continue; + } + + $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($job['packages'], Rule::RULE_JOB_INSTALL, $job); + $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 - foreach ($job['packages'] as $package) { + $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); } @@ -264,18 +311,26 @@ class RuleSetGenerator } } - public function getRulesFor($jobs, $installedMap) + public function getRulesFor($jobs, $installedMap, $ignorePlatformReqs = false) { $this->jobs = $jobs; $this->rules = new RuleSet; $this->installedMap = $installedMap; + $this->whitelistedMap = array(); + foreach ($this->installedMap as $package) { + $this->whitelistFromPackage($package); + } + $this->whitelistFromJobs(); + + $this->pool->setWhitelist($this->whitelistedMap); + + $this->addedMap = array(); foreach ($this->installedMap as $package) { - $this->addRulesForPackage($package); - $this->addRulesForUpdatePackages($package); + $this->addRulesForPackage($package, $ignorePlatformReqs); } - $this->addRulesForJobs(); + $this->addRulesForJobs($ignorePlatformReqs); return $this->rules; } diff --git a/src/Composer/DependencyResolver/RuleWatchGraph.php b/src/Composer/DependencyResolver/RuleWatchGraph.php index 627a66eb3..2360c5219 100644 --- a/src/Composer/DependencyResolver/RuleWatchGraph.php +++ b/src/Composer/DependencyResolver/RuleWatchGraph.php @@ -69,11 +69,11 @@ class RuleWatchGraph * above example the rule was (-A|+B), then A turning true means that * B must now be decided true as well. * - * @param int $decidedLiteral The literal which was decided (A in our example) - * @param int $level The level at which the decision took place and at which - * all resulting decisions should be made. - * @param Decisions $decisions Used to check previous decisions and to - * register decisions resulting from propagation + * @param int $decidedLiteral The literal which was decided (A in our example) + * @param int $level The level at which the decision took place and at which + * all resulting decisions should be made. + * @param Decisions $decisions Used to check previous decisions and to + * register decisions resulting from propagation * @return Rule|null If a conflict is found the conflicting rule is returned */ public function propagateLiteral($decidedLiteral, $level, $decisions) @@ -95,7 +95,7 @@ class RuleWatchGraph $otherWatch = $node->getOtherWatch($literal); if (!$node->getRule()->isDisabled() && !$decisions->satisfy($otherWatch)) { - $ruleLiterals = $node->getRule()->getLiterals(); + $ruleLiterals = $node->getRule()->literals; $alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $decisions) { return $literal !== $ruleLiteral && diff --git a/src/Composer/DependencyResolver/RuleWatchNode.php b/src/Composer/DependencyResolver/RuleWatchNode.php index a08337f5e..cdbf6a00b 100644 --- a/src/Composer/DependencyResolver/RuleWatchNode.php +++ b/src/Composer/DependencyResolver/RuleWatchNode.php @@ -35,7 +35,7 @@ class RuleWatchNode { $this->rule = $rule; - $literals = $rule->getLiterals(); + $literals = $rule->literals; $this->watch1 = count($literals) > 0 ? $literals[0] : 0; $this->watch2 = count($literals) > 1 ? $literals[1] : 0; @@ -51,10 +51,10 @@ class RuleWatchNode */ public function watch2OnHighest(Decisions $decisions) { - $literals = $this->rule->getLiterals(); + $literals = $this->rule->literals; // if there are only 2 elements, both are being watched anyway - if ($literals < 3) { + if (count($literals) < 3) { return; } diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 0fc860c72..19e69913b 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -13,6 +13,7 @@ namespace Composer\DependencyResolver; use Composer\Repository\RepositoryInterface; +use Composer\Repository\PlatformRepository; /** * @author Nils Adermann @@ -55,13 +56,13 @@ class Solver $rulesCount = count($this->rules); for ($ruleIndex = 0; $ruleIndex < $rulesCount; $ruleIndex++) { - $rule = $this->rules->ruleById($ruleIndex); + $rule = $this->rules->ruleById[$ruleIndex]; if (!$rule->isAssertion() || $rule->isDisabled()) { continue; } - $literals = $rule->getLiterals(); + $literals = $rule->literals; $literal = $literals[0]; if (!$this->decisions->decided(abs($literal))) { @@ -82,7 +83,6 @@ class Solver $conflict = $this->decisions->decisionRule($literal); if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) { - $problem = new Problem($this->pool); $problem->addRule($rule); @@ -104,7 +104,7 @@ class Solver continue; } - $assertRuleLiterals = $assertRule->getLiterals(); + $assertRuleLiterals = $assertRule->literals; $assertRuleLiteral = $assertRuleLiterals[0]; if (abs($literal) !== abs($assertRuleLiteral)) { @@ -125,29 +125,37 @@ class Solver { $this->installedMap = array(); foreach ($this->installed->getPackages() as $package) { - $this->installedMap[$package->getId()] = $package; + $this->installedMap[$package->id] = $package; } + } + protected function checkForRootRequireProblems($ignorePlatformReqs) + { foreach ($this->jobs as $job) { switch ($job['cmd']) { case 'update': - foreach ($job['packages'] as $package) { - if (isset($this->installedMap[$package->getId()])) { - $this->updateMap[$package->getId()] = true; + $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': foreach ($this->installedMap as $package) { - $this->updateMap[$package->getId()] = true; + $this->updateMap[$package->id] = true; } break; case 'install': - if (!$job['packages']) { + 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 Rule($this->pool, array(), null, null, $job)); + $problem->addRule(new Rule(array(), null, null, $job)); $this->problems[] = $problem; } break; @@ -155,15 +163,14 @@ class Solver } } - public function solve(Request $request) + public function solve(Request $request, $ignorePlatformReqs = false) { $this->jobs = $request->getJobs(); $this->setupInstalledMap(); - + $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap, $ignorePlatformReqs); + $this->checkForRootRequireProblems($ignorePlatformReqs); $this->decisions = new Decisions($this->pool); - - $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap); $this->watchGraph = new RuleWatchGraph; foreach ($this->rules as $rule) { @@ -349,7 +356,7 @@ class Solver while (true) { $this->learnedPool[count($this->learnedPool) - 1][] = $rule; - foreach ($rule->getLiterals() as $literal) { + foreach ($rule->literals as $literal) { // skip the one true literal if ($this->decisions->satisfy($literal)) { continue; @@ -434,7 +441,7 @@ class Solver ); } - $newRule = new Rule($this->pool, $learnedLiterals, Rule::RULE_LEARNED, $why); + $newRule = new Rule($learnedLiterals, Rule::RULE_LEARNED, $why); return array($learnedLiterals[0], $ruleLevel, $newRule, $why); } @@ -473,7 +480,7 @@ class Solver $this->problems[] = $problem; $seen = array(); - $literals = $conflictRule->getLiterals(); + $literals = $conflictRule->literals; foreach ($literals as $literal) { // skip the one true literal @@ -496,7 +503,7 @@ class Solver $problem->addRule($why); $this->analyzeUnsolvableRule($problem, $why); - $literals = $why->getLiterals(); + $literals = $why->literals; foreach ($literals as $literal) { // skip the one true literal @@ -601,7 +608,6 @@ class Solver $installedPos = 0; while (true) { - if (1 === $level) { $conflictRule = $this->propagate($level); if (null !== $conflictRule) { @@ -621,7 +627,7 @@ class Solver $decisionQueue = array(); $noneSatisfied = true; - foreach ($rule->getLiterals() as $literal) { + foreach ($rule->literals as $literal) { if ($this->decisions->satisfy($literal)) { $noneSatisfied = false; break; @@ -650,7 +656,6 @@ class Solver } if ($noneSatisfied && count($decisionQueue)) { - $oLevel = $level; $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule); @@ -682,8 +687,8 @@ class Solver $i = 0; } - $rule = $this->rules->ruleById($i); - $literals = $rule->getLiterals(); + $rule = $this->rules->ruleById[$i]; + $literals = $rule->literals; if ($rule->isDisabled()) { continue; @@ -734,7 +739,6 @@ class Solver // minimization step if (count($this->branches)) { - $lastLiteral = null; $lastLevel = null; $lastBranchIndex = 0; diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php index 4c1fb124a..b847164ff 100644 --- a/src/Composer/DependencyResolver/Transaction.php +++ b/src/Composer/DependencyResolver/Transaction.php @@ -13,7 +13,6 @@ namespace Composer\DependencyResolver; use Composer\Package\AliasPackage; -use Composer\DependencyResolver\Operation; /** * @author Nils Adermann @@ -50,16 +49,15 @@ class Transaction $package = $this->pool->literalToPackage($literal); // wanted & installed || !wanted & !installed - if (($literal > 0) == (isset($this->installedMap[$package->getId()]))) { + if (($literal > 0) == (isset($this->installedMap[$package->id]))) { continue; } if ($literal > 0) { if (isset($installMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) { - $source = $installMeansUpdateMap[abs($literal)]; - $updateMap[$package->getId()] = array( + $updateMap[$package->id] = array( 'package' => $package, 'source' => $source, 'reason' => $reason, @@ -67,9 +65,9 @@ class Transaction // avoid updates to one package from multiple origins unset($installMeansUpdateMap[abs($literal)]); - $ignoreRemove[$source->getId()] = true; + $ignoreRemove[$source->id] = true; } else { - $installMap[$package->getId()] = array( + $installMap[$package->id] = array( 'package' => $package, 'reason' => $reason, ); @@ -79,16 +77,16 @@ class Transaction 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->getId()]) && - !isset($ignoreRemove[$package->getId()])) { - $uninstallMap[$package->getId()] = array( + isset($this->installedMap[$package->id]) && + !isset($ignoreRemove[$package->id])) { + $uninstallMap[$package->id] = array( 'package' => $package, 'reason' => $reason, ); - } } @@ -109,7 +107,7 @@ class Transaction while (!empty($queue)) { $package = array_pop($queue); - $packageId = $package->getId(); + $packageId = $package->id; if (!isset($visited[$packageId])) { array_push($queue, $package); @@ -126,7 +124,7 @@ class Transaction } } - $visited[$package->getId()] = true; + $visited[$package->id] = true; } else { if (isset($installMap[$packageId])) { $this->install( @@ -167,7 +165,7 @@ class Transaction $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); foreach ($possibleRequires as $require) { - unset($roots[$require->getId()]); + unset($roots[$require->id]); } } } @@ -188,13 +186,13 @@ class Transaction } // !wanted & installed - if ($literal <= 0 && isset($this->installedMap[$package->getId()])) { + if ($literal <= 0 && isset($this->installedMap[$package->id])) { $updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package); - $literals = array($package->getId()); + $literals = array($package->id); foreach ($updates as $update) { - $literals[] = $update->getId(); + $literals[] = $update->id; } foreach ($literals as $updateLiteral) { diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index bfe174fb7..204c09154 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -13,6 +13,7 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; +use Symfony\Component\Finder\Finder; /** * Base downloader for archives @@ -47,22 +48,28 @@ abstract class ArchiveDownloader extends FileDownloader throw $e; } - unlink($fileName); + $this->filesystem->unlink($fileName); - // get file list - $contentDir = $this->listFiles($temporaryDir); + $contentDir = $this->getFolderContent($temporaryDir); // only one dir in the archive, extract its contents out of it - if (1 === count($contentDir) && !is_file($contentDir[0])) { - $contentDir = $this->listFiles($contentDir[0]); + if (1 === count($contentDir) && is_dir(reset($contentDir))) { + $contentDir = $this->getFolderContent((string) reset($contentDir)); } // move files back out of the temp dir foreach ($contentDir as $file) { + $file = (string) $file; $this->filesystem->rename($file, $path . '/' . basename($file)); } $this->filesystem->removeDirectory($temporaryDir); + if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) { + $this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/'); + } + if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) { + $this->filesystem->removeDirectory($this->config->get('vendor-dir')); + } } catch (\Exception $e) { // clean up $this->filesystem->removeDirectory($path); @@ -128,14 +135,19 @@ abstract class ArchiveDownloader extends FileDownloader abstract protected function extract($file, $path); /** - * Returns the list of files in a directory including dotfiles + * Returns the folder content, excluding dotfiles + * + * @param string $dir Directory + * @return \SplFileInfo[] */ - private function listFiles($dir) + private function getFolderContent($dir) { - $files = array_merge(glob($dir . '/.*') ?: array(), glob($dir . '/*') ?: array()); + $finder = Finder::create() + ->ignoreVCS(false) + ->ignoreDotFiles(false) + ->depth(0) + ->in($dir); - return array_values(array_filter($files, function ($el) { - return basename($el) !== '.' && basename($el) !== '..'; - })); + return iterator_to_array($finder); } } diff --git a/src/Composer/Downloader/DownloadManager.php b/src/Composer/Downloader/DownloadManager.php index e1eb1a9b7..4bbbae5a9 100644 --- a/src/Composer/Downloader/DownloadManager.php +++ b/src/Composer/Downloader/DownloadManager.php @@ -13,7 +13,7 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; -use Composer\Downloader\DownloaderInterface; +use Composer\IO\IOInterface; use Composer\Util\Filesystem; /** @@ -23,6 +23,7 @@ use Composer\Util\Filesystem; */ class DownloadManager { + private $io; private $preferDist = false; private $preferSource = false; private $filesystem; @@ -31,11 +32,13 @@ class DownloadManager /** * Initializes download manager. * + * @param IOInterface $io The Input Output Interface * @param bool $preferSource prefer downloading from source * @param Filesystem|null $filesystem custom Filesystem object */ - public function __construct($preferSource = false, Filesystem $filesystem = null) + public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null) { + $this->io = $io; $this->preferSource = $preferSource; $this->filesystem = $filesystem ?: new Filesystem(); } @@ -100,7 +103,7 @@ class DownloadManager /** * Returns downloader for a specific installation type. * - * @param string $type installation type + * @param string $type installation type * @return DownloaderInterface * * @throws \InvalidArgumentException if downloader for provided type is not registered @@ -118,12 +121,12 @@ class DownloadManager /** * Returns downloader for already installed package. * - * @param PackageInterface $package package instance + * @param PackageInterface $package package instance * @return DownloaderInterface|null * * @throws \InvalidArgumentException if package has no installation source specified * @throws \LogicException if specific downloader used to load package with - * wrong type + * wrong type */ public function getDownloaderForInstalledPackage(PackageInterface $package) { @@ -161,6 +164,7 @@ class DownloadManager * @param bool $preferSource prefer installation from source * * @throws \InvalidArgumentException if package have no urls to download from + * @throws \RuntimeException */ public function download(PackageInterface $package, $targetDir, $preferSource = null) { @@ -168,18 +172,48 @@ class DownloadManager $sourceType = $package->getSourceType(); $distType = $package->getDistType(); - if ((!$package->isDev() || $this->preferDist || !$sourceType) && !($preferSource && $sourceType) && $distType) { - $package->setInstallationSource('dist'); - } elseif ($sourceType) { - $package->setInstallationSource('source'); - } else { + $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 ((!$package->isDev() || $this->preferDist) && !$preferSource) { + $sources = array_reverse($sources); + } + $this->filesystem->ensureDirectoryExists($targetDir); - $downloader = $this->getDownloaderForInstalledPackage($package); - $downloader->download($package, $targetDir); + foreach ($sources as $i => $source) { + if (isset($e)) { + $this->io->write(' Now trying to download from ' . $source . ''); + } + $package->setInstallationSource($source); + try { + $downloader = $this->getDownloaderForInstalledPackage($package); + if ($downloader) { + $downloader->download($package, $targetDir); + } + break; + } catch (\RuntimeException $e) { + if ($i === count($sources) - 1) { + throw $e; + } + + $this->io->write( + ' Failed to download '. + $package->getPrettyName(). + ' from ' . $source . ': '. + $e->getMessage().'' + ); + } + } } /** @@ -194,6 +228,10 @@ class DownloadManager public function update(PackageInterface $initial, PackageInterface $target, $targetDir) { $downloader = $this->getDownloaderForInstalledPackage($initial); + if (!$downloader) { + return; + } + $installationSource = $initial->getInstallationSource(); if ('dist' === $installationSource) { @@ -230,6 +268,8 @@ class DownloadManager public function remove(PackageInterface $package, $targetDir) { $downloader = $this->getDownloaderForInstalledPackage($package); - $downloader->remove($package, $targetDir); + if ($downloader) { + $downloader->remove($package, $targetDir); + } } } diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 2f8c048dc..96bd57c06 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -21,7 +21,6 @@ use Composer\Plugin\PluginEvents; use Composer\Plugin\PreFileDownloadEvent; use Composer\EventDispatcher\EventDispatcher; use Composer\Util\Filesystem; -use Composer\Util\GitHub; use Composer\Util\RemoteFilesystem; /** @@ -56,11 +55,10 @@ class FileDownloader implements DownloaderInterface $this->io = $io; $this->config = $config; $this->eventDispatcher = $eventDispatcher; - $this->rfs = $rfs ?: new RemoteFilesystem($io); + $this->rfs = $rfs ?: new RemoteFilesystem($io, $config); $this->filesystem = $filesystem ?: new Filesystem(); $this->cache = $cache; - if ($this->cache && $this->cache->gcIsNecessary()) { $this->cache->gc($config->get('cache-files-ttl'), $config->get('cache-files-maxsize')); } @@ -79,17 +77,39 @@ class FileDownloader implements DownloaderInterface */ public function download(PackageInterface $package, $path) { - $url = $package->getDistUrl(); - if (!$url) { + if (!$package->getDistUrl()) { throw new \InvalidArgumentException('The given package is missing url information'); } - $this->filesystem->removeDirectory($path); - $this->filesystem->ensureDirectoryExists($path); + $this->io->write(" - Installing " . $package->getName() . " (" . VersionParser::formatVersion($package) . ")"); - $fileName = $this->getFileName($package, $path); + $urls = $package->getDistUrls(); + while ($url = array_shift($urls)) { + try { + return $this->doDownload($package, $path, $url); + } catch (\Exception $e) { + if ($this->io->isDebug()) { + $this->io->write(''); + $this->io->write('Failed: ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage()); + } elseif (count($urls)) { + $this->io->write(''); + $this->io->write(' Failed, trying the next URL ('.$e->getCode().': '.$e->getMessage().')'); + } - $this->io->write(" - Installing " . $package->getName() . " (" . VersionParser::formatVersion($package) . ")"); + if (!count($urls)) { + throw $e; + } + } + } + + $this->io->write(''); + } + + protected function doDownload(PackageInterface $package, $path, $url) + { + $this->filesystem->emptyDirectory($path); + + $fileName = $this->getFileName($package, $path); $processedUrl = $this->processUrl($package, $url); $hostname = parse_url($processedUrl, PHP_URL_HOST); @@ -100,61 +120,39 @@ class FileDownloader implements DownloaderInterface } $rfs = $preFileDownloadEvent->getRemoteFilesystem(); - if (strpos($hostname, '.github.com') === (strlen($hostname) - 11)) { - $hostname = 'github.com'; - } - try { $checksum = $package->getDistSha1Checksum(); $cacheKey = $this->getCacheKey($package); - try { - // download if we don't have it in cache or the cache is invalidated - if (!$this->cache || ($checksum && $checksum !== $this->cache->sha1($cacheKey)) || !$this->cache->copyTo($cacheKey, $fileName)) { - if (!$this->outputProgress) { - $this->io->write(' Downloading'); - } + // download if we don't have it in cache or the cache is invalidated + if (!$this->cache || ($checksum && $checksum !== $this->cache->sha1($cacheKey)) || !$this->cache->copyTo($cacheKey, $fileName)) { + if (!$this->outputProgress) { + $this->io->write(' Downloading'); + } - // try to download 3 times then fail hard - $retries = 3; - while ($retries--) { - try { - $rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress); - 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; - } - if ($this->io->isVerbose()) { - $this->io->write(' Download failed, retrying...'); - } - usleep(500000); + // try to download 3 times then fail hard + $retries = 3; + while ($retries--) { + try { + $rfs->copy($hostname, $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; } + if ($this->io->isVerbose()) { + $this->io->write(' Download failed, retrying...'); + } + usleep(500000); } - - if ($this->cache) { - $this->cache->copyFrom($cacheKey, $fileName); - } - } else { - $this->io->write(' Loading from cache'); - } - } catch (TransportException $e) { - if (!in_array($e->getCode(), array(404, 403, 412))) { - throw $e; } - if ('github.com' === $hostname && !$this->io->hasAuthentication($hostname)) { - $message = "\n".'Could not fetch '.$processedUrl.', enter your GitHub credentials '.($e->getCode() === 404 ? 'to access private repos' : 'to go over the API rate limit'); - $gitHubUtil = new GitHub($this->io, $this->config, null, $rfs); - if (!$gitHubUtil->authorizeOAuth($hostname) - && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($hostname, $message)) - ) { - throw $e; - } - $rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress); - } else { - throw $e; + + if ($this->cache) { + $this->cache->copyFrom($cacheKey, $fileName); } + } else { + $this->io->write(' Loading from cache'); } if (!file_exists($fileName)) { @@ -209,10 +207,7 @@ class FileDownloader implements DownloaderInterface { $this->io->write(" - Removing " . $package->getName() . " (" . VersionParser::formatVersion($package) . ")"); if (!$this->filesystem->removeDirectory($path)) { - // retry after a bit on windows since it tends to be touchy with mass removals - if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(250000) && !$this->filesystem->removeDirectory($path))) { - throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); - } + throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); } } diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 81030580f..bdebaf4cf 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -15,6 +15,10 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\Util\GitHub; use Composer\Util\Git as GitUtil; +use Composer\Util\ProcessExecutor; +use Composer\IO\IOInterface; +use Composer\Util\Filesystem; +use Composer\Config; /** * @author Jordi Boggiano @@ -22,26 +26,37 @@ use Composer\Util\Git as GitUtil; class GitDownloader extends VcsDownloader { private $hasStashedChanges = false; + private $gitUtil; + + public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, Filesystem $fs = null) + { + parent::__construct($io, $config, $process, $fs); + $this->gitUtil = new GitUtil($this->io, $this->config, $this->process, $this->filesystem); + } /** * {@inheritDoc} */ - public function doDownload(PackageInterface $package, $path) + public function doDownload(PackageInterface $package, $path, $url) { - $this->cleanEnv(); + GitUtil::cleanEnv(); $path = $this->normalizePath($path); $ref = $package->getSourceReference(); $flag = defined('PHP_WINDOWS_VERSION_MAJOR') ? '/D ' : ''; - $command = 'git clone %s %s && cd '.$flag.'%2$s && git remote add composer %1$s && git fetch composer'; + $command = 'git clone --no-checkout %s %s && cd '.$flag.'%2$s && git remote add composer %1$s && git fetch composer'; $this->io->write(" Cloning ".$ref); - $commandCallable = function($url) use ($ref, $path, $command) { - return sprintf($command, escapeshellarg($url), escapeshellarg($path), escapeshellarg($ref)); + $commandCallable = function ($url) use ($ref, $path, $command) { + return sprintf($command, ProcessExecutor::escape($url), ProcessExecutor::escape($path), ProcessExecutor::escape($ref)); }; - $this->runCommand($commandCallable, $package->getSourceUrl(), $path, true); - $this->setPushUrl($package, $path); + $this->gitUtil->runCommand($commandCallable, $url, $path, true); + if ($url !== $package->getSourceUrl()) { + $url = $package->getSourceUrl(); + $this->process->execute(sprintf('git remote set-url origin %s', ProcessExecutor::escape($url)), $output, $path); + } + $this->setPushUrl($path, $url); if ($newRef = $this->updateToCommit($path, $ref, $package->getPrettyVersion(), $package->getReleaseDate())) { if ($package->getDistReference() === $package->getSourceReference()) { @@ -54,9 +69,9 @@ class GitDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path) + public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { - $this->cleanEnv(); + GitUtil::cleanEnv(); $path = $this->normalizePath($path); if (!is_dir($path.'/.git')) { throw new \RuntimeException('The .git directory is missing from '.$path.', see http://getcomposer.org/commit-deps for more information'); @@ -66,17 +81,11 @@ class GitDownloader extends VcsDownloader $this->io->write(" Checking out ".$ref); $command = 'git remote set-url composer %s && git fetch composer && git fetch --tags composer'; - // capture username/password from URL if there is one - $this->process->execute('git remote -v', $output, $path); - if (preg_match('{^(?:composer|origin)\s+https?://(.+):(.+)@([^/]+)}im', $output, $match)) { - $this->io->setAuthentication($match[3], urldecode($match[1]), urldecode($match[2])); - } - - $commandCallable = function($url) use ($command) { - return sprintf($command, escapeshellarg($url)); + $commandCallable = function ($url) use ($command) { + return sprintf($command, ProcessExecutor::escape ($url)); }; - $this->runCommand($commandCallable, $target->getSourceUrl(), $path); + $this->gitUtil->runCommand($commandCallable, $url, $path); if ($newRef = $this->updateToCommit($path, $ref, $target->getPrettyVersion(), $target->getReleaseDate())) { if ($target->getDistReference() === $target->getSourceReference()) { $target->setDistReference($newRef); @@ -90,7 +99,7 @@ class GitDownloader extends VcsDownloader */ public function getLocalChanges(PackageInterface $package, $path) { - $this->cleanEnv(); + GitUtil::cleanEnv(); $path = $this->normalizePath($path); if (!is_dir($path.'/.git')) { return; @@ -109,7 +118,7 @@ class GitDownloader extends VcsDownloader */ protected function cleanChanges(PackageInterface $package, $path, $update) { - $this->cleanEnv(); + GitUtil::cleanEnv(); $path = $this->normalizePath($path); if (!$changes = $this->getLocalChanges($package, $path)) { return; @@ -186,7 +195,7 @@ class GitDownloader extends VcsDownloader $path = $this->normalizePath($path); if ($this->hasStashedChanges) { $this->hasStashedChanges = false; - $this->io->write(' Re-applying stashed changes'); + $this->io->write(' Re-applying stashed changes'); if (0 !== $this->process->execute('git stash pop', $output, $path)) { throw new \RuntimeException("Failed to apply stashed changes:\n\n".$this->process->getErrorOutput()); } @@ -194,13 +203,15 @@ class GitDownloader extends VcsDownloader } /** - * Updates the given apth to the given commit ref + * Updates the given path to the given commit ref * - * @param string $path - * @param string $reference - * @param string $branch - * @param DateTime $date + * @param string $path + * @param string $reference + * @param string $branch + * @param \DateTime $date * @return null|string if a string is returned, it is the commit reference that was checked out if the original could not be found + * + * @throws \RuntimeException */ protected function updateToCommit($path, $reference, $branch, $date) { @@ -218,7 +229,7 @@ class GitDownloader extends VcsDownloader && $branches && preg_match('{^\s+composer/'.preg_quote($reference).'$}m', $branches) ) { - $command = sprintf('git checkout -B %s %s && git reset --hard %2$s', escapeshellarg($branch), escapeshellarg('composer/'.$reference)); + $command = sprintf('git checkout -B %s %s && git reset --hard %2$s', ProcessExecutor::escape($branch), ProcessExecutor::escape('composer/'.$reference)); if (0 === $this->process->execute($command, $output, $path)) { return; } @@ -231,192 +242,41 @@ class GitDownloader extends VcsDownloader $branch = 'v' . $branch; } - $command = sprintf('git checkout %s', escapeshellarg($branch)); - $fallbackCommand = sprintf('git checkout -B %s %s', escapeshellarg($branch), escapeshellarg('composer/'.$branch)); + $command = sprintf('git checkout %s', ProcessExecutor::escape($branch)); + $fallbackCommand = sprintf('git checkout -B %s %s', ProcessExecutor::escape($branch), ProcessExecutor::escape('composer/'.$branch)); if (0 === $this->process->execute($command, $output, $path) || 0 === $this->process->execute($fallbackCommand, $output, $path) ) { - $command = sprintf('git reset --hard %s', escapeshellarg($reference)); + $command = sprintf('git reset --hard %s', ProcessExecutor::escape($reference)); if (0 === $this->process->execute($command, $output, $path)) { return; } } } - $command = sprintf($template, escapeshellarg($gitRef)); + $command = sprintf($template, ProcessExecutor::escape($gitRef)); if (0 === $this->process->execute($command, $output, $path)) { return; } // reference was not found (prints "fatal: reference is not a tree: $ref") - if ($date && false !== strpos($this->process->getErrorOutput(), $reference)) { - $date = $date->format('U'); - - // guess which remote branch to look at first - $command = 'git branch -r'; - if (0 !== $this->process->execute($command, $output, $path)) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); - } - - $guessTemplate = 'git log --until=%s --date=raw -n1 --pretty=%%H %s'; - foreach ($this->process->splitLines($output) as $line) { - if (preg_match('{^composer/'.preg_quote($branch).'(?:\.x)?$}i', trim($line))) { - // find the previous commit by date in the given branch - if (0 === $this->process->execute(sprintf($guessTemplate, $date, escapeshellarg(trim($line))), $output, $path)) { - $newReference = trim($output); - } - - break; - } - } - - if (empty($newReference)) { - // no matching branch found, find the previous commit by date in all commits - if (0 !== $this->process->execute(sprintf($guessTemplate, $date, '--all'), $output, $path)) { - throw new \RuntimeException('Failed to execute ' . $this->sanitizeUrl($command) . "\n\n" . $this->process->getErrorOutput()); - } - $newReference = trim($output); - } - - // checkout the new recovered ref - $command = sprintf($template, escapeshellarg($newReference)); - if (0 === $this->process->execute($command, $output, $path)) { - $this->io->write(' '.$reference.' is gone (history was rewritten?), recovered by checking out '.$newReference); - - return $newReference; - } + if (false !== strpos($this->process->getErrorOutput(), $reference)) { + $this->io->write(' '.$reference.' is gone (history was rewritten?)'); } - throw new \RuntimeException('Failed to execute ' . $this->sanitizeUrl($command) . "\n\n" . $this->process->getErrorOutput()); + throw new \RuntimeException('Failed to execute ' . GitUtil::sanitizeUrl($command) . "\n\n" . $this->process->getErrorOutput()); } - /** - * Runs a command doing attempts for each protocol supported by github. - * - * @param callable $commandCallable A callable building the command for the given url - * @param string $url - * @param string $cwd - * @param bool $initialClone If true, the directory if cleared between every attempt - * @throws \InvalidArgumentException - * @throws \RuntimeException - */ - protected function runCommand($commandCallable, $url, $cwd, $initialClone = false) - { - if ($initialClone) { - $origCwd = $cwd; - $cwd = null; - } - - if (preg_match('{^ssh://[^@]+@[^:]+:[^0-9]+}', $url)) { - throw new \InvalidArgumentException('The source URL '.$url.' is invalid, ssh URLs should have a port number after ":".'."\n".'Use ssh://git@example.com:22/path or just git@example.com:path if you do not want to provide a password or custom port.'); - } - - // public github, autoswitch protocols - if (preg_match('{^(?:https?|git)(://'.$this->getGitHubDomainsRegex().'/.*)}', $url, $match)) { - $protocols = $this->config->get('github-protocols'); - if (!is_array($protocols)) { - throw new \RuntimeException('Config value "github-protocols" must be an array, got '.gettype($protocols)); - } - $messages = array(); - foreach ($protocols as $protocol) { - $url = $protocol . $match[1]; - if (0 === $this->process->execute(call_user_func($commandCallable, $url), $ignoredOutput, $cwd)) { - return; - } - $messages[] = '- ' . $url . "\n" . preg_replace('#^#m', ' ', $this->process->getErrorOutput()); - if ($initialClone) { - $this->filesystem->removeDirectory($origCwd); - } - } - - // failed to checkout, first check git accessibility - $this->throwException('Failed to clone ' . $this->sanitizeUrl($url) .' via '.implode(', ', $protocols).' protocols, aborting.' . "\n\n" . implode("\n", $messages), $url); - } - - $command = call_user_func($commandCallable, $url); - if (0 !== $this->process->execute($command, $ignoredOutput, $cwd)) { - // private github repository without git access, try https with auth - if (preg_match('{^git@'.$this->getGitHubDomainsRegex().':(.+?)\.git$}i', $url, $match)) { - if (!$this->io->hasAuthentication($match[1])) { - $gitHubUtil = new GitHub($this->io, $this->config, $this->process); - $message = 'Cloning failed using an ssh key for authentication, enter your GitHub credentials to access private repos'; - - if (!$gitHubUtil->authorizeOAuth($match[1]) && $this->io->isInteractive()) { - $gitHubUtil->authorizeOAuthInteractively($match[1], $message); - } - } - - if ($this->io->hasAuthentication($match[1])) { - $auth = $this->io->getAuthentication($match[1]); - $url = 'https://'.urlencode($auth['username']) . ':' . urlencode($auth['password']) . '@'.$match[1].'/'.$match[2].'.git'; - - $command = call_user_func($commandCallable, $url); - if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { - return; - } - } - } elseif ( // private non-github repo that failed to authenticate - $this->io->isInteractive() && - preg_match('{(https?://)([^/]+)(.*)$}i', $url, $match) && - strpos($this->process->getErrorOutput(), 'fatal: Authentication failed') !== false - ) { - // TODO this should use an auth manager class that prompts and stores in the config - if ($this->io->hasAuthentication($match[2])) { - $auth = $this->io->getAuthentication($match[2]); - } else { - $this->io->write($url.' requires Authentication'); - $auth = array( - 'username' => $this->io->ask('Username: '), - 'password' => $this->io->askAndHideAnswer('Password: '), - ); - } - - $url = $match[1].urlencode($auth['username']).':'.urlencode($auth['password']).'@'.$match[2].$match[3]; - - $command = call_user_func($commandCallable, $url); - if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { - $this->io->setAuthentication($match[2], $auth['username'], $auth['password']); - - return; - } - } - - if ($initialClone) { - $this->filesystem->removeDirectory($origCwd); - } - $this->throwException('Failed to execute ' . $this->sanitizeUrl($command) . "\n\n" . $this->process->getErrorOutput(), $url); - } - } - - protected function getGitHubDomainsRegex() - { - return '('.implode('|', array_map('preg_quote', $this->config->get('github-domains'))).')'; - } - - protected function throwException($message, $url) - { - if (0 !== $this->process->execute('git --version', $ignoredOutput)) { - throw new \RuntimeException('Failed to clone '.$this->sanitizeUrl($url).', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput()); - } - - throw new \RuntimeException($message); - } - - protected function sanitizeUrl($message) - { - return preg_replace('{://([^@]+?):.+?@}', '://$1:***@', $message); - } - - protected function setPushUrl(PackageInterface $package, $path) + protected function setPushUrl($path, $url) { // set push url for github projects - if (preg_match('{^(?:https?|git)://'.$this->getGitHubDomainsRegex().'/([^/]+)/([^/]+?)(?:\.git)?$}', $package->getSourceUrl(), $match)) { + if (preg_match('{^(?:https?|git)://'.GitUtil::getGitHubDomainsRegex($this->config).'/([^/]+)/([^/]+?)(?:\.git)?$}', $url, $match)) { $protocols = $this->config->get('github-protocols'); $pushUrl = 'git@'.$match[1].':'.$match[2].'/'.$match[3].'.git'; if ($protocols[0] !== 'git') { $pushUrl = 'https://' . $match[1] . '/'.$match[2].'/'.$match[3].'.git'; } - $cmd = sprintf('git remote set-url --push origin %s', escapeshellarg($pushUrl)); + $cmd = sprintf('git remote set-url --push origin %s', ProcessExecutor::escape($pushUrl)); $this->process->execute($cmd, $ignoredOutput, $path); } } @@ -462,12 +322,6 @@ class GitDownloader extends VcsDownloader $this->hasStashedChanges = true; } - protected function cleanEnv() - { - $util = new GitUtil; - $util->cleanEnv(); - } - protected function normalizePath($path) { if (defined('PHP_WINDOWS_VERSION_MAJOR') && strlen($path) > 0) { diff --git a/src/Composer/Downloader/GzipDownloader.php b/src/Composer/Downloader/GzipDownloader.php new file mode 100644 index 000000000..f8624ab24 --- /dev/null +++ b/src/Composer/Downloader/GzipDownloader.php @@ -0,0 +1,70 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Downloader; + +use Composer\Config; +use Composer\Cache; +use Composer\EventDispatcher\EventDispatcher; +use Composer\Package\PackageInterface; +use Composer\Util\ProcessExecutor; +use Composer\IO\IOInterface; + +/** + * GZip archive downloader. + * + * @author Pavel Puchkin + */ +class GzipDownloader extends ArchiveDownloader +{ + protected $process; + + public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) + { + $this->process = $process ?: new ProcessExecutor($io); + parent::__construct($io, $config, $eventDispatcher, $cache); + } + + protected function extract($file, $path) + { + $targetFilepath = $path . DIRECTORY_SEPARATOR . basename(substr($file, 0, -3)); + + // Try to use gunzip on *nix + if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + $command = 'gzip -cd ' . ProcessExecutor::escape($file) . ' > ' . ProcessExecutor::escape($targetFilepath); + + if (0 === $this->process->execute($command, $ignoredOutput)) { + return; + } + + $processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); + throw new \RuntimeException($processError); + } + + // Windows version of PHP has built-in support of gzip functions + $archiveFile = gzopen($file, 'rb'); + $targetFile = fopen($targetFilepath, 'wb'); + while ($string = gzread($archiveFile, 4096)) { + fwrite($targetFile, $string, strlen($string)); + } + gzclose($archiveFile); + fclose($targetFile); + } + + /** + * {@inheritdoc} + */ + protected function getFileName(PackageInterface $package, $path) + { + return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME); + } +} diff --git a/src/Composer/Downloader/HgDownloader.php b/src/Composer/Downloader/HgDownloader.php index 7252bf4fe..3d5cc6209 100644 --- a/src/Composer/Downloader/HgDownloader.php +++ b/src/Composer/Downloader/HgDownloader.php @@ -13,6 +13,7 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; +use Composer\Util\ProcessExecutor; /** * @author Per Bernhardt @@ -22,12 +23,12 @@ class HgDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doDownload(PackageInterface $package, $path) + public function doDownload(PackageInterface $package, $path, $url) { - $url = escapeshellarg($package->getSourceUrl()); - $ref = escapeshellarg($package->getSourceReference()); + $url = ProcessExecutor::escape($url); + $ref = ProcessExecutor::escape($package->getSourceReference()); $this->io->write(" Cloning ".$package->getSourceReference()); - $command = sprintf('hg clone %s %s', $url, escapeshellarg($path)); + $command = sprintf('hg clone %s %s', $url, ProcessExecutor::escape($path)); if (0 !== $this->process->execute($command, $ignoredOutput)) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } @@ -40,10 +41,10 @@ class HgDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path) + public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { - $url = escapeshellarg($target->getSourceUrl()); - $ref = escapeshellarg($target->getSourceReference()); + $url = ProcessExecutor::escape($url); + $ref = ProcessExecutor::escape($target->getSourceReference()); $this->io->write(" Updating to ".$target->getSourceReference()); if (!is_dir($path.'/.hg')) { diff --git a/src/Composer/Downloader/PearPackageExtractor.php b/src/Composer/Downloader/PearPackageExtractor.php index 5310c7ab7..ff0c90a9f 100644 --- a/src/Composer/Downloader/PearPackageExtractor.php +++ b/src/Composer/Downloader/PearPackageExtractor.php @@ -127,19 +127,20 @@ class PearPackageExtractor /** * Builds list of copy and list of remove actions that would transform extracted PEAR tarball into installed package. * - * @param string $source string path to extracted files - * @param array $roles array [role => roleRoot] relative root for files having that role - * @param array $vars list of values can be used for replacement tasks - * @return array array of 'source' => 'target', where source is location of file in the tarball (relative to source - * path, and target is destination of file (also relative to $source path) + * @param string $source string path to extracted files + * @param array $roles array [role => roleRoot] relative root for files having that role + * @param array $vars list of values can be used for replacement tasks + * @return array array of 'source' => 'target', where source is location of file in the tarball (relative to source + * path, and target is destination of file (also relative to $source path) * @throws \RuntimeException */ private function buildCopyActions($source, array $roles, $vars) { /** @var $package \SimpleXmlElement */ $package = simplexml_load_file($this->combine($source, 'package.xml')); - if(false === $package) + if (false === $package) { throw new \RuntimeException('Package definition file is not valid.'); + } $packageSchemaVersion = $package['version']; if ('1.0' == $packageSchemaVersion) { @@ -203,16 +204,16 @@ class PearPackageExtractor /** @var $child \SimpleXMLElement */ if ($child->getName() == 'dir') { $dirSource = $this->combine($source, (string) $child['name']); - $dirTarget = $child['baseinstalldir'] ? : $target; - $dirRole = $child['role'] ? : $role; + $dirTarget = $child['baseinstalldir'] ?: $target; + $dirRole = $child['role'] ?: $role; $dirFiles = $this->buildSourceList10($child->children(), $targetRoles, $dirSource, $dirTarget, $dirRole, $packageName); $result = array_merge($result, $dirFiles); } elseif ($child->getName() == 'file') { - $fileRole = (string) $child['role'] ? : $role; + $fileRole = (string) $child['role'] ?: $role; if (isset($targetRoles[$fileRole])) { - $fileName = (string) ($child['name'] ? : $child[0]); // $child[0] means text content + $fileName = (string) ($child['name'] ?: $child[0]); // $child[0] means text content $fileSource = $this->combine($source, $fileName); - $fileTarget = $this->combine((string) $child['baseinstalldir'] ? : $target, $fileName); + $fileTarget = $this->combine((string) $child['baseinstalldir'] ?: $target, $fileName); if (!in_array($fileRole, self::$rolesWithoutPackageNamePrefix)) { $fileTarget = $packageName . '/' . $fileTarget; } @@ -233,15 +234,15 @@ class PearPackageExtractor /** @var $child \SimpleXMLElement */ if ('dir' == $child->getName()) { $dirSource = $this->combine($source, $child['name']); - $dirTarget = $child['baseinstalldir'] ? : $target; - $dirRole = $child['role'] ? : $role; + $dirTarget = $child['baseinstalldir'] ?: $target; + $dirRole = $child['role'] ?: $role; $dirFiles = $this->buildSourceList20($child->children(), $targetRoles, $dirSource, $dirTarget, $dirRole, $packageName); $result = array_merge($result, $dirFiles); } elseif ('file' == $child->getName()) { - $fileRole = (string) $child['role'] ? : $role; + $fileRole = (string) $child['role'] ?: $role; if (isset($targetRoles[$fileRole])) { $fileSource = $this->combine($source, (string) $child['name']); - $fileTarget = $this->combine((string) ($child['baseinstalldir'] ? : $target), (string) $child['name']); + $fileTarget = $this->combine((string) ($child['baseinstalldir'] ?: $target), (string) $child['name']); $fileTasks = array(); foreach ($child->children('http://pear.php.net/dtd/tasks-1.0') as $taskNode) { if ('replace' == $taskNode->getName()) { diff --git a/src/Composer/Downloader/PerforceDownloader.php b/src/Composer/Downloader/PerforceDownloader.php index 2bb1ba619..683ea9f34 100644 --- a/src/Composer/Downloader/PerforceDownloader.php +++ b/src/Composer/Downloader/PerforceDownloader.php @@ -22,18 +22,17 @@ use Composer\Util\Perforce; class PerforceDownloader extends VcsDownloader { protected $perforce; - protected $perforceInjected = false; /** * {@inheritDoc} */ - public function doDownload(PackageInterface $package, $path) + public function doDownload(PackageInterface $package, $path, $url) { $ref = $package->getSourceReference(); - $label = $package->getPrettyVersion(); + $label = $this->getLabelFromSourceReference($ref); $this->io->write(' Cloning ' . $ref); - $this->initPerforce($package, $path); + $this->initPerforce($package, $path, $url); $this->perforce->setStream($ref); $this->perforce->p4Login($this->io); $this->perforce->writeP4ClientSpec(); @@ -42,10 +41,21 @@ class PerforceDownloader extends VcsDownloader $this->perforce->cleanupClientSpec(); } - public function initPerforce($package, $path) + private function getLabelFromSourceReference($ref) { - if ($this->perforce) { + $pos = strpos($ref,'@'); + if (false !== $pos) { + return substr($ref, $pos + 1); + } + + return null; + } + + public function initPerforce($package, $path, $url) + { + if (!empty($this->perforce)) { $this->perforce->initializePath($path); + return; } @@ -54,7 +64,7 @@ class PerforceDownloader extends VcsDownloader if ($repository instanceof VcsRepository) { $repoConfig = $this->getRepoConfig($repository); } - $this->perforce = Perforce::create($repoConfig, $package->getSourceUrl(), $path); + $this->perforce = Perforce::create($repoConfig, $url, $path, $this->process, $this->io); } private function getRepoConfig(VcsRepository $repository) @@ -65,9 +75,9 @@ class PerforceDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path) + public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { - $this->doDownload($target, $path); + $this->doDownload($target, $path, $url); } /** diff --git a/src/Composer/Downloader/RarDownloader.php b/src/Composer/Downloader/RarDownloader.php index bb62ee0a8..12823422d 100644 --- a/src/Composer/Downloader/RarDownloader.php +++ b/src/Composer/Downloader/RarDownloader.php @@ -42,7 +42,7 @@ class RarDownloader extends ArchiveDownloader // Try to use unrar on *nix if (!defined('PHP_WINDOWS_VERSION_BUILD')) { - $command = 'unrar x ' . escapeshellarg($file) . ' ' . escapeshellarg($path) . ' && chmod -R u+w ' . escapeshellarg($path); + $command = 'unrar x ' . ProcessExecutor::escape($file) . ' ' . ProcessExecutor::escape($path) . ' && chmod -R u+w ' . ProcessExecutor::escape($path); if (0 === $this->process->execute($command, $ignoredOutput)) { return; diff --git a/src/Composer/Downloader/SvnDownloader.php b/src/Composer/Downloader/SvnDownloader.php index ec789c92a..689781f6c 100644 --- a/src/Composer/Downloader/SvnDownloader.php +++ b/src/Composer/Downloader/SvnDownloader.php @@ -24,10 +24,10 @@ class SvnDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doDownload(PackageInterface $package, $path) + public function doDownload(PackageInterface $package, $path, $url) { - $url = $package->getSourceUrl(); - $ref = $package->getSourceReference(); + SvnUtil::cleanEnv(); + $ref = $package->getSourceReference(); $this->io->write(" Checking out ".$package->getSourceReference()); $this->execute($url, "svn co", sprintf("%s/%s", $url, $ref), null, $path); @@ -36,17 +36,24 @@ class SvnDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path) + public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { - $url = $target->getSourceUrl(); + SvnUtil::cleanEnv(); $ref = $target->getSourceReference(); if (!is_dir($path.'/.svn')) { throw new \RuntimeException('The .svn directory is missing from '.$path.', see http://getcomposer.org/commit-deps for more information'); } + $flags = ""; + if (0 === $this->process->execute('svn --version', $output)) { + if (preg_match('{(\d+(?:\.\d+)+)}', $output, $match) && version_compare($match[1], '1.7.0', '>=')) { + $flags .= ' --ignore-ancestry'; + } + } + $this->io->write(" Checking out " . $ref); - $this->execute($url, "svn switch", sprintf("%s/%s", $url, $ref), $path); + $this->execute($url, "svn switch" . $flags, sprintf("%s/%s", $url, $ref), $path); } /** @@ -77,7 +84,7 @@ class SvnDownloader extends VcsDownloader */ protected function execute($baseUrl, $command, $url, $cwd = null, $path = null) { - $util = new SvnUtil($baseUrl, $this->io); + $util = new SvnUtil($baseUrl, $this->io, $this->config); try { return $util->execute($command, $url, $cwd, $path, $this->io->isVerbose()); } catch (\RuntimeException $e) { @@ -144,14 +151,20 @@ class SvnDownloader extends VcsDownloader */ protected function getCommitLogs($fromReference, $toReference, $path) { - // strip paths from references and only keep the actual revision - $fromRevision = preg_replace('{.*@(\d+)$}', '$1', $fromReference); - $toRevision = preg_replace('{.*@(\d+)$}', '$1', $toReference); + if (preg_match('{.*@(\d+)$}', $fromReference) && preg_match('{.*@(\d+)$}', $toReference) ) { + // strip paths from references and only keep the actual revision + $fromRevision = preg_replace('{.*@(\d+)$}', '$1', $fromReference); + $toRevision = preg_replace('{.*@(\d+)$}', '$1', $toReference); - $command = sprintf('svn log -r%s:%s --incremental', $fromRevision, $toRevision); + $command = sprintf('svn log -r%s:%s --incremental', $fromRevision, $toRevision); - if (0 !== $this->process->execute($command, $output, $path)) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); + if (0 !== $this->process->execute($command, $output, $path)) { + throw new \RuntimeException( + 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput() + ); + } + } else { + $output = "Could not retrieve changes between $fromReference and $toReference due to missing revision information"; } return $output; diff --git a/src/Composer/Downloader/TransportException.php b/src/Composer/Downloader/TransportException.php index d157dde3c..2e4b42f01 100644 --- a/src/Composer/Downloader/TransportException.php +++ b/src/Composer/Downloader/TransportException.php @@ -15,9 +15,10 @@ namespace Composer\Downloader; /** * @author Jordi Boggiano */ -class TransportException extends \Exception +class TransportException extends \RuntimeException { protected $headers; + protected $response; public function setHeaders($headers) { @@ -28,4 +29,14 @@ class TransportException extends \Exception { return $this->headers; } + + public function setResponse($response) + { + $this->response = $response; + } + + public function getResponse() + { + return $this->response; + } } diff --git a/src/Composer/Downloader/VcsDownloader.php b/src/Composer/Downloader/VcsDownloader.php index 4e06d63f8..e653794ca 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -55,8 +55,28 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa } $this->io->write(" - Installing " . $package->getName() . " (" . VersionParser::formatVersion($package) . ")"); - $this->filesystem->removeDirectory($path); - $this->doDownload($package, $path); + $this->filesystem->emptyDirectory($path); + + $urls = $package->getSourceUrls(); + while ($url = array_shift($urls)) { + try { + if (Filesystem::isLocalPath($url)) { + $url = realpath($url); + } + $this->doDownload($package, $path, $url); + break; + } catch (\Exception $e) { + if ($this->io->isDebug()) { + $this->io->write('Failed: ['.get_class($e).'] '.$e->getMessage()); + } elseif (count($urls)) { + $this->io->write(' Failed, trying the next URL'); + } + if (!count($urls)) { + throw $e; + } + } + } + $this->io->write(''); } @@ -87,17 +107,31 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa $this->io->write(" - Updating " . $name . " (" . $from . " => " . $to . ")"); $this->cleanChanges($initial, $path, true); - try { - $this->doUpdate($initial, $target, $path); - } catch (\Exception $e) { - // in case of failed update, try to reapply the changes before aborting - $this->reapplyChanges($path); - - throw $e; + $urls = $target->getSourceUrls(); + while ($url = array_shift($urls)) { + try { + if (Filesystem::isLocalPath($url)) { + $url = realpath($url); + } + $this->doUpdate($initial, $target, $path, $url); + break; + } catch (\Exception $e) { + if ($this->io->isDebug()) { + $this->io->write('Failed: ['.get_class($e).'] '.$e->getMessage()); + } elseif (count($urls)) { + $this->io->write(' Failed, trying the next URL'); + } else { + // in case of failed update, try to reapply the changes before aborting + $this->reapplyChanges($path); + + throw $e; + } + } } + $this->reapplyChanges($path); - //print the commit logs if in verbose mode + // print the commit logs if in verbose mode if ($this->io->isVerbose()) { $message = 'Pulling in changes:'; $logs = $this->getCommitLogs($initial->getSourceReference(), $target->getSourceReference(), $path); @@ -128,10 +162,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa $this->io->write(" - Removing " . $package->getName() . " (" . $package->getPrettyVersion() . ")"); $this->cleanChanges($package, $path, false); if (!$this->filesystem->removeDirectory($path)) { - // retry after a bit on windows since it tends to be touchy with mass removals - if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(250) && !$this->filesystem->removeDirectory($path))) { - throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); - } + throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); } } @@ -147,10 +178,10 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa /** * Prompt the user to check if changes should be stashed/removed or the operation aborted * - * @param PackageInterface $package - * @param string $path - * @param bool $update if true (update) the changes can be stashed and reapplied after an update, - * if false (remove) the changes should be assumed to be lost if the operation is not aborted + * @param PackageInterface $package + * @param string $path + * @param bool $update if true (update) the changes can be stashed and reapplied after an update, + * if false (remove) the changes should be assumed to be lost if the operation is not aborted * @throws \RuntimeException in case the operation must be aborted */ protected function cleanChanges(PackageInterface $package, $path, $update) @@ -176,8 +207,9 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa * * @param PackageInterface $package package instance * @param string $path download path + * @param string $url package url */ - abstract protected function doDownload(PackageInterface $package, $path); + abstract protected function doDownload(PackageInterface $package, $path, $url); /** * Updates specific package in specific folder from initial to target version. @@ -185,8 +217,9 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa * @param PackageInterface $initial initial package * @param PackageInterface $target updated package * @param string $path download path + * @param string $url package url */ - abstract protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path); + abstract protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url); /** * Fetches the commit logs between two commits diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index 71958948d..1370d82af 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -38,12 +38,16 @@ class ZipDownloader extends ArchiveDownloader // try to use unzip on *nix if (!defined('PHP_WINDOWS_VERSION_BUILD')) { - $command = 'unzip '.escapeshellarg($file).' -d '.escapeshellarg($path) . ' && chmod -R u+w ' . escapeshellarg($path); - if (0 === $this->process->execute($command, $ignoredOutput)) { - return; + $command = 'unzip '.ProcessExecutor::escape($file).' -d '.ProcessExecutor::escape($path) . ' && chmod -R u+w ' . ProcessExecutor::escape($path); + try { + if (0 === $this->process->execute($command, $ignoredOutput)) { + return; + } + + $processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); + } catch (\Exception $e) { + $processError = 'Failed to execute ' . $command . "\n\n" . $e->getMessage(); } - - $processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); } if (!class_exists('ZipArchive')) { diff --git a/src/Composer/EventDispatcher/Event.php b/src/Composer/EventDispatcher/Event.php index 0b3c9c951..38ae4f440 100644 --- a/src/Composer/EventDispatcher/Event.php +++ b/src/Composer/EventDispatcher/Event.php @@ -24,6 +24,16 @@ class Event */ protected $name; + /** + * @var array Arguments passed by the user, these will be forwarded to CLI script handlers + */ + protected $args; + + /** + * @var array Flags usable in PHP script handlers + */ + protected $flags; + /** * @var boolean Whether the event should not be passed to more listeners */ @@ -32,11 +42,15 @@ class Event /** * Constructor. * - * @param string $name The event name + * @param string $name The event name + * @param array $args Arguments passed by the user + * @param array $flags Optional flags to pass data not as argument */ - public function __construct($name) + public function __construct($name, array $args = array(), array $flags = array()) { $this->name = $name; + $this->args = $args; + $this->flags = $flags; } /** @@ -49,6 +63,26 @@ class Event return $this->name; } + /** + * Returns the event's arguments. + * + * @return array The event arguments + */ + public function getArguments() + { + return $this->args; + } + + /** + * Returns the event's flags. + * + * @return array The event flags + */ + public function getFlags() + { + return $this->flags; + } + /** * Checks if stopPropagation has been called * diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index f1e94e6d5..dff5456e1 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -12,9 +12,14 @@ namespace Composer\EventDispatcher; +use Composer\DependencyResolver\PolicyInterface; +use Composer\DependencyResolver\Pool; +use Composer\DependencyResolver\Request; +use Composer\Installer\InstallerEvent; use Composer\IO\IOInterface; use Composer\Composer; use Composer\DependencyResolver\Operation\OperationInterface; +use Composer\Repository\CompositeRepository; use Composer\Script; use Composer\Script\CommandEvent; use Composer\Script\PackageEvent; @@ -39,6 +44,7 @@ class EventDispatcher protected $io; protected $loader; protected $process; + protected $listeners; /** * Constructor. @@ -57,8 +63,10 @@ class EventDispatcher /** * Dispatch an event * - * @param string $eventName An event name - * @param Event $event + * @param string $eventName An event name + * @param Event $event + * @return int return code of the executed script if any, for php scripts a false return + * value is changed to 1, anything else to 0 */ public function dispatch($eventName, Event $event = null) { @@ -66,51 +74,78 @@ class EventDispatcher $event = new Event($eventName); } - $this->doDispatch($event); + return $this->doDispatch($event); } /** * Dispatch a script event. * - * @param string $eventName The constant in ScriptEvents - * @param Script\Event $event + * @param string $eventName The constant in ScriptEvents + * @param bool $devMode + * @param array $additionalArgs Arguments passed by the user + * @param array $flags Optional flags to pass data not as argument + * @return int return code of the executed script if any, for php scripts a false return + * value is changed to 1, anything else to 0 */ - public function dispatchScript($eventName, Script\Event $event = null) + public function dispatchScript($eventName, $devMode = false, $additionalArgs = array(), $flags = array()) { - if (null == $event) { - $event = new Script\Event($eventName, $this->composer, $this->io); - } - - $this->doDispatch($event); + return $this->doDispatch(new Script\Event($eventName, $this->composer, $this->io, $devMode, $additionalArgs, $flags)); } /** * Dispatch a package event. * - * @param string $eventName The constant in ScriptEvents - * @param boolean $devMode Whether or not we are in dev mode - * @param OperationInterface $operation The package being installed/updated/removed + * @param string $eventName The constant in ScriptEvents + * @param boolean $devMode Whether or not we are in dev mode + * @param OperationInterface $operation The package being installed/updated/removed + * @return int return code of the executed script if any, for php scripts a false return + * value is changed to 1, anything else to 0 */ public function dispatchPackageEvent($eventName, $devMode, OperationInterface $operation) { - $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $operation)); + return $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $operation)); } /** * Dispatch a command event. * - * @param string $eventName The constant in ScriptEvents - * @param boolean $devMode Whether or not we are in dev mode + * @param string $eventName The constant in ScriptEvents + * @param boolean $devMode Whether or not we are in dev mode + * @param array $additionalArgs Arguments passed by the user + * @param array $flags Optional flags to pass data not as argument + * @return int return code of the executed script if any, for php scripts a false return + * value is changed to 1, anything else to 0 + */ + public function dispatchCommandEvent($eventName, $devMode, $additionalArgs = array(), $flags = array()) + { + return $this->doDispatch(new CommandEvent($eventName, $this->composer, $this->io, $devMode, $additionalArgs, $flags)); + } + + /** + * Dispatch a installer event. + * + * @param string $eventName The constant in InstallerEvents + * @param PolicyInterface $policy The policy + * @param Pool $pool The pool + * @param CompositeRepository $installedRepo The installed repository + * @param Request $request The request + * @param array $operations The list of operations + * + * @return int return code of the executed script if any, for php scripts a false return + * value is changed to 1, anything else to 0 */ - public function dispatchCommandEvent($eventName, $devMode) + public function dispatchInstallerEvent($eventName, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations = array()) { - $this->doDispatch(new CommandEvent($eventName, $this->composer, $this->io, $devMode)); + return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $policy, $pool, $installedRepo, $request, $operations)); } /** * Triggers the listeners of an event. * - * @param Event $event The event object to pass to the event handlers/listeners. + * @param Event $event The event object to pass to the event handlers/listeners. + * @param string $additionalArgs + * @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 * @throws \RuntimeException * @throws \Exception */ @@ -118,9 +153,11 @@ class EventDispatcher { $listeners = $this->getListeners($event); + $return = 0; foreach ($listeners as $callable) { if (!is_string($callable) && is_callable($callable)) { - call_user_func($callable, $event); + $event = $this->checkListenerExpectedEvent($callable, $event); + $return = false === call_user_func($callable, $event) ? 1 : 0; } elseif ($this->isPhpScript($callable)) { $className = substr($callable, 0, strpos($callable, '::')); $methodName = substr($callable, strpos($callable, '::') + 2); @@ -135,15 +172,16 @@ class EventDispatcher } try { - $this->executeEventPhpScript($className, $methodName, $event); + $return = false === $this->executeEventPhpScript($className, $methodName, $event) ? 1 : 0; } catch (\Exception $e) { $message = "Script %s handling the %s event terminated with an exception"; $this->io->write(''.sprintf($message, $callable, $event->getName()).''); throw $e; } } else { - if (0 !== ($exitCode = $this->process->execute($callable))) { - $event->getIO()->write(sprintf('Script %s handling the %s event returned with an error', $callable, $event->getName())); + $args = implode(' ', array_map(array('Composer\Util\ProcessExecutor','escape'), $event->getArguments())); + if (0 !== ($exitCode = $this->process->execute($callable . ($args === '' ? '' : ' '.$args)))) { + $this->io->write(sprintf('Script %s handling the %s event returned with an error', $callable, $event->getName())); throw new \RuntimeException('Error Output: '.$this->process->getErrorOutput(), $exitCode); } @@ -153,6 +191,8 @@ class EventDispatcher break; } } + + return $return; } /** @@ -162,7 +202,41 @@ class EventDispatcher */ protected function executeEventPhpScript($className, $methodName, Event $event) { - $className::$methodName($event); + $event = $this->checkListenerExpectedEvent(array($className, $methodName), $event); + + return $className::$methodName($event); + } + + /** + * @param mixed $target + * @param Event $event + * @return Event|CommandEvent + */ + protected function checkListenerExpectedEvent($target, Event $event) + { + if (!$event instanceof Script\Event) { + return $event; + } + + try { + $reflected = new \ReflectionParameter($target, 0); + } catch (\Exception $e) { + return $event; + } + + $typehint = $reflected->getClass(); + + if (!$typehint instanceof \ReflectionClass) { + return $event; + } + + $expected = $typehint->getName(); + + if (!$event instanceof $expected && $expected === 'Composer\Script\CommandEvent') { + $event = new CommandEvent($event->getName(), $event->getComposer(), $event->getIO(), $event->isDevMode(), $event->getArguments()); + } + + return $event; } /** @@ -220,6 +294,19 @@ class EventDispatcher return call_user_func_array('array_merge', $listeners[$event->getName()]); } + /** + * Checks if an event has listeners registered + * + * @param Event $event + * @return boolean + */ + public function hasEventListeners(Event $event) + { + $listeners = $this->getListeners($event); + + return count($listeners) > 0; + } + /** * Finds all listeners defined as scripts in the package * diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index f829cb459..3290b51dc 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -17,7 +17,7 @@ use Composer\Json\JsonFile; use Composer\IO\IOInterface; use Composer\Package\Archiver; use Composer\Repository\RepositoryManager; -use Composer\Repository\RepositoryInterface; +use Composer\Repository\WritableRepositoryInterface; use Composer\Util\ProcessExecutor; use Composer\Util\RemoteFilesystem; use Symfony\Component\Console\Formatter\OutputFormatterStyle; @@ -36,14 +36,12 @@ use Composer\Package\Version\VersionParser; class Factory { /** + * @return string * @throws \RuntimeException - * @return Config */ - public static function createConfig() + protected static function getHomeDir() { - // determine home and cache dirs $home = getenv('COMPOSER_HOME'); - $cacheDir = getenv('COMPOSER_CACHE_DIR'); if (!$home) { if (defined('PHP_WINDOWS_VERSION_MAJOR')) { if (!getenv('APPDATA')) { @@ -57,6 +55,18 @@ class Factory $home = rtrim(getenv('HOME'), '/') . '/.composer'; } } + + return $home; + } + + /** + * @param string $home + * + * @return string + */ + protected static function getCacheDir($home) + { + $cacheDir = getenv('COMPOSER_CACHE_DIR'); if (!$cacheDir) { if (defined('PHP_WINDOWS_VERSION_MAJOR')) { if ($cacheDir = getenv('LOCALAPPDATA')) { @@ -70,6 +80,21 @@ class Factory } } + return $cacheDir; + } + + /** + * @param IOInterface|null $io + * @return Config + */ + public static function createConfig(IOInterface $io = null, $cwd = null) + { + $cwd = $cwd ?: getcwd(); + + // determine home and cache dirs + $home = self::getHomeDir(); + $cacheDir = self::getCacheDir($home); + // Protect directory against web access. Since HOME could be // the www-data's user home and be web-accessible it is a // potential security risk @@ -82,48 +107,30 @@ class Factory } } - $config = new Config(); + $config = new Config(true, $cwd); // add dirs to the config $config->merge(array('config' => array('home' => $home, 'cache-dir' => $cacheDir))); + // load global config $file = new JsonFile($home.'/config.json'); if ($file->exists()) { + if ($io && $io->isDebug()) { + $io->write('Loading config file ' . $file->getPath()); + } $config->merge($file->read()); } $config->setConfigSource(new JsonConfigSource($file)); - // move old cache dirs to the new locations - $legacyPaths = array( - 'cache-repo-dir' => array('/cache' => '/http*', '/cache.svn' => '/*', '/cache.github' => '/*'), - 'cache-vcs-dir' => array('/cache.git' => '/*', '/cache.hg' => '/*'), - 'cache-files-dir' => array('/cache.files' => '/*'), - ); - foreach ($legacyPaths as $key => $oldPaths) { - foreach ($oldPaths as $oldPath => $match) { - $dir = $config->get($key); - if ('/cache.github' === $oldPath) { - $dir .= '/github.com'; - } - $oldPath = $config->get('home').$oldPath; - $oldPathMatch = $oldPath . $match; - if (is_dir($oldPath) && $dir !== $oldPath) { - if (!is_dir($dir)) { - if (!@mkdir($dir, 0777, true)) { - continue; - } - } - if (is_array($children = glob($oldPathMatch))) { - foreach ($children as $child) { - @rename($child, $dir.'/'.basename($child)); - } - } - if ($config->get('cache-dir') != $oldPath) { - @rmdir($oldPath); - } - } + // load global auth file + $file = new JsonFile($config->get('home').'/auth.json'); + if ($file->exists()) { + if ($io && $io->isDebug()) { + $io->write('Loading config file ' . $file->getPath()); } + $config->merge(array('config' => $file->read())); } + $config->setAuthConfigSource(new JsonConfigSource($file, true)); return $config; } @@ -146,7 +153,7 @@ class Factory $repos = array(); if (!$config) { - $config = static::createConfig(); + $config = static::createConfig($io); } if (!$rm) { if (!$io) { @@ -157,11 +164,14 @@ class Factory } foreach ($config->getRepositories() as $index => $repo) { + if (is_string($repo)) { + throw new \UnexpectedValueException('"repositories" should be an array of repository definitions, only a single repository was given'); + } if (!is_array($repo)) { - throw new \UnexpectedValueException('Repository '.$index.' ('.json_encode($repo).') should be an array, '.gettype($repo).' given'); + throw new \UnexpectedValueException('Repository "'.$index.'" ('.json_encode($repo).') should be an array, '.gettype($repo).' given'); } if (!isset($repo['type'])) { - throw new \UnexpectedValueException('Repository '.$index.' ('.json_encode($repo).') must have a type defined'); + throw new \UnexpectedValueException('Repository "'.$index.'" ('.json_encode($repo).') must have a type defined'); } $name = is_int($index) && isset($repo['url']) ? preg_replace('{^https?://}i', '', $repo['url']) : $index; while (isset($repos[$name])) { @@ -176,16 +186,19 @@ class Factory /** * Creates a Composer instance * - * @param IOInterface $io IO instance - * @param array|string|null $localConfig either a configuration array or a filename to read from, if null it will - * read from the default filename + * @param IOInterface $io IO instance + * @param array|string|null $localConfig either a configuration array or a filename to read from, if null it will + * read from the default filename * @param bool $disablePlugins Whether plugins should not be loaded + * @param bool $fullLoad Whether to initialize everything or only main project stuff (used when loading the global composer) * @throws \InvalidArgumentException * @throws \UnexpectedValueException * @return Composer */ - public function createComposer(IOInterface $io, $localConfig = null, $disablePlugins = false) + public function createComposer(IOInterface $io, $localConfig = null, $disablePlugins = false, $cwd = null, $fullLoad = true) { + $cwd = $cwd ?: getcwd(); + // load Composer configuration if (null === $localConfig) { $localConfig = static::getComposerFile(); @@ -197,7 +210,7 @@ class Factory if (!$file->exists()) { if ($localConfig === './composer.json' || $localConfig === 'composer.json') { - $message = 'Composer could not find a composer.json file in '.getcwd(); + $message = 'Composer could not find a composer.json file in '.$cwd; } else { $message = 'Composer could not find the config file: '.$localConfig; } @@ -209,26 +222,45 @@ class Factory $localConfig = $file->read(); } - // Configuration defaults - $config = static::createConfig(); + // Load config and override with local config/auth config + $config = static::createConfig($io, $cwd); $config->merge($localConfig); - $io->loadConfiguration($config); + if (isset($composerFile)) { + if ($io && $io->isDebug()) { + $io->write('Loading config file ' . $composerFile); + } + $localAuthFile = new JsonFile(dirname(realpath($composerFile)) . '/auth.json'); + if ($localAuthFile->exists()) { + if ($io && $io->isDebug()) { + $io->write('Loading config file ' . $localAuthFile->getPath()); + } + $config->merge(array('config' => $localAuthFile->read())); + $config->setAuthConfigSource(new JsonConfigSource($localAuthFile, true)); + } + } $vendorDir = $config->get('vendor-dir'); $binDir = $config->get('bin-dir'); - // setup process timeout - ProcessExecutor::setTimeout((int) $config->get('process-timeout')); - // initialize composer $composer = new Composer(); $composer->setConfig($config); + if ($fullLoad) { + // load auth configs into the IO instance + $io->loadConfiguration($config); + + // setup process timeout + ProcessExecutor::setTimeout((int) $config->get('process-timeout')); + } + // initialize event dispatcher $dispatcher = new EventDispatcher($composer, $io); + $composer->setEventDispatcher($dispatcher); // initialize repository manager $rm = $this->createRepositoryManager($io, $config, $dispatcher); + $composer->setRepositoryManager($rm); // load local repository $this->addLocalRepository($rm, $vendorDir); @@ -237,45 +269,47 @@ class Factory $parser = new VersionParser; $loader = new Package\Loader\RootPackageLoader($rm, $config, $parser, new ProcessExecutor($io)); $package = $loader->load($localConfig); + $composer->setPackage($package); // initialize installation manager $im = $this->createInstallationManager(); - - // Composer composition - $composer->setPackage($package); - $composer->setRepositoryManager($rm); $composer->setInstallationManager($im); - // initialize download manager - $dm = $this->createDownloadManager($io, $config, $dispatcher); - - $composer->setDownloadManager($dm); - $composer->setEventDispatcher($dispatcher); + if ($fullLoad) { + // initialize download manager + $dm = $this->createDownloadManager($io, $config, $dispatcher); + $composer->setDownloadManager($dm); - // initialize autoload generator - $generator = new AutoloadGenerator($dispatcher); - $composer->setAutoloadGenerator($generator); + // initialize autoload generator + $generator = new AutoloadGenerator($dispatcher, $io); + $composer->setAutoloadGenerator($generator); + } - // add installers to the manager + // add installers to the manager (must happen after download manager is created since they read it out of $composer) $this->createDefaultInstallers($im, $composer, $io); - $globalRepository = $this->createGlobalRepository($config, $vendorDir); - $pm = $this->createPluginManager($composer, $io, $globalRepository); - $composer->setPluginManager($pm); + if ($fullLoad) { + $globalComposer = $this->createGlobalComposer($io, $config, $disablePlugins); + $pm = $this->createPluginManager($io, $composer, $globalComposer); + $composer->setPluginManager($pm); - if (!$disablePlugins) { - $pm->loadInstalledPlugins(); - } + if (!$disablePlugins) { + $pm->loadInstalledPlugins(); + } - // purge packages if they have been deleted on the filesystem - $this->purgePackages($rm, $im); + // once we have plugins and custom installers we can + // purge packages from local repos if they have been deleted on the filesystem + if ($rm->getLocalRepository()) { + $this->purgePackages($rm->getLocalRepository(), $im); + } + } // init locker if possible - if (isset($composerFile)) { + if ($fullLoad && isset($composerFile)) { $lockFile = "json" === pathinfo($composerFile, PATHINFO_EXTENSION) ? substr($composerFile, 0, -4).'lock' : $composerFile . '.lock'; - $locker = new Package\Locker($io, new JsonFile($lockFile, new RemoteFilesystem($io)), $rm, $im, md5_file($composerFile)); + $locker = new Package\Locker($io, new JsonFile($lockFile, new RemoteFilesystem($io, $config)), $rm, $im, md5_file($composerFile)); $composer->setLocker($locker); } @@ -313,22 +347,26 @@ class Factory $rm->setLocalRepository(new Repository\InstalledFilesystemRepository(new JsonFile($vendorDir.'/composer/installed.json'))); } - /** - * @param Config $config - * @param string $vendorDir + /** + * @param Config $config + * @return Composer|null */ - protected function createGlobalRepository(Config $config, $vendorDir) + protected function createGlobalComposer(IOInterface $io, Config $config, $disablePlugins) { - if ($config->get('home') == $vendorDir) { - return null; + if (realpath($config->get('home')) === getcwd()) { + return; } - $path = $config->get('home').'/vendor/composer/installed.json'; - if (!file_exists($path)) { - return null; + $composer = null; + try { + $composer = self::createComposer($io, $config->get('home') . '/composer.json', $disablePlugins, $config->get('home'), false); + } catch (\Exception $e) { + if ($io->isDebug()) { + $io->write('Failed to initialize global composer: '.$e->getMessage()); + } } - return new Repository\InstalledFilesystemRepository(new JsonFile($path)); + return $composer; } /** @@ -344,7 +382,7 @@ class Factory $cache = new Cache($io, $config->get('cache-files-dir'), 'a-z0-9_./'); } - $dm = new Downloader\DownloadManager(); + $dm = new Downloader\DownloadManager($io); switch ($config->get('preferred-install')) { case 'dist': $dm->setPreferDist(true); @@ -365,6 +403,7 @@ class Factory $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache)); $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $eventDispatcher, $cache)); $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache)); + $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $eventDispatcher, $cache)); $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache)); $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache)); @@ -392,11 +431,14 @@ class Factory } /** + * @param IOInterface $io + * @param Composer $composer + * @param Composer $globalComposer * @return Plugin\PluginManager */ - protected function createPluginManager(Composer $composer, IOInterface $io, RepositoryInterface $globalRepository = null) + protected function createPluginManager(IOInterface $io, Composer $composer, Composer $globalComposer = null) { - return new Plugin\PluginManager($composer, $io, $globalRepository); + return new Plugin\PluginManager($io, $composer, $globalComposer); } /** @@ -421,12 +463,11 @@ class Factory } /** - * @param Repository\RepositoryManager $rm - * @param Installer\InstallationManager $im + * @param WritableRepositoryInterface $repo repository to purge packages from + * @param Installer\InstallationManager $im manager to check whether packages are still installed */ - protected function purgePackages(Repository\RepositoryManager $rm, Installer\InstallationManager $im) + protected function purgePackages(WritableRepositoryInterface $repo, Installer\InstallationManager $im) { - $repo = $rm->getLocalRepository(); foreach ($repo->getPackages() as $package) { if (!$im->isPackageInstalled($repo, $package)) { $repo->removePackage($package); @@ -435,10 +476,10 @@ class Factory } /** - * @param IOInterface $io IO instance - * @param mixed $config either a configuration array or a filename to read from, if null it will read from - * the default filename - * @param bool $disablePlugins Whether plugins should not be loaded + * @param IOInterface $io IO instance + * @param mixed $config either a configuration array or a filename to read from, if null it will read from + * the default filename + * @param bool $disablePlugins Whether plugins should not be loaded * @return Composer */ public static function create(IOInterface $io, $config = null, $disablePlugins = false) diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index 29cae4f07..8d684833e 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -68,5 +68,12 @@ abstract class BaseIO implements IOInterface $this->setAuthentication($domain, $token, 'x-oauth-basic'); } } + + // reload http basic credentials from config if available + if ($creds = $config->get('http-basic')) { + foreach ($creds as $domain => $cred) { + $this->setAuthentication($domain, $cred['username'], $cred['password']); + } + } } } diff --git a/src/Composer/IO/ConsoleIO.php b/src/Composer/IO/ConsoleIO.php index 39c070db2..7df26eabf 100644 --- a/src/Composer/IO/ConsoleIO.php +++ b/src/Composer/IO/ConsoleIO.php @@ -15,6 +15,7 @@ namespace Composer\IO; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Process\ExecutableFinder; /** * The Input/Output helper. @@ -95,13 +96,11 @@ class ConsoleIO extends BaseIO public function write($messages, $newline = true) { if (null !== $this->startTime) { - $messages = (array) $messages; - $messages[0] = sprintf( - '[%.1fMB/%.2fs] %s', - memory_get_usage() / 1024 / 1024, - microtime(true) - $this->startTime, - $messages[0] - ); + $memoryUsage = memory_get_usage() / 1024 / 1024; + $timeSpent = microtime(true) - $this->startTime; + $messages = array_map(function ($message) use ($memoryUsage, $timeSpent) { + return sprintf('[%.1fMB/%.2fs] %s', $memoryUsage, $timeSpent, $message); + }, (array) $messages); } $this->output->write($messages, $newline); $this->lastMessage = join($newline ? "\n" : '', (array) $messages); @@ -112,6 +111,14 @@ class ConsoleIO extends BaseIO */ public function overwrite($messages, $newline = true, $size = null) { + if (!$this->output->isDecorated()) { + if (!$messages) { + return; + } + + return $this->write($messages, count($messages) === 1 || $newline); + } + // messages can be an array, let's convert it to string anyway $messages = join($newline ? "\n" : '', (array) $messages); @@ -171,6 +178,18 @@ class ConsoleIO extends BaseIO { // handle windows if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $finder = new ExecutableFinder(); + + // use bash if it's present + if ($finder->find('bash') && $finder->find('stty')) { + $this->write($question, false); + $value = rtrim(shell_exec('bash -c "stty -echo; read -n0 discard; read -r mypassword; stty echo; echo $mypassword"')); + $this->write(''); + + return $value; + } + + // fallback to hiddeninput executable $exe = __DIR__.'\\hiddeninput.exe'; // handle code running from a phar diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index e1dbd496f..3594336e3 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -26,11 +26,12 @@ use Composer\DependencyResolver\SolverProblemsException; use Composer\Downloader\DownloadManager; use Composer\EventDispatcher\EventDispatcher; use Composer\Installer\InstallationManager; -use Composer\Config; +use Composer\Installer\InstallerEvents; use Composer\Installer\NoopInstaller; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Package\AliasPackage; +use Composer\Package\CompletePackage; use Composer\Package\Link; use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\Locker; @@ -104,7 +105,16 @@ class Installer protected $dryRun = false; protected $verbose = false; protected $update = false; + protected $dumpAutoloader = true; protected $runScripts = true; + protected $ignorePlatformReqs = false; + protected $preferStable = false; + protected $preferLowest = false; + /** + * Array of package names/globs flagged for update + * + * @var array|null + */ protected $updateWhitelist = null; protected $whitelistDependencies = false; @@ -148,9 +158,14 @@ class Installer * Run installation (or update) * * @return int 0 on success or a positive error code on failure + * + * @throws \Exception */ public function run() { + gc_collect_cycles(); + gc_disable(); + if ($this->dryRun) { $this->verbose = true; $this->runScripts = false; @@ -218,16 +233,37 @@ class Installer } $this->installationManager->notifyInstalls(); - // output suggestions - foreach ($this->suggestedPackages as $suggestion) { - $target = $suggestion['target']; - foreach ($installedRepo->getPackages() as $package) { - if (in_array($target, $package->getNames())) { - continue 2; + // output suggestions if we're in dev mode + if ($this->devMode) { + foreach ($this->suggestedPackages as $suggestion) { + $target = $suggestion['target']; + foreach ($installedRepo->getPackages() as $package) { + if (in_array($target, $package->getNames())) { + continue 2; + } } + + $this->io->write($suggestion['source'].' suggests installing '.$suggestion['target'].' ('.$suggestion['reason'].')'); + } + } + + # Find abandoned packages and warn user + foreach ($localRepo->getPackages() as $package) { + if (!$package instanceof CompletePackage || !$package->isAbandoned()) { + continue; } - $this->io->write($suggestion['source'].' suggests installing '.$suggestion['target'].' ('.$suggestion['reason'].')'); + $replacement = (is_string($package->getReplacementPackage())) + ? 'Use ' . $package->getReplacementPackage() . ' instead' + : 'No replacement was suggested'; + + $this->io->write( + sprintf( + "Package %s is abandoned, you should avoid using it. %s.", + $package->getPrettyName(), + $replacement + ) + ); } if (!$this->dryRun) { @@ -252,8 +288,10 @@ class Installer $request->install($link->getTarget(), $link->getConstraint()); } + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $policy, $pool, $installedRepo, $request); $solver = new Solver($policy, $pool, $installedRepo); - $ops = $solver->solve($request); + $ops = $solver->solve($request, $this->ignorePlatformReqs); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $policy, $pool, $installedRepo, $request, $ops); foreach ($ops as $op) { if ($op->getJobType() === 'uninstall') { $devPackages[] = $op->getPackage(); @@ -271,27 +309,37 @@ class Installer $platformDevReqs, $aliases, $this->package->getMinimumStability(), - $this->package->getStabilityFlags() + $this->package->getStabilityFlags(), + $this->preferStable || $this->package->getPreferStable(), + $this->preferLowest ); if ($updatedLock) { $this->io->write('Writing lock file'); } } - // write autoloader - if ($this->optimizeAutoloader) { - $this->io->write('Generating optimized autoload files'); - } else { - $this->io->write('Generating autoload files'); - } + if ($this->dumpAutoloader) { + // write autoloader + if ($this->optimizeAutoloader) { + $this->io->write('Generating optimized autoload files'); + } else { + $this->io->write('Generating autoload files'); + } - $this->autoloadGenerator->dump($this->config, $localRepo, $this->package, $this->installationManager, 'composer', $this->optimizeAutoloader); + $this->autoloadGenerator->setDevMode($this->devMode); + $this->autoloadGenerator->dump($this->config, $localRepo, $this->package, $this->installationManager, 'composer', $this->optimizeAutoloader); + } if ($this->runScripts) { // dispatch post event $eventName = $this->update ? ScriptEvents::POST_UPDATE_CMD : ScriptEvents::POST_INSTALL_CMD; $this->eventDispatcher->dispatchCommandEvent($eventName, $this->devMode); } + + $vendorDir = $this->config->get('vendor-dir'); + if (is_dir($vendorDir)) { + touch($vendorDir); + } } return 0; @@ -361,7 +409,7 @@ class Installer } if ($this->update) { - $this->io->write('Updating dependencies'.($withDevReqs?' (including require-dev)':'').''); + $this->io->write('Updating dependencies'.($withDevReqs ? ' (including require-dev)' : '').''); $request->updateAll(); @@ -412,7 +460,7 @@ class Installer } } } elseif ($installFromLock) { - $this->io->write('Installing dependencies'.($withDevReqs?' (including require-dev)':'').' from lock file'); + $this->io->write('Installing dependencies'.($withDevReqs ? ' (including require-dev)' : '').' from lock file'); if (!$this->locker->isFresh()) { $this->io->write('Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. Run update to update them.'); @@ -432,7 +480,7 @@ class Installer $request->install($link->getTarget(), $link->getConstraint()); } } else { - $this->io->write('Installing dependencies'.($withDevReqs?' (including require-dev)':'').''); + $this->io->write('Installing dependencies'.($withDevReqs ? ' (including require-dev)' : '').''); if ($withDevReqs) { $links = array_merge($this->package->getRequires(), $this->package->getDevRequires()); @@ -449,9 +497,11 @@ class Installer $this->processDevPackages($localRepo, $pool, $policy, $repositories, $lockedRepository, $installFromLock, 'force-links'); // solve dependencies + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $policy, $pool, $installedRepo, $request); $solver = new Solver($policy, $pool, $installedRepo); try { - $operations = $solver->solve($request); + $operations = $solver->solve($request, $this->ignorePlatformReqs); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $policy, $pool, $installedRepo, $request, $operations); } catch (SolverProblemsException $e) { $this->io->write('Your requirements could not be resolved to an installable set of packages.'); $this->io->write($e->getMessage()); @@ -468,6 +518,7 @@ class Installer } $operations = $this->movePluginsToFront($operations); + $operations = $this->moveUninstallsToFront($operations); foreach ($operations as $operation) { // collect suggestions @@ -481,11 +532,6 @@ class Installer } } - $event = 'Composer\Script\ScriptEvents::PRE_PACKAGE_'.strtoupper($operation->getJobType()); - if (defined($event) && $this->runScripts) { - $this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $operation); - } - // not installing from lock, force dev packages' references if they're in root package refs if (!$installFromLock) { $package = null; @@ -501,6 +547,23 @@ class Installer $package->setDistReference($references[$package->getName()]); } } + if ('update' === $operation->getJobType() + && $operation->getTargetPackage()->isDev() + && $operation->getTargetPackage()->getVersion() === $operation->getInitialPackage()->getVersion() + && $operation->getTargetPackage()->getSourceReference() === $operation->getInitialPackage()->getSourceReference() + ) { + if ($this->io->isDebug()) { + $this->io->write(' - Skipping update of '. $operation->getTargetPackage()->getPrettyName().' to the same reference-locked version'); + $this->io->write(''); + } + + continue; + } + } + + $event = 'Composer\Script\ScriptEvents::PRE_PACKAGE_'.strtoupper($operation->getJobType()); + if (defined($event) && $this->runScripts) { + $this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $operation); } // output non-alias ops in dry run, output alias ops in debug verbosity @@ -520,11 +583,11 @@ class Installer if ($reason instanceof Rule) { switch ($reason->getReason()) { case Rule::RULE_JOB_INSTALL: - $this->io->write(' REASON: Required by root: '.$reason->getRequiredPackage()); + $this->io->write(' REASON: Required by root: '.$reason->getPrettyString($pool)); $this->io->write(''); break; case Rule::RULE_PACKAGE_REQUIRES: - $this->io->write(' REASON: '.$reason->getPrettyString()); + $this->io->write(' REASON: '.$reason->getPrettyString($pool)); $this->io->write(''); break; } @@ -569,15 +632,45 @@ class Installer continue; } - if ($package->getRequires() === array() && ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer')) { - $installerOps[] = $op; - unset($operations[$idx]); + if ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer') { + // ignore requirements to platform or composer-plugin-api + $requires = array_keys($package->getRequires()); + foreach ($requires as $index => $req) { + if ($req === 'composer-plugin-api' || preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req)) { + unset($requires[$index]); + } + } + // if there are no other requirements, move the plugin to the top of the op list + if (!count($requires)) { + $installerOps[] = $op; + unset($operations[$idx]); + } } } return array_merge($installerOps, $operations); } + /** + * Removals of packages should be executed before installations in + * case two packages resolve to the same path (due to custom installers) + * + * @param OperationInterface[] $operations + * @return OperationInterface[] reordered operation list + */ + private function moveUninstallsToFront(array $operations) + { + $uninstOps = array(); + foreach ($operations as $idx => $op) { + if ($op instanceof UninstallOperation) { + $uninstOps[] = $op; + unset($operations[$idx]); + } + } + + return array_merge($uninstOps, $operations); + } + private function createPool($withDevReqs) { $minimumStability = $this->package->getMinimumStability(); @@ -594,6 +687,10 @@ class Installer } $rootConstraints = array(); foreach ($requires as $req => $constraint) { + // skip platform requirements from the root package to avoid filtering out existing platform packages + if ($this->ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req)) { + continue; + } $rootConstraints[$req] = $constraint->getConstraint(); } @@ -602,7 +699,22 @@ class Installer private function createPolicy() { - return new DefaultPolicy($this->package->getPreferStable()); + $preferStable = null; + $preferLowest = null; + if (!$this->update && $this->locker->isLocked()) { + $preferStable = $this->locker->getPreferStable(); + $preferLowest = $this->locker->getPreferLowest(); + } + // old lock file without prefer stable/lowest will return null + // so in this case we use the composer.json info + if (null === $preferStable) { + $preferStable = $this->preferStable || $this->package->getPreferStable(); + } + if (null === $preferLowest) { + $preferLowest = $this->preferLowest; + } + + return new DefaultPolicy($preferStable, $preferLowest); } private function createRequest(Pool $pool, RootPackageInterface $rootPackage, PlatformRepository $platformRepo) @@ -631,7 +743,7 @@ class Installer || !isset($provided[$package->getName()]) || !$provided[$package->getName()]->getConstraint()->matches($constraint) ) { - $request->install($package->getName(), $constraint); + $request->fix($package->getName(), $constraint); } } @@ -785,9 +897,8 @@ class Installer } foreach ($this->updateWhitelist as $whiteListedPattern => $void) { - $cleanedWhiteListedPattern = str_replace('\\*', '.*', preg_quote($whiteListedPattern)); - - if (preg_match("{^".$cleanedWhiteListedPattern."$}i", $package->getName())) { + $patternRegexp = $this->packageNameToRegexp($whiteListedPattern); + if (preg_match($patternRegexp, $package->getName())) { return true; } } @@ -795,6 +906,19 @@ class Installer return false; } + /** + * Build a regexp from a package name, expanding * globs as required + * + * @param string $whiteListedPattern + * @return string + */ + private function packageNameToRegexp($whiteListedPattern) + { + $cleanedWhiteListedPattern = str_replace('\\*', '.*', preg_quote($whiteListedPattern)); + + return "{^" . $cleanedWhiteListedPattern . "$}i"; + } + private function extractPlatformRequirements($links) { $platformReqs = array(); @@ -844,11 +968,27 @@ class Installer $seen = array(); + $rootRequiredPackageNames = array_keys($rootRequires); + foreach ($this->updateWhitelist as $packageName => $void) { $packageQueue = new \SplQueue; $depPackages = $pool->whatProvides($packageName); - if (count($depPackages) == 0 && !in_array($packageName, $requiredPackageNames) && !in_array($packageName, array('nothing', 'lock'))) { + + $nameMatchesRequiredPackage = in_array($packageName, $requiredPackageNames, true); + + // check if the name is a glob pattern that did not match directly + if (!$nameMatchesRequiredPackage) { + $whitelistPatternRegexp = $this->packageNameToRegexp($packageName); + foreach ($rootRequiredPackageNames as $rootRequiredPackageName) { + if (preg_match($whitelistPatternRegexp, $rootRequiredPackageName)) { + $nameMatchesRequiredPackage = true; + break; + } + } + } + + if (count($depPackages) == 0 && !$nameMatchesRequiredPackage && !in_array($packageName, array('nothing', 'lock'))) { $this->io->write('Package "' . $packageName . '" listed for update is not installed. Ignoring.'); } @@ -951,6 +1091,16 @@ class Installer return $this; } + /** + * Checks, if this is a dry run (simulation mode). + * + * @return bool + */ + public function isDryRun() + { + return $this->dryRun; + } + /** * prefer source installation * @@ -1016,6 +1166,21 @@ class Installer return $this; } + + /** + * set whether to run autoloader or not + * + * @param boolean $dumpAutoloader + * @return Installer + */ + public function setDumpAutoloader($dumpAutoloader = true) + { + $this->dumpAutoloader = (boolean) $dumpAutoloader; + + return $this; + } + + /** * set whether to run scripts or not * @@ -1055,6 +1220,29 @@ class Installer return $this; } + /** + * Checks, if running in verbose mode. + * + * @return bool + */ + public function isVerbose() + { + return $this->verbose; + } + + /** + * set ignore Platform Package requirements + * + * @param boolean $ignorePlatformReqs + * @return Installer + */ + public function setIgnorePlatformRequirements($ignorePlatformReqs = false) + { + $this->ignorePlatformReqs = (boolean) $ignorePlatformReqs; + + return $this; + } + /** * restrict the update operation to a few packages, all other packages * that are already installed will be kept at their current version @@ -1072,7 +1260,7 @@ class Installer /** * Should dependencies of whitelisted packages be updated recursively? * - * @param boolean $updateDependencies + * @param boolean $updateDependencies * @return Installer */ public function setWhitelistDependencies($updateDependencies = true) @@ -1082,6 +1270,32 @@ class Installer return $this; } + /** + * Should packages be prefered in a stable version when updating? + * + * @param boolean $preferStable + * @return Installer + */ + public function setPreferStable($preferStable = true) + { + $this->preferStable = (boolean) $preferStable; + + return $this; + } + + /** + * Should packages be prefered in a lowest version when updating? + * + * @param boolean $preferLowest + * @return Installer + */ + public function setPreferLowest($preferLowest = true) + { + $this->preferLowest = (boolean) $preferLowest; + + return $this; + } + /** * Disables plugins. * diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 21b16e2fd..a43acbbda 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -14,7 +14,6 @@ namespace Composer\Installer; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; -use Composer\Plugin\PluginInstaller; use Composer\Repository\RepositoryInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\DependencyResolver\Operation\OperationInterface; diff --git a/src/Composer/Installer/InstallerEvent.php b/src/Composer/Installer/InstallerEvent.php new file mode 100644 index 000000000..a9f5a728a --- /dev/null +++ b/src/Composer/Installer/InstallerEvent.php @@ -0,0 +1,146 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Installer; + +use Composer\Composer; +use Composer\DependencyResolver\PolicyInterface; +use Composer\DependencyResolver\Operation\OperationInterface; +use Composer\DependencyResolver\Pool; +use Composer\DependencyResolver\Request; +use Composer\EventDispatcher\Event; +use Composer\IO\IOInterface; +use Composer\Repository\CompositeRepository; + +/** + * An event for all installer. + * + * @author François Pluchino + */ +class InstallerEvent extends Event +{ + /** + * @var Composer + */ + private $composer; + + /** + * @var IOInterface + */ + private $io; + + /** + * @var PolicyInterface + */ + private $policy; + + /** + * @var Pool + */ + private $pool; + + /** + * @var CompositeRepository + */ + private $installedRepo; + + /** + * @var Request + */ + private $request; + + /** + * @var OperationInterface[] + */ + private $operations; + + /** + * Constructor. + * + * @param string $eventName + * @param Composer $composer + * @param IOInterface $io + * @param PolicyInterface $policy + * @param Pool $pool + * @param CompositeRepository $installedRepo + * @param Request $request + * @param OperationInterface[] $operations + */ + public function __construct($eventName, Composer $composer, IOInterface $io, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations = array()) + { + parent::__construct($eventName); + + $this->composer = $composer; + $this->io = $io; + $this->policy = $policy; + $this->pool = $pool; + $this->installedRepo = $installedRepo; + $this->request = $request; + $this->operations = $operations; + } + + /** + * @return Composer + */ + public function getComposer() + { + return $this->composer; + } + + /** + * @return IOInterface + */ + public function getIO() + { + return $this->io; + } + + /** + * @return PolicyInterface + */ + public function getPolicy() + { + return $this->policy; + } + + /** + * @return Pool + */ + public function getPool() + { + return $this->pool; + } + + /** + * @return CompositeRepository + */ + public function getInstalledRepo() + { + return $this->installedRepo; + } + + /** + * @return Request + */ + public function getRequest() + { + return $this->request; + } + + /** + * @return OperationInterface[] + */ + public function getOperations() + { + return $this->operations; + } +} diff --git a/src/Composer/Installer/InstallerEvents.php b/src/Composer/Installer/InstallerEvents.php new file mode 100644 index 000000000..e05c92587 --- /dev/null +++ b/src/Composer/Installer/InstallerEvents.php @@ -0,0 +1,43 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Installer; + +/** + * The Installer Events. + * + * @author François Pluchino + */ +class InstallerEvents +{ + /** + * The PRE_DEPENDENCIES_SOLVING event occurs as a installer begins + * resolve operations. + * + * The event listener method receives a + * Composer\Installer\InstallerEvent instance. + * + * @var string + */ + const PRE_DEPENDENCIES_SOLVING = 'pre-dependencies-solving'; + + /** + * The POST_DEPENDENCIES_SOLVING event occurs as a installer after + * resolve operations. + * + * The event listener method receives a + * Composer\Installer\InstallerEvent instance. + * + * @var string + */ + const POST_DEPENDENCIES_SOLVING = 'post-dependencies-solving'; +} diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index b1677cec2..05cd420d7 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -17,6 +17,7 @@ use Composer\IO\IOInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; use Composer\Util\Filesystem; +use Composer\Util\ProcessExecutor; /** * Package installation manager. @@ -126,7 +127,7 @@ class LibraryInstaller implements InstallerInterface $downloadPath = $this->getPackageBasePath($package); if (strpos($package->getName(), '/')) { $packageVendorDir = dirname($downloadPath); - if (is_dir($packageVendorDir) && !glob($packageVendorDir.'/*')) { + if (is_dir($packageVendorDir) && $this->filesystem->isDirEmpty($packageVendorDir)) { @rmdir($packageVendorDir); } } @@ -196,7 +197,7 @@ class LibraryInstaller implements InstallerInterface foreach ($binaries as $bin) { $binPath = $this->getInstallPath($package).'/'.$bin; if (!file_exists($binPath)) { - $this->io->write(' Skipped installation of '.$bin.' for package '.$package->getName().': file not found in package'); + $this->io->write(' Skipped installation of bin '.$bin.' for package '.$package->getName().': file not found in package'); continue; } @@ -215,7 +216,7 @@ class LibraryInstaller implements InstallerInterface // is a fresh install of the vendor. @chmod($link, 0777 & ~umask()); } - $this->io->write(' Skipped installation of '.$bin.' for package '.$package->getName().': name conflicts with an existing file'); + $this->io->write(' Skipped installation of bin '.$bin.' for package '.$package->getName().': name conflicts with an existing file'); continue; } if (defined('PHP_WINDOWS_VERSION_BUILD')) { @@ -225,7 +226,7 @@ class LibraryInstaller implements InstallerInterface @chmod($link, 0777 & ~umask()); $link .= '.bat'; if (file_exists($link)) { - $this->io->write(' Skipped installation of '.$bin.'.bat proxy for package '.$package->getName().': a .bat proxy was already installed'); + $this->io->write(' Skipped installation of bin '.$bin.'.bat proxy for package '.$package->getName().': a .bat proxy was already installed'); } } if (!file_exists($link)) { @@ -259,10 +260,10 @@ class LibraryInstaller implements InstallerInterface foreach ($binaries as $bin) { $link = $this->binDir.'/'.basename($bin); if (is_link($link) || file_exists($link)) { - unlink($link); + $this->filesystem->unlink($link); } if (file_exists($link.'.bat')) { - unlink($link.'.bat'); + $this->filesystem->unlink($link.'.bat'); } } } @@ -296,7 +297,7 @@ class LibraryInstaller implements InstallerInterface } return "@ECHO OFF\r\n". - "SET BIN_TARGET=%~dp0/".trim(escapeshellarg($binPath), '"')."\r\n". + "SET BIN_TARGET=%~dp0/".trim(ProcessExecutor::escape($binPath), '"')."\r\n". "{$caller} \"%BIN_TARGET%\" %*\r\n"; } @@ -307,7 +308,7 @@ class LibraryInstaller implements InstallerInterface return "#!/usr/bin/env sh\n". 'SRC_DIR="`pwd`"'."\n". 'cd "`dirname "$0"`"'."\n". - 'cd '.escapeshellarg(dirname($binPath))."\n". + 'cd '.ProcessExecutor::escape(dirname($binPath))."\n". 'BIN_TARGET="`pwd`/'.basename($binPath)."\"\n". 'cd "$SRC_DIR"'."\n". '"$BIN_TARGET" "$@"'."\n"; diff --git a/src/Composer/Installer/PearInstaller.php b/src/Composer/Installer/PearInstaller.php index b44f61f14..fd3bbd976 100644 --- a/src/Composer/Installer/PearInstaller.php +++ b/src/Composer/Installer/PearInstaller.php @@ -17,6 +17,7 @@ use Composer\Composer; use Composer\Downloader\PearPackageExtractor; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; +use Composer\Util\ProcessExecutor; /** * Package installation manager. @@ -77,7 +78,7 @@ class PearInstaller extends LibraryInstaller if ($this->io->isVerbose()) { $this->io->write(' Cleaning up'); } - unlink($packageArchive); + $this->filesystem->unlink($packageArchive); } protected function getBinaries(PackageInterface $package) @@ -124,7 +125,7 @@ class PearInstaller extends LibraryInstaller "pushd .\r\n". "cd %~dp0\r\n". "set PHP_PROXY=%CD%\\composer-php.bat\r\n". - "cd ".escapeshellarg(dirname($binPath))."\r\n". + "cd ".ProcessExecutor::escape(dirname($binPath))."\r\n". "set BIN_TARGET=%CD%\\".basename($binPath)."\r\n". "popd\r\n". "%PHP_PROXY% \"%BIN_TARGET%\" %*\r\n"; @@ -134,7 +135,7 @@ class PearInstaller extends LibraryInstaller return "@echo off\r\n". "pushd .\r\n". "cd %~dp0\r\n". - "cd ".escapeshellarg(dirname($binPath))."\r\n". + "cd ".ProcessExecutor::escape(dirname($binPath))."\r\n". "set BIN_TARGET=%CD%\\".basename($binPath)."\r\n". "popd\r\n". $caller." \"%BIN_TARGET%\" %*\r\n"; diff --git a/src/Composer/Installer/PluginInstaller.php b/src/Composer/Installer/PluginInstaller.php index 61c5a2823..ddb8e5bdd 100644 --- a/src/Composer/Installer/PluginInstaller.php +++ b/src/Composer/Installer/PluginInstaller.php @@ -40,7 +40,6 @@ class PluginInstaller extends LibraryInstaller { parent::__construct($io, $composer, 'composer-plugin'); $this->installationManager = $composer->getInstallationManager(); - } /** @@ -62,7 +61,7 @@ class PluginInstaller extends LibraryInstaller } parent::install($repo, $package); - $this->composer->getPluginManager()->registerPackage($package); + $this->composer->getPluginManager()->registerPackage($package, true); } /** @@ -76,6 +75,6 @@ class PluginInstaller extends LibraryInstaller } parent::update($repo, $initial, $target); - $this->composer->getPluginManager()->registerPackage($target); + $this->composer->getPluginManager()->registerPackage($target, true); } } diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index 70b97b18d..3375329c7 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -154,8 +154,7 @@ class JsonFile if ($schema === self::LAX_SCHEMA) { $schemaData->additionalProperties = true; - $schemaData->properties->name->required = false; - $schemaData->properties->description->required = false; + $schemaData->required = array(); } $validator = new Validator(); @@ -177,11 +176,6 @@ class JsonFile /** * Encodes an array into (optionally pretty-printed) JSON * - * This code is based on the function found at: - * http://recursive-design.com/blog/2008/03/11/format-json-with-php/ - * - * Originally licensed under MIT by Dave Perrett - * * @param mixed $data Data to encode into a formatted JSON string * @param int $options json_encode options (defaults to JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) * @return string Encoded json @@ -189,7 +183,15 @@ class JsonFile public static function encode($data, $options = 448) { if (version_compare(PHP_VERSION, '5.4', '>=')) { - return json_encode($data, $options); + $json = json_encode($data, $options); + + // compact brackets to follow recent php versions + if (PHP_VERSION_ID < 50428 || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50512) || (defined('JSON_C_VERSION') && version_compare(phpversion('json'), '1.3.6', '<'))) { + $json = preg_replace('/\[\s+\]/', '[]', $json); + $json = preg_replace('/\{\s+\}/', '{}', $json); + } + + return $json; } $json = json_encode($data); @@ -202,81 +204,7 @@ class JsonFile return $json; } - $result = ''; - $pos = 0; - $strLen = strlen($json); - $indentStr = ' '; - $newLine = "\n"; - $outOfQuotes = true; - $buffer = ''; - $noescape = true; - - for ($i = 0; $i < $strLen; $i++) { - // Grab the next character in the string - $char = substr($json, $i, 1); - - // Are we inside a quoted string? - if ('"' === $char && $noescape) { - $outOfQuotes = !$outOfQuotes; - } - - if (!$outOfQuotes) { - $buffer .= $char; - $noescape = '\\' === $char ? !$noescape : true; - continue; - } elseif ('' !== $buffer) { - if ($unescapeSlashes) { - $buffer = str_replace('\\/', '/', $buffer); - } - - if ($unescapeUnicode && function_exists('mb_convert_encoding')) { - // http://stackoverflow.com/questions/2934563/how-to-decode-unicode-escape-sequences-like-u00ed-to-proper-utf-8-encoded-cha - $buffer = preg_replace_callback('/\\\\u([0-9a-f]{4})/i', function($match) { - return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE'); - }, $buffer); - } - - $result .= $buffer.$char; - $buffer = ''; - continue; - } - - if (':' === $char) { - // Add a space after the : character - $char .= ' '; - } elseif (('}' === $char || ']' === $char)) { - $pos--; - $prevChar = substr($json, $i - 1, 1); - - if ('{' !== $prevChar && '[' !== $prevChar) { - // If this character is the end of an element, - // output a new line and indent the next line - $result .= $newLine; - for ($j = 0; $j < $pos; $j++) { - $result .= $indentStr; - } - } else { - // Collapse empty {} and [] - $result = rtrim($result)."\n\n".$indentStr; - } - } - - $result .= $char; - - // If the last character was the beginning of an element, - // output a new line and indent the next line - if (',' === $char || '{' === $char || '[' === $char) { - $result .= $newLine; - - if ('{' === $char || '[' === $char) { - $pos++; - } - - for ($j = 0; $j < $pos; $j++) { - $result .= $indentStr; - } - } - } + $result = JsonFormatter::format($json, $unescapeUnicode, $unescapeSlashes); return $result; } diff --git a/src/Composer/Json/JsonFormatter.php b/src/Composer/Json/JsonFormatter.php new file mode 100644 index 000000000..025a53950 --- /dev/null +++ b/src/Composer/Json/JsonFormatter.php @@ -0,0 +1,128 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Json; + +/** + * Formats json strings used for php < 5.4 because the json_encode doesn't + * supports the flags JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE + * in these versions + * + * @author Konstantin Kudryashiv + * @author Jordi Boggiano + */ +class JsonFormatter +{ + /** + * + * This code is based on the function found at: + * http://recursive-design.com/blog/2008/03/11/format-json-with-php/ + * + * Originally licensed under MIT by Dave Perrett + * + * + * @param string $json + * @param bool $unescapeUnicode Un escape unicode + * @param bool $unescapeSlashes Un escape slashes + * @return string + */ + public static function format($json, $unescapeUnicode, $unescapeSlashes) + { + $result = ''; + $pos = 0; + $strLen = strlen($json); + $indentStr = ' '; + $newLine = "\n"; + $outOfQuotes = true; + $buffer = ''; + $noescape = true; + + for ($i = 0; $i < $strLen; $i++) { + // Grab the next character in the string + $char = substr($json, $i, 1); + + // Are we inside a quoted string? + if ('"' === $char && $noescape) { + $outOfQuotes = !$outOfQuotes; + } + + if (!$outOfQuotes) { + $buffer .= $char; + $noescape = '\\' === $char ? !$noescape : true; + continue; + } elseif ('' !== $buffer) { + if ($unescapeSlashes) { + $buffer = str_replace('\\/', '/', $buffer); + } + + if ($unescapeUnicode && function_exists('mb_convert_encoding')) { + // http://stackoverflow.com/questions/2934563/how-to-decode-unicode-escape-sequences-like-u00ed-to-proper-utf-8-encoded-cha + $buffer = preg_replace_callback('/(\\\\+)u([0-9a-f]{4})/i', function ($match) { + $l = strlen($match[1]); + + if ($l % 2) { + return str_repeat('\\', $l - 1) . mb_convert_encoding( + pack('H*', $match[2]), + 'UTF-8', + 'UCS-2BE' + ); + } + + return $match[0]; + }, $buffer); + } + + $result .= $buffer.$char; + $buffer = ''; + continue; + } + + if (':' === $char) { + // Add a space after the : character + $char .= ' '; + } elseif (('}' === $char || ']' === $char)) { + $pos--; + $prevChar = substr($json, $i - 1, 1); + + if ('{' !== $prevChar && '[' !== $prevChar) { + // If this character is the end of an element, + // output a new line and indent the next line + $result .= $newLine; + for ($j = 0; $j < $pos; $j++) { + $result .= $indentStr; + } + } else { + // Collapse empty {} and [] + $result = rtrim($result); + } + } + + $result .= $char; + + // If the last character was the beginning of an element, + // output a new line and indent the next line + if (',' === $char || '{' === $char || '[' === $char) { + $result .= $newLine; + + if ('{' === $char || '[' === $char) { + $pos++; + } + + for ($j = 0; $j < $pos; $j++) { + $result .= $indentStr; + } + } + } + + return $result; + } +} diff --git a/src/Composer/Json/JsonManipulator.php b/src/Composer/Json/JsonManipulator.php index 1cb9ff79e..d30a68385 100644 --- a/src/Composer/Json/JsonManipulator.php +++ b/src/Composer/Json/JsonManipulator.php @@ -18,6 +18,7 @@ namespace Composer\Json; class JsonManipulator { private static $RECURSE_BLOCKS; + private static $RECURSE_ARRAYS; private static $JSON_VALUE; private static $JSON_STRING; @@ -29,15 +30,19 @@ class JsonManipulator { if (!self::$RECURSE_BLOCKS) { self::$RECURSE_BLOCKS = '(?:[^{}]*|\{(?:[^{}]*|\{(?:[^{}]*|\{(?:[^{}]*|\{[^{}]*\})*\})*\})*\})*'; - self::$JSON_STRING = '"(?:\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4}|[^\0-\x09\x0a-\x1f\\\\"])*"'; - self::$JSON_VALUE = '(?:[0-9.]+|null|true|false|'.self::$JSON_STRING.'|\[[^\]]*\]|\{'.self::$RECURSE_BLOCKS.'\})'; + self::$RECURSE_ARRAYS = '(?:[^\]]*|\[(?:[^\]]*|\[(?:[^\]]*|\[(?:[^\]]*|\[[^\]]*\])*\])*\])*\]|'.self::$RECURSE_BLOCKS.')*'; + self::$JSON_STRING = '"(?:[^\0-\x09\x0a-\x1f\\\\"]+|\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4})*"'; + self::$JSON_VALUE = '(?:[0-9.]+|null|true|false|'.self::$JSON_STRING.'|\['.self::$RECURSE_ARRAYS.'\]|\{'.self::$RECURSE_BLOCKS.'\})'; } $contents = trim($contents); + if ($contents === '') { + $contents = '{}'; + } if (!$this->pregMatch('#^\{(.*)\}$#s', $contents)) { throw new \InvalidArgumentException('The json file must be an object ({})'); } - $this->newline = false !== strpos($contents, "\r\n") ? "\r\n": "\n"; + $this->newline = false !== strpos($contents, "\r\n") ? "\r\n" : "\n"; $this->contents = $contents === '{}' ? '{' . $this->newline . '}' : $contents; $this->detectIndenting(); } @@ -47,7 +52,7 @@ class JsonManipulator return $this->contents . $this->newline; } - public function addLink($type, $package, $constraint) + public function addLink($type, $package, $constraint, $sortPackages = false) { $decoded = JsonFile::parseJson($this->contents); @@ -85,6 +90,13 @@ class JsonManipulator } } + if (true === $sortPackages) { + $requirements = json_decode($links, true); + + ksort($requirements); + $links = $this->format($requirements); + } + $this->contents = $matches[1] . $matches[2] . $links . $matches[4]; return true; @@ -122,17 +134,25 @@ class JsonManipulator } $subName = null; - if (false !== strpos($name, '.')) { + if (in_array($mainNode, array('config', 'repositories')) && false !== strpos($name, '.')) { list($name, $subName) = explode('.', $name, 2); } // main node content not match-able - $nodeRegex = '#("'.$mainNode.'":\s*\{)('.self::$RECURSE_BLOCKS.')(\})#s'; - if (!$this->pregMatch($nodeRegex, $this->contents, $match)) { - return false; + $nodeRegex = '{^(\s*\{\s*(?:'.self::$JSON_STRING.'\s*:\s*'.self::$JSON_VALUE.'\s*,\s*)*?)'. + '('.preg_quote(JsonFile::encode($mainNode)).'\s*:\s*\{)('.self::$RECURSE_BLOCKS.')(\})(.*)}s'; + try { + if (!$this->pregMatch($nodeRegex, $this->contents, $match)) { + return false; + } + } catch (\RuntimeException $e) { + if ($e->getCode() === PREG_BACKTRACK_LIMIT_ERROR) { + return false; + } + throw $e; } - $children = $match[2]; + $children = $match[3]; // invalid match due to un-regexable content, abort if (!@json_decode('{'.$children.'}')) { @@ -172,7 +192,7 @@ class JsonManipulator $children = $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $children; } - $this->contents = preg_replace($nodeRegex, addcslashes('${1}'.$children.'$3', '\\'), $this->contents); + $this->contents = preg_replace($nodeRegex, addcslashes('${1}${2}'.$children.'${4}${5}', '\\'), $this->contents); return true; } @@ -187,23 +207,36 @@ class JsonManipulator } // no node content match-able - $nodeRegex = '#("'.$mainNode.'":\s*\{)('.self::$RECURSE_BLOCKS.')(\})#s'; - if (!$this->pregMatch($nodeRegex, $this->contents, $match)) { - return false; + $nodeRegex = '{^(\s*\{\s*(?:'.self::$JSON_STRING.'\s*:\s*'.self::$JSON_VALUE.'\s*,\s*)*?)'. + '('.preg_quote(JsonFile::encode($mainNode)).'\s*:\s*\{)('.self::$RECURSE_BLOCKS.')(\})(.*)}s'; + try { + if (!$this->pregMatch($nodeRegex, $this->contents, $match)) { + return false; + } + } catch (\RuntimeException $e) { + if ($e->getCode() === PREG_BACKTRACK_LIMIT_ERROR) { + return false; + } + throw $e; } - $children = $match[2]; + $children = $match[3]; // invalid match due to un-regexable content, abort - if (!@json_decode('{'.$children.'}')) { + if (!@json_decode('{'.$children.'}', true)) { return false; } $subName = null; - if (false !== strpos($name, '.')) { + if (in_array($mainNode, array('config', 'repositories')) && false !== strpos($name, '.')) { list($name, $subName) = explode('.', $name, 2); } + // no node to remove + if (!isset($decoded[$mainNode][$name]) || ($subName && !isset($decoded[$mainNode][$name][$subName]))) { + return true; + } + // try and find a match for the subkey if ($this->pregMatch('{"'.preg_quote($name).'"\s*:}i', $children)) { // find best match for the value of "name" @@ -222,11 +255,13 @@ class JsonManipulator } } } + } else { + $childrenClean = $children; } // no child data left, $name was the only key in if (!trim($childrenClean)) { - $this->contents = preg_replace($nodeRegex, '$1'.$this->newline.$this->indent.'}', $this->contents); + $this->contents = preg_replace($nodeRegex, '$1$2'.$this->newline.$this->indent.'$4$5', $this->contents); // we have a subname, so we restore the rest of $name if ($subName !== null) { @@ -241,12 +276,12 @@ class JsonManipulator $that = $this; $this->contents = preg_replace_callback($nodeRegex, function ($matches) use ($that, $name, $subName, $childrenClean) { if ($subName !== null) { - $curVal = json_decode('{'.$matches[2].'}', true); + $curVal = json_decode('{'.$matches[3].'}', true); unset($curVal[$name][$subName]); $childrenClean = substr($that->format($curVal, 0), 1, -1); } - return $matches[1] . $childrenClean . $matches[3]; + return $matches[1] . $matches[2] . $childrenClean . $matches[4] . $matches[5]; }, $this->contents); return true; @@ -333,17 +368,17 @@ class JsonManipulator if ($count === false) { switch (preg_last_error()) { case PREG_NO_ERROR: - throw new \RuntimeException('Failed to execute regex: PREG_NO_ERROR'); + throw new \RuntimeException('Failed to execute regex: PREG_NO_ERROR', PREG_NO_ERROR); case PREG_INTERNAL_ERROR: - throw new \RuntimeException('Failed to execute regex: PREG_INTERNAL_ERROR'); + throw new \RuntimeException('Failed to execute regex: PREG_INTERNAL_ERROR', PREG_INTERNAL_ERROR); case PREG_BACKTRACK_LIMIT_ERROR: - throw new \RuntimeException('Failed to execute regex: PREG_BACKTRACK_LIMIT_ERROR'); + throw new \RuntimeException('Failed to execute regex: PREG_BACKTRACK_LIMIT_ERROR', PREG_BACKTRACK_LIMIT_ERROR); case PREG_RECURSION_LIMIT_ERROR: - throw new \RuntimeException('Failed to execute regex: PREG_RECURSION_LIMIT_ERROR'); + throw new \RuntimeException('Failed to execute regex: PREG_RECURSION_LIMIT_ERROR', PREG_RECURSION_LIMIT_ERROR); case PREG_BAD_UTF8_ERROR: - throw new \RuntimeException('Failed to execute regex: PREG_BAD_UTF8_ERROR'); + throw new \RuntimeException('Failed to execute regex: PREG_BAD_UTF8_ERROR', PREG_BAD_UTF8_ERROR); case PREG_BAD_UTF8_OFFSET_ERROR: - throw new \RuntimeException('Failed to execute regex: PREG_BAD_UTF8_OFFSET_ERROR'); + throw new \RuntimeException('Failed to execute regex: PREG_BAD_UTF8_OFFSET_ERROR', PREG_BAD_UTF8_OFFSET_ERROR); default: throw new \RuntimeException('Failed to execute regex: Unknown error'); } diff --git a/src/Composer/Json/JsonValidationException.php b/src/Composer/Json/JsonValidationException.php index 0b2b2ba70..2fa5eb489 100644 --- a/src/Composer/Json/JsonValidationException.php +++ b/src/Composer/Json/JsonValidationException.php @@ -21,10 +21,10 @@ class JsonValidationException extends Exception { protected $errors; - public function __construct($message, $errors = array()) + public function __construct($message, $errors = array(), Exception $previous = null) { $this->errors = $errors; - parent::__construct($message); + parent::__construct($message, 0, $previous); } public function getErrors() diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php index 6f1fb5095..3ab9c944d 100644 --- a/src/Composer/Package/AliasPackage.php +++ b/src/Composer/Package/AliasPackage.php @@ -209,6 +209,10 @@ class AliasPackage extends BasePackage implements CompletePackageInterface { return $this->aliasOf->getSourceUrl(); } + public function getSourceUrls() + { + return $this->aliasOf->getSourceUrls(); + } public function getSourceReference() { return $this->aliasOf->getSourceReference(); @@ -217,6 +221,14 @@ class AliasPackage extends BasePackage implements CompletePackageInterface { return $this->aliasOf->setSourceReference($reference); } + public function setSourceMirrors($mirrors) + { + return $this->aliasOf->setSourceMirrors($mirrors); + } + public function getSourceMirrors() + { + return $this->aliasOf->getSourceMirrors(); + } public function getDistType() { return $this->aliasOf->getDistType(); @@ -225,14 +237,38 @@ class AliasPackage extends BasePackage implements CompletePackageInterface { return $this->aliasOf->getDistUrl(); } + public function getDistUrls() + { + return $this->aliasOf->getDistUrls(); + } public function getDistReference() { return $this->aliasOf->getDistReference(); } + public function setDistReference($reference) + { + return $this->aliasOf->setDistReference($reference); + } public function getDistSha1Checksum() { return $this->aliasOf->getDistSha1Checksum(); } + public function setTransportOptions(array $options) + { + return $this->aliasOf->setTransportOptions($options); + } + public function getTransportOptions() + { + return $this->aliasOf->getTransportOptions(); + } + public function setDistMirrors($mirrors) + { + return $this->aliasOf->setDistMirrors($mirrors); + } + public function getDistMirrors() + { + return $this->aliasOf->getDistMirrors(); + } public function getScripts() { return $this->aliasOf->getScripts(); @@ -245,6 +281,10 @@ class AliasPackage extends BasePackage implements CompletePackageInterface { return $this->aliasOf->getAutoload(); } + public function getDevAutoload() + { + return $this->aliasOf->getDevAutoload(); + } public function getIncludePaths() { return $this->aliasOf->getIncludePaths(); @@ -293,6 +333,14 @@ class AliasPackage extends BasePackage implements CompletePackageInterface { return $this->aliasOf->getArchiveExcludes(); } + public function isAbandoned() + { + return $this->aliasOf->isAbandoned(); + } + public function getReplacementPackage() + { + return $this->aliasOf->getReplacementPackage(); + } public function __toString() { return parent::__toString().' (alias of '.$this->aliasOf->getVersion().')'; diff --git a/src/Composer/Package/Archiver/ArchivableFilesFinder.php b/src/Composer/Package/Archiver/ArchivableFilesFinder.php index f6cadbe21..44c682616 100644 --- a/src/Composer/Package/Archiver/ArchivableFilesFinder.php +++ b/src/Composer/Package/Archiver/ArchivableFilesFinder.php @@ -52,6 +52,10 @@ class ArchivableFilesFinder extends \FilterIterator $this->finder = new Finder\Finder(); $filter = function (\SplFileInfo $file) use ($sources, $filters, $fs) { + if ($file->isLink() && strpos($file->getLinkTarget(), $sources) !== 0) { + return false; + } + $relativePath = preg_replace( '#^'.preg_quote($sources, '#').'#', '', diff --git a/src/Composer/Package/Archiver/ArchiveManager.php b/src/Composer/Package/Archiver/ArchiveManager.php index 80865245e..d600a7a75 100644 --- a/src/Composer/Package/Archiver/ArchiveManager.php +++ b/src/Composer/Package/Archiver/ArchiveManager.php @@ -14,7 +14,7 @@ namespace Composer\Package\Archiver; use Composer\Downloader\DownloadManager; use Composer\Package\PackageInterface; -use Composer\Package\RootPackage; +use Composer\Package\RootPackageInterface; use Composer\Util\Filesystem; use Composer\Json\JsonFile; @@ -72,7 +72,7 @@ class ArchiveManager */ public function getPackageFilename(PackageInterface $package) { - $nameParts = array(preg_replace('#[^a-z0-9-_.]#i', '-', $package->getName())); + $nameParts = array(preg_replace('#[^a-z0-9-_]#i', '-', $package->getName())); if (preg_match('{^[a-f0-9]{40}$}', $package->getDistReference())) { $nameParts = array_merge($nameParts, array($package->getDistReference(), $package->getDistType())); @@ -133,11 +133,11 @@ class ArchiveManager return $target; } - if ($package instanceof RootPackage) { + if ($package instanceof RootPackageInterface) { $sourcePath = realpath('.'); } else { // Directory used to download the sources - $sourcePath = sys_get_temp_dir().'/composer_archiver/'.$packageName; + $sourcePath = sys_get_temp_dir().'/composer_archive'.uniqid(); $filesystem->ensureDirectoryExists($sourcePath); // Download sources @@ -154,13 +154,18 @@ class ArchiveManager } // Create the archive - $archivePath = $usableArchiver->archive($sourcePath, $target, $format, $package->getArchiveExcludes()); + $tempTarget = sys_get_temp_dir().'/composer_archive'.uniqid().'.'.$format; + $filesystem->ensureDirectoryExists(dirname($tempTarget)); + + $archivePath = $usableArchiver->archive($sourcePath, $tempTarget, $format, $package->getArchiveExcludes()); + rename($archivePath, $target); // cleanup temporary download - if (!$package instanceof RootPackage) { + if (!$package instanceof RootPackageInterface) { $filesystem->removeDirectory($sourcePath); } + $filesystem->remove($tempTarget); - return $archivePath; + return $target; } } diff --git a/src/Composer/Package/Archiver/BaseExcludeFilter.php b/src/Composer/Package/Archiver/BaseExcludeFilter.php index bba2003a8..d724f31e4 100644 --- a/src/Composer/Package/Archiver/BaseExcludeFilter.php +++ b/src/Composer/Package/Archiver/BaseExcludeFilter.php @@ -82,17 +82,14 @@ abstract class BaseExcludeFilter function ($line) use ($lineParser) { $line = trim($line); - $commentHash = strpos($line, '#'); - if ($commentHash !== false) { - $line = substr($line, 0, $commentHash); + if (!$line || 0 === strpos($line, '#')) { + return; } - if ($line) { - return call_user_func($lineParser, $line); - } - - return null; - }, $lines), + return call_user_func($lineParser, $line); + }, + $lines + ), function ($pattern) { return $pattern !== null; } @@ -136,11 +133,15 @@ abstract class BaseExcludeFilter if (strlen($rule) && $rule[0] === '/') { $pattern .= '^/'; $rule = substr($rule, 1); - } elseif (false === strpos($rule, '/') || strlen($rule) - 1 === strpos($rule, '/')) { + } elseif (strlen($rule) - 1 === strpos($rule, '/')) { + $pattern .= '/'; + $rule = substr($rule, 0, -1); + } elseif (false === strpos($rule, '/')) { $pattern .= '/'; } - $pattern .= substr(Finder\Glob::toRegex($rule), 2, -2); + // remove delimiters as well as caret (^) and dollar sign ($) from the regex + $pattern .= substr(Finder\Glob::toRegex($rule), 2, -2) . '(?=$|/)'; return array($pattern . '#', $negate, false); } diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index a783ca7fc..965e5ddc8 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -44,11 +44,17 @@ abstract class BasePackage implements PackageInterface 'dev' => self::STABILITY_DEV, ); + /** + * READ-ONLY: The package id, public for fast access in dependency solver + * @var int + */ + public $id; + protected $name; protected $prettyName; protected $repository; - protected $id; + protected $transportOptions; /** * All descendants' constructors should call this parent constructor @@ -60,6 +66,7 @@ abstract class BasePackage implements PackageInterface $this->prettyName = $name; $this->name = strtolower($name); $this->id = -1; + $this->transportOptions = array(); } /** @@ -133,6 +140,24 @@ abstract class BasePackage implements PackageInterface return $this->repository; } + /** + * {@inheritDoc} + */ + public function getTransportOptions() + { + return $this->transportOptions; + } + + /** + * Configures the list of options to download package dist files + * + * @param array $options + */ + public function setTransportOptions(array $options) + { + $this->transportOptions = $options; + } + /** * checks if this package is a platform package * diff --git a/src/Composer/Package/CompletePackage.php b/src/Composer/Package/CompletePackage.php index 418c87d9e..0de1609d0 100644 --- a/src/Composer/Package/CompletePackage.php +++ b/src/Composer/Package/CompletePackage.php @@ -27,6 +27,7 @@ class CompletePackage extends Package implements CompletePackageInterface protected $homepage; protected $scripts = array(); protected $support = array(); + protected $abandoned = false; /** * @param array $scripts @@ -47,7 +48,7 @@ class CompletePackage extends Package implements CompletePackageInterface /** * Set the repositories * - * @param string $repositories + * @param array $repositories */ public function setRepositories($repositories) { @@ -169,4 +170,30 @@ class CompletePackage extends Package implements CompletePackageInterface { return $this->support; } + + /** + * @return boolean + */ + public function isAbandoned() + { + return (boolean) $this->abandoned; + } + + /** + * @param boolean|string $abandoned + */ + public function setAbandoned($abandoned) + { + $this->abandoned = $abandoned; + } + + /** + * If the package is abandoned and has a suggested replacement, this method returns it + * + * @return string|null + */ + public function getReplacementPackage() + { + return is_string($this->abandoned) ? $this->abandoned : null; + } } diff --git a/src/Composer/Package/CompletePackageInterface.php b/src/Composer/Package/CompletePackageInterface.php index a341766d3..8263a6535 100644 --- a/src/Composer/Package/CompletePackageInterface.php +++ b/src/Composer/Package/CompletePackageInterface.php @@ -78,4 +78,18 @@ interface CompletePackageInterface extends PackageInterface * @return array */ public function getSupport(); + + /** + * Returns if the package is abandoned or not + * + * @return boolean + */ + public function isAbandoned(); + + /** + * If the package is abandoned and has a suggested replacement, this method returns it + * + * @return string + */ + public function getReplacementPackage(); } diff --git a/src/Composer/Package/Dumper/ArrayDumper.php b/src/Composer/Package/Dumper/ArrayDumper.php index be94adec4..714c5183b 100644 --- a/src/Composer/Package/Dumper/ArrayDumper.php +++ b/src/Composer/Package/Dumper/ArrayDumper.php @@ -31,6 +31,7 @@ class ArrayDumper 'extra', 'installationSource' => 'installation-source', 'autoload', + 'devAutoload' => 'autoload-dev', 'notificationUrl' => 'notification-url', 'includePaths' => 'include-path', ); @@ -48,6 +49,9 @@ class ArrayDumper $data['source']['type'] = $package->getSourceType(); $data['source']['url'] = $package->getSourceUrl(); $data['source']['reference'] = $package->getSourceReference(); + if ($mirrors = $package->getSourceMirrors()) { + $data['source']['mirrors'] = $mirrors; + } } if ($package->getDistType()) { @@ -55,6 +59,9 @@ class ArrayDumper $data['dist']['url'] = $package->getDistUrl(); $data['dist']['reference'] = $package->getDistReference(); $data['dist']['shasum'] = $package->getDistSha1Checksum(); + if ($mirrors = $package->getDistMirrors()) { + $data['dist']['mirrors'] = $mirrors; + } } if ($package->getArchiveExcludes()) { @@ -98,6 +105,10 @@ class ArrayDumper if (isset($data['keywords']) && is_array($data['keywords'])) { sort($data['keywords']); } + + if ($package->isAbandoned()) { + $data['abandoned'] = $package->getReplacementPackage() ?: true; + } } if ($package instanceof RootPackageInterface) { @@ -107,6 +118,10 @@ class ArrayDumper } } + if (count($package->getTransportOptions()) > 0) { + $data['transport-options'] = $package->getTransportOptions(); + } + return $data; } diff --git a/src/Composer/Package/Link.php b/src/Composer/Package/Link.php index 159fa3ae9..5aba8b119 100644 --- a/src/Composer/Package/Link.php +++ b/src/Composer/Package/Link.php @@ -13,7 +13,6 @@ namespace Composer\Package; use Composer\Package\LinkConstraint\LinkConstraintInterface; -use Composer\Package\PackageInterface; /** * Represents a link between two packages, represented by their names diff --git a/src/Composer/Package/LinkConstraint/MultiConstraint.php b/src/Composer/Package/LinkConstraint/MultiConstraint.php index f2eff93ec..3871aeb2f 100644 --- a/src/Composer/Package/LinkConstraint/MultiConstraint.php +++ b/src/Composer/Package/LinkConstraint/MultiConstraint.php @@ -78,6 +78,6 @@ class MultiConstraint implements LinkConstraintInterface $constraints[] = $constraint->__toString(); } - return '['.implode($this->conjunctive ? ', ' : ' | ', $constraints).']'; + return '['.implode($this->conjunctive ? ' ' : ' || ', $constraints).']'; } } diff --git a/src/Composer/Package/LinkConstraint/SpecificConstraint.php b/src/Composer/Package/LinkConstraint/SpecificConstraint.php index eda09c6ec..e0904dbbf 100644 --- a/src/Composer/Package/LinkConstraint/SpecificConstraint.php +++ b/src/Composer/Package/LinkConstraint/SpecificConstraint.php @@ -50,5 +50,4 @@ abstract class SpecificConstraint implements LinkConstraintInterface // implementations must implement a method of this format: // not declared abstract here because type hinting violates parameter coherence (TODO right word?) // public function matchSpecific( $provider); - } diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index 3940bdeb0..558a24e35 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -25,13 +25,15 @@ use Composer\Package\Version\VersionParser; class ArrayLoader implements LoaderInterface { protected $versionParser; + protected $loadOptions; - public function __construct(VersionParser $parser = null) + public function __construct(VersionParser $parser = null, $loadOptions = false) { if (!$parser) { $parser = new VersionParser; } $this->versionParser = $parser; + $this->loadOptions = $loadOptions; } public function load(array $config, $class = 'Composer\Package\CompletePackage') @@ -65,7 +67,7 @@ class ArrayLoader implements LoaderInterface throw new \UnexpectedValueException('Package '.$config['name'].'\'s bin key should be an array, '.gettype($config['bin']).' given.'); } foreach ($config['bin'] as $key => $bin) { - $config['bin'][$key]= ltrim($bin, '/'); + $config['bin'][$key] = ltrim($bin, '/'); } $package->setBinaries($config['bin']); } @@ -85,6 +87,9 @@ class ArrayLoader implements LoaderInterface $package->setSourceType($config['source']['type']); $package->setSourceUrl($config['source']['url']); $package->setSourceReference($config['source']['reference']); + if (isset($config['source']['mirrors'])) { + $package->setSourceMirrors($config['source']['mirrors']); + } } if (isset($config['dist'])) { @@ -101,6 +106,9 @@ class ArrayLoader implements LoaderInterface $package->setDistUrl($config['dist']['url']); $package->setDistReference(isset($config['dist']['reference']) ? $config['dist']['reference'] : null); $package->setDistSha1Checksum(isset($config['dist']['shasum']) ? $config['dist']['shasum'] : null); + if (isset($config['dist']['mirrors'])) { + $package->setDistMirrors($config['dist']['mirrors']); + } } foreach (Package\BasePackage::$supportedLinkTypes as $type => $opts) { @@ -130,6 +138,10 @@ class ArrayLoader implements LoaderInterface $package->setAutoload($config['autoload']); } + if (isset($config['autoload-dev'])) { + $package->setDevAutoload($config['autoload-dev']); + } + if (isset($config['include-path'])) { $package->setIncludePaths($config['include-path']); } @@ -183,6 +195,10 @@ class ArrayLoader implements LoaderInterface if (isset($config['support'])) { $package->setSupport($config['support']); } + + if (isset($config['abandoned'])) { + $package->setAbandoned($config['abandoned']); + } } if ($aliasNormalized = $this->getBranchAlias($config)) { @@ -193,6 +209,10 @@ class ArrayLoader implements LoaderInterface } } + if ($this->loadOptions && isset($config['transport-options'])) { + $package->setTransportOptions($config['transport-options']); + } + return $package; } @@ -204,7 +224,7 @@ class ArrayLoader implements LoaderInterface */ public function getBranchAlias(array $config) { - if ('dev-' !== substr($config['version'], 0, 4) + if (('dev-' !== substr($config['version'], 0, 4) && '-dev' !== substr($config['version'], -4)) || !isset($config['extra']['branch-alias']) || !is_array($config['extra']['branch-alias']) ) { @@ -228,6 +248,14 @@ class ArrayLoader implements LoaderInterface continue; } + // If using numeric aliases ensure the alias is a valid subversion + if(($sourcePrefix = $this->versionParser->parseNumericAliasPrefix($sourceBranch)) + && ($targetPrefix = $this->versionParser->parseNumericAliasPrefix($targetBranch)) + && (stripos($targetPrefix, $sourcePrefix) !== 0) + ) { + continue; + } + return $validatedTargetBranch; } } diff --git a/src/Composer/Package/Loader/RootPackageLoader.php b/src/Composer/Package/Loader/RootPackageLoader.php index a067fa6a8..250f2952f 100644 --- a/src/Composer/Package/Loader/RootPackageLoader.php +++ b/src/Composer/Package/Loader/RootPackageLoader.php @@ -22,6 +22,7 @@ use Composer\Repository\Vcs\HgDriver; use Composer\IO\NullIO; use Composer\Util\ProcessExecutor; use Composer\Util\Git as GitUtil; +use Composer\Util\Svn as SvnUtil; /** * ArrayLoader built for the sole purpose of loading the root package @@ -130,7 +131,7 @@ class RootPackageLoader extends ArrayLoader $minimumStability = $stabilities[$minimumStability]; foreach ($requires as $reqName => $reqVersion) { // parse explicit stability flags to the most unstable - if (preg_match('{^[^,\s]*?@('.implode('|', array_keys($stabilities)).')$}i', $reqVersion, $match)) { + if (preg_match('{^[^@]*?@('.implode('|', array_keys($stabilities)).')$}i', $reqVersion, $match)) { $name = strtolower($reqName); $stability = $stabilities[VersionParser::normalizeStability($match[1])]; @@ -179,14 +180,18 @@ class RootPackageLoader extends ArrayLoader return $version; } - return $this->guessHgVersion($config); + $version = $this->guessHgVersion($config); + if (null !== $version) { + return $version; + } + + return $this->guessSvnVersion($config); } } private function guessGitVersion(array $config) { - $util = new GitUtil; - $util->cleanEnv(); + GitUtil::cleanEnv(); // try to fetch current version from git tags if (0 === $this->process->execute('git describe --exact-match --tags', $output)) { @@ -204,7 +209,7 @@ class RootPackageLoader extends ArrayLoader // find current branch and collect all branch names foreach ($this->process->splitLines($output) as $branch) { - if ($branch && preg_match('{^(?:\* ) *(\(no branch\)|\(detached from [a-f0-9]+\)|\S+) *([a-f0-9]+) .*$}', $branch, $match)) { + if ($branch && preg_match('{^(?:\* ) *(\(no branch\)|\(detached from \S+\)|\S+) *([a-f0-9]+) .*$}', $branch, $match)) { if ($match[1] === '(no branch)' || substr($match[1], 0, 10) === '(detached ') { $version = 'dev-'.$match[2]; $isFeatureBranch = true; @@ -295,4 +300,32 @@ class RootPackageLoader extends ArrayLoader return $version; } + + private function guessSvnVersion(array $config) + { + SvnUtil::cleanEnv(); + + // try to fetch current version from svn + if (0 === $this->process->execute('svn info --xml', $output)) { + $trunkPath = isset($config['trunk-path']) ? preg_quote($config['trunk-path'], '#') : 'trunk'; + $branchesPath = isset($config['branches-path']) ? preg_quote($config['branches-path'], '#') : 'branches'; + $tagsPath = isset($config['tags-path']) ? preg_quote($config['tags-path'], '#') : 'tags'; + + $urlPattern = '#.*/('.$trunkPath.'|('.$branchesPath.'|'. $tagsPath .')/(.*))#'; + + if (preg_match($urlPattern, $output, $matches)) { + if (isset($matches[2]) && ($branchesPath === $matches[2] || $tagsPath === $matches[2])) { + // we are in a branches path + $version = $this->versionParser->normalizeBranch($matches[3]); + if ('9999999-dev' === $version) { + $version = 'dev-'.$matches[3]; + } + + return $version; + } + + return $this->versionParser->normalize(trim($matches[1])); + } + } + } } diff --git a/src/Composer/Package/Loader/ValidatingArrayLoader.php b/src/Composer/Package/Loader/ValidatingArrayLoader.php index a877739e1..9a6f4dd32 100644 --- a/src/Composer/Package/Loader/ValidatingArrayLoader.php +++ b/src/Composer/Package/Loader/ValidatingArrayLoader.php @@ -14,25 +14,32 @@ namespace Composer\Package\Loader; use Composer\Package; use Composer\Package\BasePackage; +use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\Version\VersionParser; +use Composer\Repository\PlatformRepository; /** * @author Jordi Boggiano */ class ValidatingArrayLoader implements LoaderInterface { + const CHECK_ALL = 1; + const CHECK_UNBOUND_CONSTRAINTS = 1; + private $loader; private $versionParser; private $errors; private $warnings; private $config; private $strictName; + private $flags; - public function __construct(LoaderInterface $loader, $strictName = true, VersionParser $parser = null) + public function __construct(LoaderInterface $loader, $strictName = true, VersionParser $parser = null, $flags = 0) { $this->loader = $loader; $this->versionParser = $parser ?: new VersionParser(); $this->strictName = $strictName; + $this->flags = $flags; } public function load(array $config, $class = 'Composer\Package\CompletePackage') @@ -51,8 +58,8 @@ class ValidatingArrayLoader implements LoaderInterface try { $this->versionParser->normalize($this->config['version']); } catch (\Exception $e) { - unset($this->config['version']); $this->errors[] = 'version : invalid value ('.$this->config['version'].'): '.$e->getMessage(); + unset($this->config['version']); } } @@ -142,6 +149,8 @@ class ValidatingArrayLoader implements LoaderInterface } } + $unboundConstraint = new VersionConstraint('=', $this->versionParser->normalize('dev-master')); + foreach (array_keys(BasePackage::$supportedLinkTypes) as $linkType) { if ($this->validateArray($linkType) && isset($this->config[$linkType])) { foreach ($this->config[$linkType] as $package => $constraint) { @@ -153,10 +162,21 @@ class ValidatingArrayLoader implements LoaderInterface unset($this->config[$linkType][$package]); } elseif ('self.version' !== $constraint) { try { - $this->versionParser->parseConstraints($constraint); + $linkConstraint = $this->versionParser->parseConstraints($constraint); } catch (\Exception $e) { $this->errors[] = $linkType.'.'.$package.' : invalid version constraint ('.$e->getMessage().')'; unset($this->config[$linkType][$package]); + continue; + } + + // check requires for unbound constraints on non-platform packages + if ( + ($this->flags & self::CHECK_UNBOUND_CONSTRAINTS) + && 'require' === $linkType + && $linkConstraint->matches($unboundConstraint) + && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $package) + ) { + $this->warnings[] = $linkType.'.'.$package.' : unbound version constraints ('.$constraint.') should be avoided'; } } } @@ -210,6 +230,7 @@ class ValidatingArrayLoader implements LoaderInterface // TODO validate package repositories' packages using this recursively $this->validateFlatArray('include-path'); + $this->validateArray('transport-options'); // branch alias validation if (isset($this->config['extra']['branch-alias'])) { @@ -230,6 +251,17 @@ class ValidatingArrayLoader implements LoaderInterface if ('-dev' !== substr($validatedTargetBranch, -4)) { $this->warnings[] = 'extra.branch-alias.'.$sourceBranch.' : the target branch ('.$targetBranch.') must be a parseable number like 2.0-dev'; unset($this->config['extra']['branch-alias'][$sourceBranch]); + + continue; + } + + // If using numeric aliases ensure the alias is a valid subversion + if(($sourcePrefix = $this->versionParser->parseNumericAliasPrefix($sourceBranch)) + && ($targetPrefix = $this->versionParser->parseNumericAliasPrefix($targetBranch)) + && (stripos($targetPrefix, $sourcePrefix) !== 0) + ) { + $this->warnings[] = 'extra.branch-alias.'.$sourceBranch.' : the target branch ('.$targetBranch.') is not a valid numeric alias for this version'; + unset($this->config['extra']['branch-alias'][$sourceBranch]); } } } diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index a928584e4..40ff8907c 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -16,7 +16,6 @@ use Composer\Json\JsonFile; use Composer\Installer\InstallationManager; use Composer\Repository\RepositoryManager; use Composer\Util\ProcessExecutor; -use Composer\Package\AliasPackage; use Composer\Repository\ArrayRepository; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Loader\ArrayLoader; @@ -56,7 +55,7 @@ class Locker $this->repositoryManager = $repositoryManager; $this->installationManager = $installationManager; $this->hash = $hash; - $this->loader = new ArrayLoader(); + $this->loader = new ArrayLoader(null, true); $this->dumper = new ArrayDumper(); $this->process = new ProcessExecutor($io); } @@ -174,6 +173,24 @@ class Locker return isset($lockData['stability-flags']) ? $lockData['stability-flags'] : array(); } + public function getPreferStable() + { + $lockData = $this->getLockData(); + + // return null if not set to allow caller logic to choose the + // right behavior since old lock files have no prefer-stable + return isset($lockData['prefer-stable']) ? $lockData['prefer-stable'] : null; + } + + public function getPreferLowest() + { + $lockData = $this->getLockData(); + + // return null if not set to allow caller logic to choose the + // right behavior since old lock files have no prefer-lowest + return isset($lockData['prefer-lowest']) ? $lockData['prefer-lowest'] : null; + } + public function getAliases() { $lockData = $this->getLockData(); @@ -204,19 +221,25 @@ class Locker * @param array $aliases array of aliases * @param string $minimumStability * @param array $stabilityFlags + * @param bool $preferStable + * @param bool $preferLowest * * @return bool */ - public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags) + public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable, $preferLowest) { $lock = array( - '_readme' => array('This file locks the dependencies of your project to a known state', 'Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file'), + '_readme' => array('This file locks the dependencies of your project to a known state', + 'Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file', + 'This file is @gener'.'ated automatically'), 'hash' => $this->hash, 'packages' => null, 'packages-dev' => null, 'aliases' => array(), 'minimum-stability' => $minimumStability, 'stability-flags' => $stabilityFlags, + 'prefer-stable' => $preferStable, + 'prefer-lowest' => $preferLowest, ); foreach ($aliases as $package => $versions) { @@ -285,7 +308,7 @@ class Locker $time = $this->getPackageTime($package) ?: $time; } if (null !== $time) { - $spec['time'] = $time; + $spec['time'] = $time; } unset($spec['installation-source']); @@ -327,16 +350,15 @@ class Locker $sourceRef = $package->getSourceReference() ?: $package->getDistReference(); switch ($sourceType) { case 'git': - $util = new GitUtil; - $util->cleanEnv(); + GitUtil::cleanEnv(); - if (0 === $this->process->execute('git log -n1 --pretty=%ct '.escapeshellarg($sourceRef), $output, $path) && preg_match('{^\s*\d+\s*$}', $output)) { + if (0 === $this->process->execute('git log -n1 --pretty=%ct '.ProcessExecutor::escape($sourceRef), $output, $path) && preg_match('{^\s*\d+\s*$}', $output)) { $datetime = new \DateTime('@'.trim($output), new \DateTimeZone('UTC')); } break; case 'hg': - if (0 === $this->process->execute('hg log --template "{date|hgdate}" -r '.escapeshellarg($sourceRef), $output, $path) && preg_match('{^\s*(\d+)\s*}', $output, $match)) { + if (0 === $this->process->execute('hg log --template "{date|hgdate}" -r '.ProcessExecutor::escape($sourceRef), $output, $path) && preg_match('{^\s*(\d+)\s*}', $output, $match)) { $datetime = new \DateTime('@'.$match[1], new \DateTimeZone('UTC')); } break; diff --git a/src/Composer/Package/Package.php b/src/Composer/Package/Package.php index b8a8252bc..0c98a0e56 100644 --- a/src/Composer/Package/Package.php +++ b/src/Composer/Package/Package.php @@ -13,6 +13,7 @@ namespace Composer\Package; use Composer\Package\Version\VersionParser; +use Composer\Util\ComposerMirror; /** * Core package definitions that are needed to resolve dependencies and install packages @@ -27,10 +28,12 @@ class Package extends BasePackage protected $sourceType; protected $sourceUrl; protected $sourceReference; + protected $sourceMirrors; protected $distType; protected $distUrl; protected $distReference; protected $distSha1Checksum; + protected $distMirrors; protected $version; protected $prettyVersion; protected $releaseDate; @@ -47,6 +50,7 @@ class Package extends BasePackage protected $devRequires = array(); protected $suggests = array(); protected $autoload = array(); + protected $devAutoload = array(); protected $includePaths = array(); protected $archiveExcludes = array(); @@ -216,6 +220,30 @@ class Package extends BasePackage return $this->sourceReference; } + /** + * @param array|null $mirrors + */ + public function setSourceMirrors($mirrors) + { + $this->sourceMirrors = $mirrors; + } + + /** + * {@inheritDoc} + */ + public function getSourceMirrors() + { + return $this->sourceMirrors; + } + + /** + * {@inheritDoc} + */ + public function getSourceUrls() + { + return $this->getUrls($this->sourceUrl, $this->sourceMirrors, $this->sourceReference, $this->sourceType, 'source'); + } + /** * @param string $type */ @@ -280,6 +308,30 @@ class Package extends BasePackage return $this->distSha1Checksum; } + /** + * @param array|null $mirrors + */ + public function setDistMirrors($mirrors) + { + $this->distMirrors = $mirrors; + } + + /** + * {@inheritDoc} + */ + public function getDistMirrors() + { + return $this->distMirrors; + } + + /** + * {@inheritDoc} + */ + public function getDistUrls() + { + return $this->getUrls($this->distUrl, $this->distMirrors, $this->distReference, $this->distType, 'dist'); + } + /** * {@inheritDoc} */ @@ -440,6 +492,24 @@ class Package extends BasePackage return $this->autoload; } + /** + * Set the dev autoload mapping + * + * @param array $devAutoload Mapping of dev autoloading rules + */ + public function setDevAutoload(array $devAutoload) + { + $this->devAutoload = $devAutoload; + } + + /** + * {@inheritDoc} + */ + public function getDevAutoload() + { + return $this->devAutoload; + } + /** * Sets the list of paths added to PHP's include path. * @@ -493,4 +563,45 @@ class Package extends BasePackage { return $this->archiveExcludes; } + + /** + * Replaces current version and pretty version with passed values. + * It also sets stability. + * + * @param string $version The package's normalized version + * @param string $prettyVersion The package's non-normalized version + */ + public function replaceVersion($version, $prettyVersion) + { + $this->version = $version; + $this->prettyVersion = $prettyVersion; + + $this->stability = VersionParser::parseStability($version); + $this->dev = $this->stability === 'dev'; + } + + protected function getUrls($url, $mirrors, $ref, $type, $urlType) + { + if (!$url) { + return array(); + } + $urls = array($url); + if ($mirrors) { + foreach ($mirrors as $mirror) { + if ($urlType === 'dist') { + $mirrorUrl = ComposerMirror::processUrl($mirror['url'], $this->name, $this->version, $ref, $type); + } elseif ($urlType === 'source' && $type === 'git') { + $mirrorUrl = ComposerMirror::processGitUrl($mirror['url'], $this->name, $url, $type); + } elseif ($urlType === 'source' && $type === 'hg') { + $mirrorUrl = ComposerMirror::processHgUrl($mirror['url'], $this->name, $url, $type); + } + if (!in_array($mirrorUrl, $urls)) { + $func = $mirror['preferred'] ? 'array_unshift' : 'array_push'; + $func($urls, $mirrorUrl); + } + } + } + + return $urls; + } } diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index a3c8a2793..a51274d5b 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -115,6 +115,13 @@ interface PackageInterface */ public function getSourceUrl(); + /** + * Returns the repository urls of this package including mirrors, e.g. git://github.com/naderman/composer.git + * + * @return array + */ + public function getSourceUrls(); + /** * Returns the repository reference of this package, e.g. master, 1.0.0 or a commit hash for git * @@ -122,6 +129,13 @@ interface PackageInterface */ public function getSourceReference(); + /** + * Returns the source mirrors of this package + * + * @return array|null + */ + public function getSourceMirrors(); + /** * Returns the type of the distribution archive of this version, e.g. zip, tarball * @@ -136,6 +150,13 @@ interface PackageInterface */ public function getDistUrl(); + /** + * Returns the urls of the distribution archive of this version, including mirrors + * + * @return array + */ + public function getDistUrls(); + /** * Returns the reference of the distribution archive of this version, e.g. master, 1.0.0 or a commit hash for git * @@ -150,6 +171,13 @@ interface PackageInterface */ public function getDistSha1Checksum(); + /** + * Returns the dist mirrors of this package + * + * @return array|null + */ + public function getDistMirrors(); + /** * Returns the version of this package * @@ -231,13 +259,25 @@ interface PackageInterface * * {"": {""}} * - * Type is either "psr-0" or "pear". Namespaces are mapped to directories - * for autoloading using the type specified. + * Type is either "psr-4", "psr-0", "classmap" or "files". Namespaces are mapped to + * directories for autoloading using the type specified. * * @return array Mapping of autoloading rules */ public function getAutoload(); + /** + * Returns an associative array of dev autoloading rules + * + * {"": {""}} + * + * Type is either "psr-4", "psr-0", "classmap" or "files". Namespaces are mapped to + * directories for autoloading using the type specified. + * + * @return array Mapping of dev autoloading rules + */ + public function getDevAutoload(); + /** * Returns a list of directories which should get added to PHP's * include path. @@ -301,4 +341,11 @@ interface PackageInterface * @return array */ public function getArchiveExcludes(); + + /** + * Returns a list of options to download package dist files + * + * @return array + */ + public function getTransportOptions(); } diff --git a/src/Composer/Package/Version/VersionParser.php b/src/Composer/Package/Version/VersionParser.php index ee13b77a5..9707ac017 100644 --- a/src/Composer/Package/Version/VersionParser.php +++ b/src/Composer/Package/Version/VersionParser.php @@ -89,7 +89,7 @@ class VersionParser * @param string $version * @param string $fullVersion optional complete version string to give more context * @throws \UnexpectedValueException - * @return array + * @return string */ public function normalize($version, $fullVersion = null) { @@ -103,6 +103,11 @@ class VersionParser $version = $match[1]; } + // ignore build metadata + if (preg_match('{^([^,\s+]+)\+[^\s]+$}', $version, $match)) { + $version = $match[1]; + } + // match master-like branches if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) { return '9999999-dev'; @@ -122,6 +127,12 @@ class VersionParser } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)'.self::$modifierRegex.'$}i', $version, $matches)) { // match date-based versioning $version = preg_replace('{\D}', '-', $matches[1]); $index = 2; + } elseif (preg_match('{^v?(\d{4,})(\.\d+)?(\.\d+)?(\.\d+)?'.self::$modifierRegex.'$}i', $version, $matches)) { + $version = $matches[1] + .(!empty($matches[2]) ? $matches[2] : '.0') + .(!empty($matches[3]) ? $matches[3] : '.0') + .(!empty($matches[4]) ? $matches[4] : '.0'); + $index = 5; } // add version modifiers if a version was matched @@ -144,7 +155,8 @@ class VersionParser if (preg_match('{(.*?)[.-]?dev$}i', $version, $match)) { try { return $this->normalizeBranch($match[1]); - } catch (\Exception $e) {} + } catch (\Exception $e) { + } } $extraMessage = ''; @@ -157,11 +169,27 @@ class VersionParser throw new \UnexpectedValueException('Invalid version string "'.$version.'"'.$extraMessage); } + /** + * Extract numeric prefix from alias, if it is in numeric format, suitable for + * version comparison + * + * @param string $branch Branch name (e.g. 2.1.x-dev) + * @return string|false Numeric prefix if present (e.g. 2.1.) or false + */ + public function parseNumericAliasPrefix($branch) + { + if (preg_match('/^(?P(\d+\\.)*\d+)(?:\.x)?-dev$/i', $branch, $matches)) { + return $matches['version']."."; + } + + return false; + } + /** * Normalizes a branch name to be able to perform comparisons on it * * @param string $name - * @return array + * @return string */ public function normalizeBranch($name) { @@ -171,10 +199,10 @@ class VersionParser return $this->normalize($name); } - if (preg_match('#^v?(\d+)(\.(?:\d+|[x*]))?(\.(?:\d+|[x*]))?(\.(?:\d+|[x*]))?$#i', $name, $matches)) { + if (preg_match('#^v?(\d+)(\.(?:\d+|[xX*]))?(\.(?:\d+|[xX*]))?(\.(?:\d+|[xX*]))?$#i', $name, $matches)) { $version = ''; for ($i = 1; $i < 5; $i++) { - $version .= isset($matches[$i]) ? str_replace('*', 'x', $matches[$i]) : '.x'; + $version .= isset($matches[$i]) ? str_replace(array('*', 'X'), 'x', $matches[$i]) : '.x'; } return str_replace('x', '9999999', $version).'-dev'; @@ -223,11 +251,10 @@ class VersionParser $constraints = $match[1]; } - $orConstraints = preg_split('{\s*\|\s*}', trim($constraints)); + $orConstraints = preg_split('{\s*\|\|?\s*}', trim($constraints)); $orGroups = array(); foreach ($orConstraints as $constraints) { - $andConstraints = preg_split('{\s*,\s*}', $constraints); - + $andConstraints = preg_split('{(?< ,]) *(? 1) { $constraintObjects = array(); foreach ($andConstraints as $constraint) { @@ -266,16 +293,18 @@ class VersionParser } } - if (preg_match('{^[x*](\.[x*])*$}i', $constraint)) { + if (preg_match('{^[xX*](\.[xX*])*$}i', $constraint)) { return array(new EmptyConstraint); } + $versionRegex = '(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?'.self::$modifierRegex; + // match tilde constraints // like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous // version, to ensure that unstable instances of the current version are allowed. // however, if a stability suffix is added to the constraint, then a >= match on the current version is // used instead - if (preg_match('{^~>?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?'.self::$modifierRegex.'?$}i', $constraint, $matches)) { + if (preg_match('{^~>?'.$versionRegex.'$}i', $constraint, $matches)) { if (substr($constraint, 0, 2) === '~>') { throw new \UnexpectedValueException( 'Could not parse version constraint '.$constraint.': '. @@ -322,8 +351,39 @@ class VersionParser ); } + // match caret constraints + if (preg_match('{^\^'.$versionRegex.'($)}i', $constraint, $matches)) { + // Work out which position in the version we are operating at + if ('0' !== $matches[1] || '' === $matches[2]) { + $position = 1; + } elseif ('0' !== $matches[2] || '' === $matches[3]) { + $position = 2; + } else { + $position = 3; + } + + // Calculate the stability suffix + $stabilitySuffix = ''; + if (empty($matches[5]) && empty($matches[7])) { + $stabilitySuffix .= '-dev'; + } + + $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1)); + $lowerBound = new VersionConstraint('>=', $lowVersion); + + // For upper bound, we increment the position of one more significance, + // but highPosition = 0 would be illegal + $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev'; + $upperBound = new VersionConstraint('<', $highVersion); + + return array( + $lowerBound, + $upperBound + ); + } + // match wildcard constraints - if (preg_match('{^(\d+)(?:\.(\d+))?(?:\.(\d+))?\.[x*]$}', $constraint, $matches)) { + if (preg_match('{^(\d+)(?:\.(\d+))?(?:\.(\d+))?\.[xX*]$}', $constraint, $matches)) { if (isset($matches[3]) && '' !== $matches[3]) { $position = 3; } elseif (isset($matches[2]) && '' !== $matches[2]) { @@ -345,6 +405,33 @@ class VersionParser ); } + // match hyphen constraints + if (preg_match('{^(?P'.$versionRegex.') +- +(?P'.$versionRegex.')($)}i', $constraint, $matches)) { + // Calculate the stability suffix + $lowStabilitySuffix = ''; + if (empty($matches[6]) && empty($matches[8])) { + $lowStabilitySuffix = '-dev'; + } + + $lowVersion = $this->normalize($matches['from']); + $lowerBound = new VersionConstraint('>=', $lowVersion . $lowStabilitySuffix); + + $highVersion = $matches[10]; + if ((!empty($matches[11]) && !empty($matches[12])) || !empty($matches[14]) || !empty($matches[16])) { + $highVersion = $this->normalize($matches['to']); + $upperBound = new VersionConstraint('<=', $highVersion); + } else { + $highMatch = array('', $matches[10], $matches[11], $matches[12], $matches[13]); + $highVersion = $this->manipulateVersionString($highMatch, empty($matches[11]) ? 1 : 2, 1) . '-dev'; + $upperBound = new VersionConstraint('<', $highVersion); + } + + return array( + $lowerBound, + $upperBound + ); + } + // match operators constraints if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) { try { @@ -353,13 +440,14 @@ class VersionParser if (!empty($stabilityModifier) && $this->parseStability($version) === 'stable') { $version .= '-' . $stabilityModifier; } elseif ('<' === $matches[1]) { - if (!preg_match('/-stable$/', strtolower($matches[2]))) { + if (!preg_match('/-' . self::$modifierRegex . '$/', strtolower($matches[2]))) { $version .= '-dev'; } } return array(new VersionConstraint($matches[1] ?: '=', $version)); - } catch (\Exception $e) { } + } catch (\Exception $e) { + } } $message = 'Could not parse version constraint '.$constraint; diff --git a/src/Composer/Package/Version/VersionSelector.php b/src/Composer/Package/Version/VersionSelector.php new file mode 100644 index 000000000..b26f8915f --- /dev/null +++ b/src/Composer/Package/Version/VersionSelector.php @@ -0,0 +1,144 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package\Version; + +use Composer\DependencyResolver\Pool; +use Composer\Package\PackageInterface; +use Composer\Package\Loader\ArrayLoader; +use Composer\Package\Dumper\ArrayDumper; + +/** + * Selects the best possible version for a package + * + * @author Ryan Weaver + */ +class VersionSelector +{ + private $pool; + + private $parser; + + public function __construct(Pool $pool) + { + $this->pool = $pool; + } + + /** + * Given a package name and optional version, returns the latest PackageInterface + * that matches. + * + * @param string $packageName + * @param string $targetPackageVersion + * @return PackageInterface|bool + */ + public function findBestCandidate($packageName, $targetPackageVersion = null) + { + $constraint = $targetPackageVersion ? $this->getParser()->parseConstraints($targetPackageVersion) : null; + $candidates = $this->pool->whatProvides($packageName, $constraint, true); + + if (!$candidates) { + return false; + } + + // select highest version if we have many + $package = reset($candidates); + foreach ($candidates as $candidate) { + if (version_compare($package->getVersion(), $candidate->getVersion(), '<')) { + $package = $candidate; + } + } + + return $package; + } + + /** + * Given a concrete version, this returns a ~ constraint (when possible) + * that should be used, for example, in composer.json. + * + * For example: + * * 1.2.1 -> ~1.2 + * * 1.2 -> ~1.2 + * * v3.2.1 -> ~3.2 + * * 2.0-beta.1 -> ~2.0@beta + * * dev-master -> ~2.1@dev (dev version with alias) + * * dev-master -> dev-master (dev versions are untouched) + * + * @param PackageInterface $package + * @return string + */ + public function findRecommendedRequireVersion(PackageInterface $package) + { + $version = $package->getVersion(); + if (!$package->isDev()) { + return $this->transformVersion($version, $package->getPrettyVersion(), $package->getStability()); + } + + $loader = new ArrayLoader($this->getParser()); + $dumper = new ArrayDumper(); + $extra = $loader->getBranchAlias($dumper->dump($package)); + if ($extra) { + $extra = preg_replace('{^(\d+\.\d+\.\d+)(\.9999999)-dev$}', '$1.0', $extra, -1, $count); + if ($count) { + $extra = str_replace('.9999999', '.0', $extra); + + return $this->transformVersion($extra, $extra, 'dev'); + } + } + + return $package->getPrettyVersion(); + } + + private function transformVersion($version, $prettyVersion, $stability) + { + // attempt to transform 2.1.1 to 2.1 + // this allows you to upgrade through minor versions + $semanticVersionParts = explode('.', $version); + $op = '~'; + + // check to see if we have a semver-looking version + if (count($semanticVersionParts) == 4 && preg_match('{^0\D?}', $semanticVersionParts[3])) { + // remove the last parts (i.e. the patch version number and any extra) + if ($semanticVersionParts[0] === '0') { + if ($semanticVersionParts[1] === '0') { + $semanticVersionParts[3] = '*'; + } else { + $semanticVersionParts[2] = '*'; + unset($semanticVersionParts[3]); + } + $op = ''; + } else { + unset($semanticVersionParts[2], $semanticVersionParts[3]); + } + $version = implode('.', $semanticVersionParts); + } else { + return $prettyVersion; + } + + // append stability flag if not default + if ($stability != 'stable') { + $version .= '@'.$stability; + } + + // 2.1 -> ~2.1 + return $op.$version; + } + + private function getParser() + { + if ($this->parser === null) { + $this->parser = new VersionParser(); + } + + return $this->parser; + } +} diff --git a/src/Composer/Plugin/CommandEvent.php b/src/Composer/Plugin/CommandEvent.php index 0f75bed9e..0697df97a 100644 --- a/src/Composer/Plugin/CommandEvent.php +++ b/src/Composer/Plugin/CommandEvent.php @@ -45,10 +45,12 @@ class CommandEvent extends Event * @param string $commandName The command name * @param InputInterface $input * @param OutputInterface $output + * @param array $args Arguments passed by the user + * @param array $flags Optional flags to pass data not as argument */ - public function __construct($name, $commandName, $input, $output) + public function __construct($name, $commandName, $input, $output, array $args = array(), array $flags = array()) { - parent::__construct($name); + parent::__construct($name, $args, $flags); $this->commandName = $commandName; $this->input = $input; $this->output = $output; diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index d7c3ae07a..9e4c12391 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -28,30 +28,32 @@ use Composer\DependencyResolver\Pool; * Plugin manager * * @author Nils Adermann + * @author Jordi Boggiano */ class PluginManager { protected $composer; protected $io; - protected $globalRepository; + protected $globalComposer; protected $versionParser; protected $plugins = array(); + protected $registeredPlugins = array(); private static $classCounter = 0; /** * Initializes plugin manager * - * @param Composer $composer * @param IOInterface $io - * @param RepositoryInterface $globalRepository + * @param Composer $composer + * @param Composer $globalComposer */ - public function __construct(Composer $composer, IOInterface $io, RepositoryInterface $globalRepository = null) + public function __construct(IOInterface $io, Composer $composer, Composer $globalComposer = null) { - $this->composer = $composer; $this->io = $io; - $this->globalRepository = $globalRepository; + $this->composer = $composer; + $this->globalComposer = $globalComposer; $this->versionParser = new VersionParser(); } @@ -61,12 +63,12 @@ class PluginManager public function loadInstalledPlugins() { $repo = $this->composer->getRepositoryManager()->getLocalRepository(); - + $globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; if ($repo) { $this->loadRepository($repo); } - if ($this->globalRepository) { - $this->loadRepository($this->globalRepository); + if ($globalRepo) { + $this->loadRepository($globalRepo); } } @@ -103,6 +105,8 @@ class PluginManager * call this method as early as possible. * * @param RepositoryInterface $repo Repository to scan for plugins to install + * + * @throws \RuntimeException */ public function loadRepository(RepositoryInterface $repo) { @@ -186,22 +190,31 @@ class PluginManager * instead for BC * * @param PackageInterface $package + * @param bool $failOnMissingClasses By default this silently skips plugins that can not be found, but if set to true it fails with an exception + * + * @throws \UnexpectedValueException */ - public function registerPackage(PackageInterface $package) + public function registerPackage(PackageInterface $package, $failOnMissingClasses = false) { $oldInstallerPlugin = ($package->getType() === 'composer-installer'); + if (in_array($package->getName(), $this->registeredPlugins)) { + return; + } + $extra = $package->getExtra(); if (empty($extra['class'])) { throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.'); } $classes = is_array($extra['class']) ? $extra['class'] : array($extra['class']); - $pool = new Pool('dev'); $localRepo = $this->composer->getRepositoryManager()->getLocalRepository(); + $globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; + + $pool = new Pool('dev'); $pool->addRepository($localRepo); - if ($this->globalRepository) { - $pool->addRepository($this->globalRepository); + if ($globalRepo) { + $pool->addRepository($globalRepo); } $autoloadPackages = array($package->getName() => $package); @@ -210,7 +223,7 @@ class PluginManager $generator = $this->composer->getAutoloadGenerator(); $autoloads = array(); foreach ($autoloadPackages as $autoloadPackage) { - $downloadPath = $this->getInstallPath($autoloadPackage, ($this->globalRepository && $this->globalRepository->hasPackage($autoloadPackage))); + $downloadPath = $this->getInstallPath($autoloadPackage, ($globalRepo && $globalRepo->hasPackage($autoloadPackage))); $autoloads[] = array($autoloadPackage, $downloadPath); } @@ -230,9 +243,12 @@ class PluginManager if ($oldInstallerPlugin) { $installer = new $class($this->io, $this->composer); $this->composer->getInstallationManager()->addInstaller($installer); - } else { + } elseif (class_exists($class)) { $plugin = new $class(); $this->addPlugin($plugin); + $this->registeredPlugins[] = $package->getName(); + } elseif ($failOnMissingClasses) { + throw new \UnexpectedValueException('Plugin '.$package->getName().' could not be initialized, class not found: '.$class); } } } @@ -251,9 +267,6 @@ class PluginManager return $this->composer->getInstallationManager()->getInstallPath($package); } - $targetDir = $package->getTargetDir(); - $vendorDir = $this->composer->getConfig()->get('home').'/vendor'; - - return ($vendorDir ? $vendorDir.'/' : '').$package->getPrettyName().($targetDir ? '/'.$targetDir : ''); + return $this->globalComposer->getInstallationManager()->getInstallPath($package); } } diff --git a/src/Composer/Repository/ArrayRepository.php b/src/Composer/Repository/ArrayRepository.php index 9b60b8db8..f08f9cda7 100644 --- a/src/Composer/Repository/ArrayRepository.php +++ b/src/Composer/Repository/ArrayRepository.php @@ -98,7 +98,7 @@ class ArrayRepository implements RepositoryInterface } } - return $matches; + return array_values($matches); } /** diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php index 869e4757f..38936e40a 100644 --- a/src/Composer/Repository/ArtifactRepository.php +++ b/src/Composer/Repository/ArtifactRepository.php @@ -74,6 +74,49 @@ class ArtifactRepository extends ArrayRepository } } + /** + * Find a file by name, returning the one that has the shortest path. + * + * @param \ZipArchive $zip + * @param $filename + * @return bool|int + */ + private function locateFile(\ZipArchive $zip, $filename) + { + $indexOfShortestMatch = false; + $lengthOfShortestMatch = -1; + + for ($i = 0; $i < $zip->numFiles; $i++) { + $stat = $zip->statIndex($i); + if (strcmp(basename($stat['name']), $filename) === 0) { + $directoryName = dirname($stat['name']); + if ($directoryName == '.') { + //if composer.json is in root directory + //it has to be the one to use. + return $i; + } + + if (strpos($directoryName, '\\') !== false || + strpos($directoryName, '/') !== false) { + //composer.json files below first directory are rejected + continue; + } + + $length = strlen($stat['name']); + if ($indexOfShortestMatch == false || $length < $lengthOfShortestMatch) { + //Check it's not a directory. + $contents = $zip->getFromIndex($i); + if ($contents !== false) { + $indexOfShortestMatch = $i; + $lengthOfShortestMatch = $length; + } + } + } + } + + return $indexOfShortestMatch; + } + private function getComposerInformation(\SplFileInfo $file) { $zip = new \ZipArchive(); @@ -83,7 +126,7 @@ class ArtifactRepository extends ArrayRepository return false; } - $foundFileIndex = $zip->locateName('composer.json', \ZipArchive::FL_NODIR); + $foundFileIndex = $this->locateFile($zip, 'composer.json'); if (false === $foundFileIndex) { return false; } @@ -96,8 +139,7 @@ class ArtifactRepository extends ArrayRepository $package = JsonFile::parseJson($json, $composerFile); $package['dist'] = array( 'type' => 'zip', - 'url' => $file->getRealPath(), - 'reference' => $file->getBasename(), + 'url' => $file->getPathname(), 'shasum' => sha1_file($file->getRealPath()) ); diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 1d686cae1..ad3c9996b 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -13,6 +13,7 @@ namespace Composer\Repository; use Composer\Package\Loader\ArrayLoader; +use Composer\Package\Package; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; use Composer\Package\Version\VersionParser; @@ -29,7 +30,7 @@ use Composer\EventDispatcher\EventDispatcher; /** * @author Jordi Boggiano */ -class ComposerRepository extends ArrayRepository implements StreamableRepositoryInterface +class ComposerRepository extends ArrayRepository { protected $config; protected $options; @@ -42,6 +43,7 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository protected $searchUrl; protected $hasProviders = false; protected $providersUrl; + protected $lazyProvidersUrl; protected $providerListing; protected $providers = array(); protected $providersByUid = array(); @@ -49,8 +51,8 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository protected $rootAliases; protected $allowSslDowngrade = false; protected $eventDispatcher; - private $rawData; - private $minimalPackages; + protected $sourceMirrors; + protected $distMirrors; private $degradedMode = false; private $rootData; @@ -81,11 +83,11 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository $this->config = $config; $this->options = $repoConfig['options']; $this->url = $repoConfig['url']; - $this->baseUrl = rtrim(preg_replace('{^(.*)(?:/packages.json)?(?:[?#].*)?$}', '$1', $this->url), '/'); + $this->baseUrl = rtrim(preg_replace('{^(.*)(?:/[^/\\]+.json)?(?:[?#].*)?$}', '$1', $this->url), '/'); $this->io = $io; $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$'); $this->loader = new ArrayLoader(); - $this->rfs = new RemoteFilesystem($this->io, $this->options); + $this->rfs = new RemoteFilesystem($this->io, $this->config, $this->options); $this->eventDispatcher = $eventDispatcher; } @@ -94,56 +96,71 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository $this->rootAliases = $rootAliases; } - public function getPackages() + /** + * {@inheritDoc} + */ + public function findPackage($name, $version) { - if ($this->hasProviders()) { - throw new \LogicException('Composer repositories that have providers can not load the complete list of packages, use getProviderNames instead.'); + if (!$this->hasProviders()) { + return parent::findPackage($name, $version); + } + // normalize version & name + $versionParser = new VersionParser(); + $version = $versionParser->normalize($version); + $name = strtolower($name); + + foreach ($this->getProviderNames() as $providerName) { + if ($name === $providerName) { + $packages = $this->whatProvides(new Pool('dev'), $providerName); + foreach ($packages as $package) { + if ($name == $package->getName() && $version === $package->getVersion()) { + return $package; + } + } + } } - - return parent::getPackages(); } /** * {@inheritDoc} */ - public function getMinimalPackages() + public function findPackages($name, $version = null) { - if (isset($this->minimalPackages)) { - return $this->minimalPackages; + if (!$this->hasProviders()) { + return parent::findPackages($name, $version); } + // normalize name + $name = strtolower($name); - if (null === $this->rawData) { - $this->rawData = $this->loadDataFromServer(); + // normalize version + if (null !== $version) { + $versionParser = new VersionParser(); + $version = $versionParser->normalize($version); } - $this->minimalPackages = array(); - $versionParser = new VersionParser; + $packages = array(); - foreach ($this->rawData as $package) { - $version = !empty($package['version_normalized']) ? $package['version_normalized'] : $versionParser->normalize($package['version']); - $data = array( - 'name' => strtolower($package['name']), - 'repo' => $this, - 'version' => $version, - 'raw' => $package, - ); - if (!empty($package['replace'])) { - $data['replace'] = $package['replace']; - } - if (!empty($package['provide'])) { - $data['provide'] = $package['provide']; + foreach ($this->getProviderNames() as $providerName) { + if ($name === $providerName) { + $packages = $this->whatProvides(new Pool('dev'), $providerName); + foreach ($packages as $package) { + if ($name == $package->getName() && (null === $version || $version === $package->getVersion())) { + $packages[] = $package; + } + } } + } - // add branch aliases - if ($aliasNormalized = $this->loader->getBranchAlias($package)) { - $data['alias'] = preg_replace('{(\.9{7})+}', '.x', $aliasNormalized); - $data['alias_normalized'] = $aliasNormalized; - } + return $packages; + } - $this->minimalPackages[] = $data; + public function getPackages() + { + if ($this->hasProviders()) { + throw new \LogicException('Composer repositories that have providers can not load the complete list of packages, use getProviderNames instead.'); } - return $this->minimalPackages; + return parent::getPackages(); } /** @@ -156,7 +173,8 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository if ($this->searchUrl && $mode === self::SEARCH_FULLTEXT) { $url = str_replace('%query%', $query, $this->searchUrl); - $json = $this->rfs->getContents($url, $url, false); + $hostname = parse_url($url, PHP_URL_HOST) ?: $url; + $json = $this->rfs->getContents($hostname, $url, false); $results = JsonFile::parseJson($json, $url); return $results['results']; @@ -186,6 +204,11 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository $this->loadProviderListings($this->loadRootServerFile()); } + if ($this->lazyProvidersUrl) { + // Can not determine list of provided packages for lazy repositories + return array(); + } + if ($this->providersUrl) { return array_keys($this->providerListing); } @@ -199,29 +222,15 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository return $providers; } - /** - * {@inheritDoc} - */ - public function loadPackage(array $data) + protected function configurePackageTransportOptions(PackageInterface $package) { - $package = $this->createPackage($data['raw'], 'Composer\Package\Package'); - if ($package instanceof AliasPackage) { - $package = $package->getAliasOf(); - } - $package->setRepository($this); - - return $package; - } + foreach ($package->getDistUrls() as $url) { + if (strpos($url, $this->baseUrl) === 0) { + $package->setTransportOptions($this->options); - /** - * {@inheritDoc} - */ - public function loadAliasPackage(array $data, PackageInterface $aliasOf) - { - $aliasPackage = $this->createAliasPackage($aliasOf, $data['version'], $data['alias']); - $aliasPackage->setRepository($this); - - return $aliasPackage; + return; + } + } } public function hasProviders() @@ -256,7 +265,11 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository $this->loadProviderListings($this->loadRootServerFile()); } - if ($this->providersUrl) { + if ($this->lazyProvidersUrl && !isset($this->providerListing[$name])) { + $hash = null; + $url = str_replace('%package%', $name, $this->lazyProvidersUrl); + $cacheKey = false; + } elseif ($this->providersUrl) { // package does not exist in this repo if (!isset($this->providerListing[$name])) { return array(); @@ -277,7 +290,7 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository $cacheKey = null; } - if ($this->cache->sha256($cacheKey) === $hash) { + if ($cacheKey && $this->cache->sha256($cacheKey) === $hash) { $packages = json_decode($this->cache->read($cacheKey), true); } else { $packages = $this->fetchFile($url, $cacheKey, $hash); @@ -303,25 +316,6 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository } } } else { - if (isset($version['provide']) || isset($version['replace'])) { - // collect names - $names = array( - strtolower($version['name']) => true, - ); - if (isset($version['provide'])) { - foreach ($version['provide'] as $target => $constraint) { - $names[strtolower($target)] = true; - } - } - if (isset($version['replace'])) { - foreach ($version['replace'] as $target => $constraint) { - $names[strtolower($target)] = true; - } - } - $names = array_keys($names); - } else { - $names = array(strtolower($version['name'])); - } if (!$pool->isPackageAcceptable(strtolower($version['name']), VersionParser::parseStability($version['version']))) { continue; } @@ -347,10 +341,10 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository // handle root package aliases unset($rootAliasData); - if (isset($this->rootAliases[$name][$package->getVersion()])) { - $rootAliasData = $this->rootAliases[$name][$package->getVersion()]; - } elseif ($package instanceof AliasPackage && isset($this->rootAliases[$name][$package->getAliasOf()->getVersion()])) { - $rootAliasData = $this->rootAliases[$name][$package->getAliasOf()->getVersion()]; + if (isset($this->rootAliases[$package->getName()][$package->getVersion()])) { + $rootAliasData = $this->rootAliases[$package->getName()][$package->getVersion()]; + } elseif ($package instanceof AliasPackage && isset($this->rootAliases[$package->getName()][$package->getAliasOf()->getVersion()])) { + $rootAliasData = $this->rootAliases[$package->getName()][$package->getAliasOf()->getVersion()]; } if (isset($rootAliasData)) { @@ -381,6 +375,17 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository } } + /** + * Adds a new package to the repository + * + * @param PackageInterface $package + */ + public function addPackage(PackageInterface $package) + { + parent::addPackage($package); + $this->configurePackageTransportOptions($package); + } + protected function loadRootServerFile() { if (null !== $this->rootData) { @@ -393,7 +398,7 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository $jsonUrlParts = parse_url($this->url); - if (isset($jsonUrlParts['path']) && false !== strpos($jsonUrlParts['path'], '/packages.json')) { + if (isset($jsonUrlParts['path']) && false !== strpos($jsonUrlParts['path'], '.json')) { $jsonUrl = $this->url; } else { $jsonUrl = $this->url . '/packages.json'; @@ -414,6 +419,29 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository $this->searchUrl = $this->canonicalizeUrl($data['search']); } + if (!empty($data['mirrors'])) { + foreach ($data['mirrors'] as $mirror) { + if (!empty($mirror['git-url'])) { + $this->sourceMirrors['git'][] = array('url' => $mirror['git-url'], 'preferred' => !empty($mirror['preferred'])); + } + if (!empty($mirror['hg-url'])) { + $this->sourceMirrors['hg'][] = array('url' => $mirror['hg-url'], 'preferred' => !empty($mirror['preferred'])); + } + if (!empty($mirror['dist-url'])) { + $this->distMirrors[] = array('url' => $mirror['dist-url'], 'preferred' => !empty($mirror['preferred'])); + } + } + } + + if (!empty($data['warning'])) { + $this->io->write('Warning from '.$this->url.': '.$data['warning'].''); + } + + if (!empty($data['providers-lazy-url'])) { + $this->lazyProvidersUrl = $this->canonicalizeUrl($data['providers-lazy-url']); + $this->hasProviders = true; + } + if ($this->allowSslDowngrade) { $this->url = str_replace('https://', 'http://', $this->url); } @@ -527,7 +555,14 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository $data['notification-url'] = $this->notifyUrl; } - return $this->loader->load($data, 'Composer\Package\CompletePackage'); + $package = $this->loader->load($data, 'Composer\Package\CompletePackage'); + if (isset($this->sourceMirrors[$package->getSourceType()])) { + $package->setSourceMirrors($this->sourceMirrors[$package->getSourceType()]); + } + $package->setDistMirrors($this->distMirrors); + $this->configurePackageTransportOptions($package); + + return $package; } catch (\Exception $e) { throw new \RuntimeException('Could not load package '.(isset($data['name']) ? $data['name'] : json_encode($data)).' in '.$this->url.': ['.get_class($e).'] '.$e->getMessage(), 0, $e); } @@ -535,7 +570,7 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository protected function fetchFile($filename, $cacheKey = null, $sha256 = null) { - if (!$cacheKey) { + if (null === $cacheKey) { $cacheKey = $filename; $filename = $this->baseUrl.'/'.$filename; } @@ -547,7 +582,9 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository if ($this->eventDispatcher) { $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); } - $json = $preFileDownloadEvent->getRemoteFilesystem()->getContents($filename, $filename, false); + + $hostname = parse_url($filename, PHP_URL_HOST) ?: $filename; + $json = $preFileDownloadEvent->getRemoteFilesystem()->getContents($hostname, $filename, false); if ($sha256 && $sha256 !== hash('sha256', $json)) { if ($retries) { usleep(100000); @@ -559,7 +596,9 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository throw new RepositorySecurityException('The contents of '.$filename.' do not match its signature. This should indicate a man-in-the-middle attack. Try running composer again and report this if you think it is a mistake.'); } $data = JsonFile::parseJson($json, $filename); - $this->cache->write($cacheKey, $json); + if ($cacheKey) { + $this->cache->write($cacheKey, $json); + } break; } catch (\Exception $e) { @@ -572,7 +611,7 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository throw $e; } - if ($contents = $this->cache->read($cacheKey)) { + if ($cacheKey && ($contents = $this->cache->read($cacheKey))) { if (!$this->degradedMode) { $this->io->write(''.$e->getMessage().''); $this->io->write(''.$this->url.' could not be fully loaded, package information was loaded from the local cache and may be out of date'); diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index f6ea19ab7..0ffac9f1d 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -57,7 +57,7 @@ class FilesystemRepository extends WritableArrayRepository throw new InvalidRepositoryException('Invalid repository data in '.$this->file->getPath().', packages could not be loaded: ['.get_class($e).'] '.$e->getMessage()); } - $loader = new ArrayLoader(); + $loader = new ArrayLoader(null, true); foreach ($packages as $packageData) { $package = $loader->load($packageData); $this->addPackage($package); diff --git a/src/Composer/Repository/Pear/PackageDependencyParser.php b/src/Composer/Repository/Pear/PackageDependencyParser.php index 515993d03..a124cd07a 100644 --- a/src/Composer/Repository/Pear/PackageDependencyParser.php +++ b/src/Composer/Repository/Pear/PackageDependencyParser.php @@ -51,7 +51,7 @@ class PackageDependencyParser */ private function buildDependency10Info($depArray) { - static $dep10toOperatorMap = array('has'=>'==', 'eq' => '==', 'ge' => '>=', 'gt' => '>', 'le' => '<=', 'lt' => '<', 'not' => '!='); + static $dep10toOperatorMap = array('has' => '==', 'eq' => '==', 'ge' => '>=', 'gt' => '>', 'le' => '<=', 'lt' => '<', 'not' => '!='); $result = array(); @@ -255,7 +255,7 @@ class PackageDependencyParser */ private function parse20VersionConstraint(array $data) { - static $dep20toOperatorMap = array('has'=>'==', 'min' => '>=', 'max' => '<=', 'exclude' => '!='); + static $dep20toOperatorMap = array('has' => '==', 'min' => '>=', 'max' => '<=', 'exclude' => '!='); $versions = array(); $values = array_intersect_key($data, $dep20toOperatorMap); diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php index a106385a5..6086df1ed 100644 --- a/src/Composer/Repository/PearRepository.php +++ b/src/Composer/Repository/PearRepository.php @@ -57,7 +57,7 @@ class PearRepository extends ArrayRepository $this->url = rtrim($repoConfig['url'], '/'); $this->io = $io; - $this->rfs = $rfs ?: new RemoteFilesystem($this->io); + $this->rfs = $rfs ?: new RemoteFilesystem($this->io, $config); $this->vendorAlias = isset($repoConfig['vendor-alias']) ? $repoConfig['vendor-alias'] : null; $this->versionParser = new VersionParser(); } @@ -160,6 +160,7 @@ class PearRepository extends ArrayRepository $package = new CompletePackage($composerPackageName, $normalizedVersion, $version); $package->setType('pear-library'); $package->setDescription($packageDefinition->getDescription()); + $package->setLicense(array($packageDefinition->getLicense())); $package->setDistType('file'); $package->setDistUrl($distUrl); $package->setAutoload(array('classmap' => array(''))); diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 2b462fca9..636cba16b 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -21,7 +21,7 @@ use Composer\Plugin\PluginInterface; */ class PlatformRepository extends ArrayRepository { - const PLATFORM_PACKAGE_REGEX = '{^(?:php(?:-64bit)?|(?:ext|lib)-[^/]+)$}i'; + const PLATFORM_PACKAGE_REGEX = '{^(?:php(?:-64bit)?|hhvm|(?:ext|lib)-[^/]+)$}i'; protected function initialize() { @@ -146,12 +146,12 @@ class PlatformRepository extends ArrayRepository parent::addPackage($lib); } - if (defined('HPHP_VERSION')) { + if (defined('HHVM_VERSION')) { try { - $prettyVersion = HPHP_VERSION; + $prettyVersion = HHVM_VERSION; $version = $versionParser->normalize($prettyVersion); } catch (\UnexpectedValueException $e) { - $prettyVersion = preg_replace('#^([^~+-]+).*$#', '$1', HPHP_VERSION); + $prettyVersion = preg_replace('#^([^~+-]+).*$#', '$1', HHVM_VERSION); $version = $versionParser->normalize($prettyVersion); } @@ -161,7 +161,6 @@ class PlatformRepository extends ArrayRepository } } - private function buildPackageName($name) { return 'ext-' . str_replace(' ', '-', $name); diff --git a/src/Composer/Repository/RepositoryManager.php b/src/Composer/Repository/RepositoryManager.php index 1a9054f14..83352cf2f 100644 --- a/src/Composer/Repository/RepositoryManager.php +++ b/src/Composer/Repository/RepositoryManager.php @@ -89,7 +89,7 @@ class RepositoryManager * Returns a new repository for a specific installation type. * * @param string $type repository type - * @param string $config repository configuration + * @param array $config repository configuration * @return RepositoryInterface * @throws \InvalidArgumentException if repository for provided type is not registered */ diff --git a/src/Composer/Repository/StreamableRepositoryInterface.php b/src/Composer/Repository/StreamableRepositoryInterface.php deleted file mode 100644 index f5c694642..000000000 --- a/src/Composer/Repository/StreamableRepositoryInterface.php +++ /dev/null @@ -1,61 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Repository; - -use Composer\Package\AliasPackage; -use Composer\Package\PackageInterface; - -/** - * @author Jordi Boggiano - */ -interface StreamableRepositoryInterface extends RepositoryInterface -{ - /** - * Return partial package data without loading them all to save on memory - * - * The function must return an array of package arrays. - * - * The package array must contain the following fields: - * - name: package name (normalized/lowercased) - * - repo: reference to the repository instance - * - version: normalized version - * - replace: array of package name => version constraint, optional - * - provide: array of package name => version constraint, optional - * - alias: pretty alias that this package should be aliased to, optional - * - alias_normalized: normalized alias that this package should be aliased to, optional - * - * Any additional information can be returned and will be sent back - * into loadPackage/loadAliasPackage for completing the package loading - * when it's needed. - * - * @return array - */ - public function getMinimalPackages(); - - /** - * Loads a package from minimal info of the package - * - * @param array $data the minimal info as was returned by getMinimalPackage - * @return PackageInterface - */ - public function loadPackage(array $data); - - /** - * Loads an alias package from minimal info of the package - * - * @param array $data the minimal info as was returned by getMinimalPackage - * @param PackageInterface $aliasOf the package which this alias is an alias of - * @return AliasPackage - */ - public function loadAliasPackage(array $data, PackageInterface $aliasOf); -} diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php index 12eba6d3a..68389dc33 100644 --- a/src/Composer/Repository/Vcs/GitBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php @@ -33,7 +33,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function initialize() { - preg_match('#^https://bitbucket\.org/([^/]+)/(.+?)\.git$#', $this->url, $match); + preg_match('#^https?://bitbucket\.org/([^/]+)/(.+?)\.git$#', $this->url, $match); $this->owner = $match[1]; $this->repository = $match[2]; $this->originUrl = 'bitbucket.org'; @@ -93,7 +93,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface $composer = JsonFile::parseJson($composer, $resource); - if (!isset($composer['time'])) { + if (empty($composer['time'])) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/changesets/'.$identifier; $changeset = JsonFile::parseJson($this->getContents($resource), $resource); $composer['time'] = $changeset['timestamp']; @@ -143,7 +143,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public static function supports(IOInterface $io, Config $config, $url, $deep = false) { - if (!preg_match('#^https://bitbucket\.org/([^/]+)/(.+?)\.git$#', $url)) { + if (!preg_match('#^https?://bitbucket\.org/([^/]+)/(.+?)\.git$#', $url)) { return false; } diff --git a/src/Composer/Repository/Vcs/GitDriver.php b/src/Composer/Repository/Vcs/GitDriver.php index ecfad77db..298639bc0 100644 --- a/src/Composer/Repository/Vcs/GitDriver.php +++ b/src/Composer/Repository/Vcs/GitDriver.php @@ -37,13 +37,13 @@ class GitDriver extends VcsDriver */ public function initialize() { - if (static::isLocalUrl($this->url)) { - $this->repoDir = str_replace('file://', '', $this->url); + if (Filesystem::isLocalPath($this->url)) { + $this->repoDir = $this->url; + $cacheUrl = realpath($this->url); } else { $this->repoDir = $this->config->get('cache-vcs-dir') . '/' . preg_replace('{[^a-z0-9.]}i', '-', $this->url) . '/'; - $util = new GitUtil; - $util->cleanEnv(); + GitUtil::cleanEnv(); $fs = new Filesystem(); $fs->ensureDirectoryExists(dirname($this->repoDir)); @@ -56,32 +56,37 @@ class GitDriver extends VcsDriver throw new \InvalidArgumentException('The source URL '.$this->url.' is invalid, ssh URLs should have a port number after ":".'."\n".'Use ssh://git@example.com:22/path or just git@example.com:path if you do not want to provide a password or custom port.'); } + $gitUtil = new GitUtil($this->io, $this->config, $this->process, $fs); + // update the repo if it is a valid git repository - if (is_dir($this->repoDir) && 0 === $this->process->execute('git remote', $output, $this->repoDir)) { - if (0 !== $this->process->execute('git remote update --prune origin', $output, $this->repoDir)) { - $this->io->write('Failed to update '.$this->url.', package information from this repository may be outdated ('.$this->process->getErrorOutput().')'); + if (is_dir($this->repoDir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $this->repoDir) && trim($output) === '.') { + try { + $commandCallable = function ($url) { + return sprintf('git remote set-url origin %s && git remote update --prune origin', ProcessExecutor::escape($url)); + }; + $gitUtil->runCommand($commandCallable, $this->url, $this->repoDir); + } catch (\Exception $e) { + $this->io->write('Failed to update '.$this->url.', package information from this repository may be outdated ('.$e->getMessage().')'); } } else { // clean up directory and do a fresh clone into it $fs->removeDirectory($this->repoDir); - $command = sprintf('git clone --mirror %s %s', escapeshellarg($this->url), escapeshellarg($this->repoDir)); - if (0 !== $this->process->execute($command, $output)) { - $output = $this->process->getErrorOutput(); - - if (0 !== $this->process->execute('git --version', $ignoredOutput)) { - throw new \RuntimeException('Failed to clone '.$this->url.', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput()); - } + $repoDir = $this->repoDir; + $commandCallable = function ($url) use ($repoDir) { + return sprintf('git clone --mirror %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape($repoDir)); + }; - throw new \RuntimeException('Failed to clone '.$this->url.', could not read packages from it' . "\n\n" .$output); - } + $gitUtil->runCommand($commandCallable, $this->url, $this->repoDir, true); } + + $cacheUrl = $this->url; } $this->getTags(); $this->getBranches(); - $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url)); + $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $cacheUrl)); } /** @@ -142,7 +147,7 @@ class GitDriver extends VcsDriver } if (!isset($this->infoCache[$identifier])) { - $resource = sprintf('%s:composer.json', escapeshellarg($identifier)); + $resource = sprintf('%s:composer.json', ProcessExecutor::escape($identifier)); $this->process->execute(sprintf('git show %s', $resource), $composer, $this->repoDir); if (!trim($composer)) { @@ -151,8 +156,8 @@ class GitDriver extends VcsDriver $composer = JsonFile::parseJson($composer, $resource); - if (!isset($composer['time'])) { - $this->process->execute(sprintf('git log -1 --format=%%at %s', escapeshellarg($identifier)), $output, $this->repoDir); + if (empty($composer['time'])) { + $this->process->execute(sprintf('git log -1 --format=%%at %s', ProcessExecutor::escape($identifier)), $output, $this->repoDir); $date = new \DateTime('@'.trim($output), new \DateTimeZone('UTC')); $composer['time'] = $date->format('Y-m-d H:i:s'); } @@ -197,8 +202,8 @@ class GitDriver extends VcsDriver $this->process->execute('git branch --no-color --no-abbrev -v', $output, $this->repoDir); foreach ($this->process->splitLines($output) as $branch) { if ($branch && !preg_match('{^ *[^/]+/HEAD }', $branch)) { - if (preg_match('{^(?:\* )? *(\S+) *([a-f0-9]+) .*$}', $branch, $match)) { - $branches[$match[1]] = $match[2]; + if (preg_match('{^(?:\* )? *(\S+) *([a-f0-9]+)(?: .*)?$}', $branch, $match)) { + $branches[$match[1]] = $match[2]; } } } @@ -219,13 +224,13 @@ class GitDriver extends VcsDriver } // local filesystem - if (static::isLocalUrl($url)) { + if (Filesystem::isLocalPath($url)) { + $url = Filesystem::getPlatformPath($url); if (!is_dir($url)) { - throw new \RuntimeException('Directory does not exist: '.$url); + return false; } - $process = new ProcessExecutor(); - $url = str_replace('file://', '', $url); + $process = new ProcessExecutor($io); // check whether there is a git repo in that path if ($process->execute('git tag', $output, $url) === 0) { return true; @@ -236,7 +241,11 @@ class GitDriver extends VcsDriver return false; } - // TODO try to connect to the server + $process = new ProcessExecutor($io); + if($process->execute('git ls-remote --heads ' . ProcessExecutor::escape($url), $output) === 0) { + return true; + } + return false; } } diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 3cf2befb5..9dbd21059 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -46,15 +46,26 @@ class GitHubDriver extends VcsDriver */ public function initialize() { - preg_match('#^(?:(?:https?|git)://([^/]+)/|git@([^:]+):)([^/]+)/(.+?)(?:\.git)?$#', $this->url, $match); + preg_match('#^(?:(?:https?|git)://([^/]+)/|git@([^:]+):)([^/]+)/(.+?)(?:\.git|/)?$#', $this->url, $match); $this->owner = $match[3]; $this->repository = $match[4]; $this->originUrl = !empty($match[1]) ? $match[1] : $match[2]; $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->owner.'/'.$this->repository); + if (isset($this->repoConfig['no-api']) && $this->repoConfig['no-api']) { + $this->setupGitDriver($this->url); + + return; + } + $this->fetchRootIdentifier(); } + public function getRepositoryUrl() + { + return 'https://'.$this->originUrl.'/'.$this->owner.'/'.$this->repository; + } + /** * {@inheritDoc} */ @@ -117,10 +128,6 @@ class GitHubDriver extends VcsDriver */ public function getDist($identifier) { - if ($this->gitDriver) { - return $this->gitDriver->getDist($identifier); - } - $url = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/zipball/'.$identifier; return array('type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => ''); @@ -164,7 +171,7 @@ class GitHubDriver extends VcsDriver if ($composer) { $composer = JsonFile::parseJson($composer, $resource); - if (!isset($composer['time'])) { + if (empty($composer['time'])) { $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier); $commit = JsonFile::parseJson($this->getContents($resource), $resource); $composer['time'] = $commit['commit']['committer']['date']; @@ -197,12 +204,17 @@ class GitHubDriver extends VcsDriver return $this->gitDriver->getTags(); } if (null === $this->tags) { - $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/tags'; - $tagsData = JsonFile::parseJson($this->getContents($resource), $resource); $this->tags = array(); - foreach ($tagsData as $tag) { - $this->tags[$tag['name']] = $tag['commit']['sha']; - } + $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/tags?per_page=100'; + + do { + $tagsData = JsonFile::parseJson($this->getContents($resource), $resource); + foreach ($tagsData as $tag) { + $this->tags[$tag['name']] = $tag['commit']['sha']; + } + + $resource = $this->getNextPage(); + } while ($resource); } return $this->tags; @@ -217,13 +229,22 @@ class GitHubDriver extends VcsDriver return $this->gitDriver->getBranches(); } if (null === $this->branches) { - $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/git/refs/heads'; - $branchData = JsonFile::parseJson($this->getContents($resource), $resource); $this->branches = array(); - foreach ($branchData as $branch) { - $name = substr($branch['ref'], 11); - $this->branches[$name] = $branch['object']['sha']; - } + $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/git/refs/heads?per_page=100'; + + $branchBlacklist = array('gh-pages'); + + do { + $branchData = JsonFile::parseJson($this->getContents($resource), $resource); + foreach ($branchData as $branch) { + $name = substr($branch['ref'], 11); + if (!in_array($name, $branchBlacklist)) { + $this->branches[$name] = $branch['object']['sha']; + } + } + + $resource = $this->getNextPage(); + } while ($resource); } return $this->branches; @@ -234,7 +255,7 @@ class GitHubDriver extends VcsDriver */ public static function supports(IOInterface $io, Config $config, $url, $deep = false) { - if (!preg_match('#^((?:https?|git)://([^/]+)/|git@([^:]+):)([^/]+)/(.+?)(?:\.git)?$#', $url, $matches)) { + if (!preg_match('#^((?:https?|git)://([^/]+)/|git@([^:]+):)([^/]+)/(.+?)(?:\.git|/)?$#', $url, $matches)) { return false; } @@ -385,6 +406,9 @@ class GitHubDriver extends VcsDriver return; } + $this->owner = $repoData['owner']['login']; + $this->repository = $repoData['name']; + $this->isPrivate = !empty($repoData['private']); if (isset($repoData['default_branch'])) { $this->rootIdentifier = $repoData['default_branch']; @@ -405,14 +429,7 @@ class GitHubDriver extends VcsDriver // GitHub returns 404 for private repositories) and we // cannot ask for authentication credentials (because we // are not interactive) then we fallback to GitDriver. - $this->gitDriver = new GitDriver( - array('url' => $this->generateSshUrl()), - $this->io, - $this->config, - $this->process, - $this->remoteFilesystem - ); - $this->gitDriver->initialize(); + $this->setupGitDriver($this->generateSshUrl()); return; } catch (\RuntimeException $e) { @@ -422,4 +439,31 @@ class GitHubDriver extends VcsDriver throw $e; } } + + protected function setupGitDriver($url) + { + $this->gitDriver = new GitDriver( + array('url' => $url), + $this->io, + $this->config, + $this->process, + $this->remoteFilesystem + ); + $this->gitDriver->initialize(); + } + + protected function getNextPage() + { + $headers = $this->remoteFilesystem->getLastHeaders(); + foreach ($headers as $header) { + if (substr($header, 0, 5) === 'Link:') { + $links = explode(',', substr($header, 5)); + foreach ($links as $link) { + if (preg_match('{<(.+?)>; *rel="next"}', $link, $match)) { + return $match[1]; + } + } + } + } + } } diff --git a/src/Composer/Repository/Vcs/HgBitbucketDriver.php b/src/Composer/Repository/Vcs/HgBitbucketDriver.php index c6eac73b9..cc2b386eb 100644 --- a/src/Composer/Repository/Vcs/HgBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/HgBitbucketDriver.php @@ -33,7 +33,7 @@ class HgBitbucketDriver extends VcsDriver */ public function initialize() { - preg_match('#^https://bitbucket\.org/([^/]+)/([^/]+)/?$#', $this->url, $match); + preg_match('#^https?://bitbucket\.org/([^/]+)/([^/]+)/?$#', $this->url, $match); $this->owner = $match[1]; $this->repository = $match[2]; $this->originUrl = 'bitbucket.org'; @@ -102,7 +102,7 @@ class HgBitbucketDriver extends VcsDriver $composer = JsonFile::parseJson($repoData['data'], $resource); - if (!isset($composer['time'])) { + if (empty($composer['time'])) { $resource = $this->getScheme() . '://bitbucket.org/api/1.0/repositories/'.$this->owner.'/'.$this->repository.'/changesets/'.$identifier; $changeset = JsonFile::parseJson($this->getContents($resource), $resource); $composer['time'] = $changeset['timestamp']; @@ -153,7 +153,7 @@ class HgBitbucketDriver extends VcsDriver */ public static function supports(IOInterface $io, Config $config, $url, $deep = false) { - if (!preg_match('#^https://bitbucket\.org/([^/]+)/([^/]+)/?$#', $url)) { + if (!preg_match('#^https?://bitbucket\.org/([^/]+)/([^/]+)/?$#', $url)) { return false; } diff --git a/src/Composer/Repository/Vcs/HgDriver.php b/src/Composer/Repository/Vcs/HgDriver.php index 060a77e45..ed8e927b9 100644 --- a/src/Composer/Repository/Vcs/HgDriver.php +++ b/src/Composer/Repository/Vcs/HgDriver.php @@ -34,8 +34,8 @@ class HgDriver extends VcsDriver */ public function initialize() { - if (static::isLocalUrl($this->url)) { - $this->repoDir = str_replace('file://', '', $this->url); + if (Filesystem::isLocalPath($this->url)) { + $this->repoDir = $this->url; } else { $cacheDir = $this->config->get('cache-vcs-dir'); $this->repoDir = $cacheDir . '/' . preg_replace('{[^a-z0-9]}i', '-', $this->url) . '/'; @@ -56,7 +56,7 @@ class HgDriver extends VcsDriver // clean up directory and do a fresh clone into it $fs->removeDirectory($this->repoDir); - if (0 !== $this->process->execute(sprintf('hg clone --noupdate %s %s', escapeshellarg($this->url), escapeshellarg($this->repoDir)), $output, $cacheDir)) { + if (0 !== $this->process->execute(sprintf('hg clone --noupdate %s %s', ProcessExecutor::escape($this->url), ProcessExecutor::escape($this->repoDir)), $output, $cacheDir)) { $output = $this->process->getErrorOutput(); if (0 !== $this->process->execute('hg --version', $ignoredOutput)) { @@ -116,7 +116,7 @@ class HgDriver extends VcsDriver public function getComposerInformation($identifier) { if (!isset($this->infoCache[$identifier])) { - $this->process->execute(sprintf('hg cat -r %s composer.json', escapeshellarg($identifier)), $composer, $this->repoDir); + $this->process->execute(sprintf('hg cat -r %s composer.json', ProcessExecutor::escape($identifier)), $composer, $this->repoDir); if (!trim($composer)) { return; @@ -124,8 +124,8 @@ class HgDriver extends VcsDriver $composer = JsonFile::parseJson($composer, $identifier); - if (!isset($composer['time'])) { - $this->process->execute(sprintf('hg log --template "{date|rfc3339date}" -r %s', escapeshellarg($identifier)), $output, $this->repoDir); + if (empty($composer['time'])) { + $this->process->execute(sprintf('hg log --template "{date|rfc3339date}" -r %s', ProcessExecutor::escape($identifier)), $output, $this->repoDir); $date = new \DateTime(trim($output), new \DateTimeZone('UTC')); $composer['time'] = $date->format('Y-m-d H:i:s'); } @@ -197,13 +197,13 @@ class HgDriver extends VcsDriver } // local filesystem - if (static::isLocalUrl($url)) { + if (Filesystem::isLocalPath($url)) { + $url = Filesystem::getPlatformPath($url); if (!is_dir($url)) { - throw new \RuntimeException('Directory does not exist: '.$url); + return false; } $process = new ProcessExecutor(); - $url = str_replace('file://', '', $url); // check whether there is a hg repo in that path if ($process->execute('hg summary', $output, $url) === 0) { return true; @@ -215,7 +215,7 @@ class HgDriver extends VcsDriver } $processExecutor = new ProcessExecutor(); - $exit = $processExecutor->execute(sprintf('hg identify %s', escapeshellarg($url)), $ignored); + $exit = $processExecutor->execute(sprintf('hg identify %s', ProcessExecutor::escape($url)), $ignored); return $exit === 0; } diff --git a/src/Composer/Repository/Vcs/PerforceDriver.php b/src/Composer/Repository/Vcs/PerforceDriver.php index 79500f1d6..c7c225b27 100644 --- a/src/Composer/Repository/Vcs/PerforceDriver.php +++ b/src/Composer/Repository/Vcs/PerforceDriver.php @@ -35,7 +35,7 @@ class PerforceDriver extends VcsDriver { $this->depot = $this->repoConfig['depot']; $this->branch = ''; - if (isset($this->repoConfig['branch'])) { + if (!empty($this->repoConfig['branch'])) { $this->branch = $this->repoConfig['branch']; } @@ -51,12 +51,12 @@ class PerforceDriver extends VcsDriver private function initPerforce($repoConfig) { - if (isset($this->perforce)) { + if (!empty($this->perforce)) { return; } $repoDir = $this->config->get('cache-vcs-dir') . '/' . $this->depot; - $this->perforce = Perforce::create($repoConfig, $this->getUrl(), $repoDir, $this->process); + $this->perforce = Perforce::create($repoConfig, $this->getUrl(), $repoDir, $this->process, $this->io); } /** @@ -64,7 +64,7 @@ class PerforceDriver extends VcsDriver */ public function getComposerInformation($identifier) { - if (isset($this->composerInfoIdentifier)) { + if (!empty($this->composerInfoIdentifier)) { if (strcmp($identifier, $this->composerInfoIdentifier) === 0) { return $this->composerInfo; } @@ -140,12 +140,8 @@ class PerforceDriver extends VcsDriver { $this->composerInfo = $this->perforce->getComposerInformation('//' . $this->depot . '/' . $identifier); $this->composerInfoIdentifier = $identifier; - $result = false; - if (isset($this->composerInfo)) { - $result = count($this->composerInfo) > 0; - } - return $result; + return !empty($this->composerInfo); } /** @@ -186,9 +182,4 @@ class PerforceDriver extends VcsDriver { return $this->branch; } - - public function setPerforce(Perforce $perforce) - { - $this->perforce = $perforce; - } } diff --git a/src/Composer/Repository/Vcs/SvnDriver.php b/src/Composer/Repository/Vcs/SvnDriver.php index d69230cce..c602ddaa3 100644 --- a/src/Composer/Repository/Vcs/SvnDriver.php +++ b/src/Composer/Repository/Vcs/SvnDriver.php @@ -27,6 +27,9 @@ use Composer\Downloader\TransportException; */ class SvnDriver extends VcsDriver { + /** + * @var Cache + */ protected $cache; protected $baseUrl; protected $tags; @@ -38,6 +41,7 @@ class SvnDriver extends VcsDriver protected $branchesPath = 'branches'; protected $tagsPath = 'tags'; protected $packagePath = ''; + protected $cacheCredentials = true; /** * @var \Composer\Util\Svn @@ -51,6 +55,8 @@ class SvnDriver extends VcsDriver { $this->url = $this->baseUrl = rtrim(self::normalizeUrl($this->url), '/'); + SvnUtil::cleanEnv(); + if (isset($this->repoConfig['trunk-path'])) { $this->trunkPath = $this->repoConfig['trunk-path']; } @@ -60,6 +66,9 @@ class SvnDriver extends VcsDriver if (isset($this->repoConfig['tags-path'])) { $this->tagsPath = $this->repoConfig['tags-path']; } + if (array_key_exists('svn-cache-credentials', $this->repoConfig)) { + $this->cacheCredentials = (bool) $this->repoConfig['svn-cache-credentials']; + } if (isset($this->repoConfig['package-path'])) { $this->packagePath = '/' . trim($this->repoConfig['package-path'], '/'); } @@ -139,7 +148,7 @@ class SvnDriver extends VcsDriver $composer = JsonFile::parseJson($output, $this->baseUrl . $resource . $rev); - if (!isset($composer['time'])) { + if (empty($composer['time'])) { $output = $this->execute('svn info', $this->baseUrl . $path . $rev); foreach ($this->process->splitLines($output) as $line) { if ($line && preg_match('{^Last Changed Date: ([^(]+)}', $line, $match)) { @@ -194,10 +203,10 @@ class SvnDriver extends VcsDriver if (null === $this->branches) { $this->branches = array(); - if (false === strpos($this->trunkPath, '/')) { + if (false === $this->trunkPath) { $trunkParent = $this->baseUrl . '/'; } else { - $trunkParent = $this->baseUrl . '/' . dirname($this->trunkPath) . '/'; + $trunkParent = $this->baseUrl . '/' . $this->trunkPath; } $output = $this->execute('svn ls --verbose', $trunkParent); @@ -205,12 +214,12 @@ class SvnDriver extends VcsDriver foreach ($this->process->splitLines($output) as $line) { $line = trim($line); if ($line && preg_match('{^\s*(\S+).*?(\S+)\s*$}', $line, $match)) { - if (isset($match[1]) && isset($match[2]) && $match[2] === $this->trunkPath . '/') { - $this->branches[$this->trunkPath] = $this->buildIdentifier( + if (isset($match[1]) && isset($match[2]) && $match[2] === './') { + $this->branches['trunk'] = $this->buildIdentifier( '/' . $this->trunkPath, $match[1] ); - $this->rootIdentifier = $this->branches[$this->trunkPath]; + $this->rootIdentifier = $this->branches['trunk']; break; } } @@ -250,7 +259,7 @@ class SvnDriver extends VcsDriver } // proceed with deep check for local urls since they are fast to process - if (!$deep && !static::isLocalUrl($url)) { + if (!$deep && !Filesystem::isLocalPath($url)) { return false; } @@ -304,7 +313,8 @@ class SvnDriver extends VcsDriver protected function execute($command, $url) { if (null === $this->util) { - $this->util = new SvnUtil($this->baseUrl, $this->io, $this->process); + $this->util = new SvnUtil($this->baseUrl, $this->io, $this->config, $this->process); + $this->util->setCacheCredentials($this->cacheCredentials); } try { diff --git a/src/Composer/Repository/Vcs/VcsDriver.php b/src/Composer/Repository/Vcs/VcsDriver.php index f1112074e..b03f55684 100644 --- a/src/Composer/Repository/Vcs/VcsDriver.php +++ b/src/Composer/Repository/Vcs/VcsDriver.php @@ -17,6 +17,7 @@ use Composer\Config; use Composer\IO\IOInterface; use Composer\Util\ProcessExecutor; use Composer\Util\RemoteFilesystem; +use Composer\Util\Filesystem; /** * A driver implementation for driver with authentication interaction. @@ -44,11 +45,8 @@ abstract class VcsDriver implements VcsDriverInterface */ final public function __construct(array $repoConfig, IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null) { - - if (self::isLocalUrl($repoConfig['url'])) { - $repoConfig['url'] = realpath( - preg_replace('/^file:\/\//', '', $repoConfig['url']) - ); + if (Filesystem::isLocalPath($repoConfig['url'])) { + $repoConfig['url'] = Filesystem::getPlatformPath($repoConfig['url']); } $this->url = $repoConfig['url']; @@ -57,7 +55,7 @@ abstract class VcsDriver implements VcsDriverInterface $this->io = $io; $this->config = $config; $this->process = $process ?: new ProcessExecutor($io); - $this->remoteFilesystem = $remoteFilesystem ?: new RemoteFilesystem($io); + $this->remoteFilesystem = $remoteFilesystem ?: new RemoteFilesystem($io, $config); } /** @@ -101,17 +99,6 @@ abstract class VcsDriver implements VcsDriverInterface return $this->remoteFilesystem->getContents($this->originUrl, $url, false); } - /** - * Return if current repository url is local - * - * @param string $url - * @return boolean Repository url is local - */ - protected static function isLocalUrl($url) - { - return (bool) preg_match('{^(file://|/|[a-z]:[\\\\/])}i', $url); - } - /** * {@inheritDoc} */ diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index 62e7acef5..6c9dc5d4b 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -265,7 +265,7 @@ class VcsRepository extends ArrayRepository } } - private function preProcess(VcsDriverInterface $driver, array $data, $identifier) + protected function preProcess(VcsDriverInterface $driver, array $data, $identifier) { // keep the name of the main identifier for all packages $data['name'] = $this->packageName ?: $data['name']; diff --git a/src/Composer/Script/Event.php b/src/Composer/Script/Event.php index 40b109b2d..bde7e3b6f 100644 --- a/src/Composer/Script/Event.php +++ b/src/Composer/Script/Event.php @@ -14,6 +14,7 @@ namespace Composer\Script; use Composer\Composer; use Composer\IO\IOInterface; +use Composer\EventDispatcher\Event as BaseEvent; /** * The script event class @@ -21,7 +22,7 @@ use Composer\IO\IOInterface; * @author François Pluchino * @author Nils Adermann */ -class Event extends \Composer\EventDispatcher\Event +class Event extends BaseEvent { /** * @var Composer The composer instance @@ -45,10 +46,12 @@ class Event extends \Composer\EventDispatcher\Event * @param Composer $composer The composer object * @param IOInterface $io The IOInterface object * @param boolean $devMode Whether or not we are in dev mode + * @param array $args Arguments passed by the user + * @param array $flags Optional flags to pass data not as argument */ - public function __construct($name, Composer $composer, IOInterface $io, $devMode = false) + public function __construct($name, Composer $composer, IOInterface $io, $devMode = false, array $args = array(), array $flags = array()) { - parent::__construct($name); + parent::__construct($name, $args, $flags); $this->composer = $composer; $this->io = $io; $this->devMode = $devMode; diff --git a/src/Composer/Script/ScriptEvents.php b/src/Composer/Script/ScriptEvents.php index 5f3eaafd8..616b2b97e 100644 --- a/src/Composer/Script/ScriptEvents.php +++ b/src/Composer/Script/ScriptEvents.php @@ -165,4 +165,21 @@ class ScriptEvents */ const POST_CREATE_PROJECT_CMD = 'post-create-project-cmd'; + /** + * The PRE_ARCHIVE_CMD event occurs before the update command is executed. + * + * The event listener method receives a Composer\Script\CommandEvent instance. + * + * @var string + */ + const PRE_ARCHIVE_CMD = 'pre-archive-cmd'; + + /** + * The POST_ARCHIVE_CMD event occurs after the status command is executed. + * + * The event listener method receives a Composer\Script\CommandEvent instance. + * + * @var string + */ + const POST_ARCHIVE_CMD = 'post-archive-cmd'; } diff --git a/src/Composer/Util/AuthHelper.php b/src/Composer/Util/AuthHelper.php new file mode 100644 index 000000000..4accba716 --- /dev/null +++ b/src/Composer/Util/AuthHelper.php @@ -0,0 +1,63 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +use Composer\Config; +use Composer\IO\IOInterface; + +/** + * @author Jordi Boggiano + */ +class AuthHelper +{ + protected $io; + protected $config; + + public function __construct(IOInterface $io, Config $config) + { + $this->io = $io; + $this->config = $config; + } + + public function storeAuth($originUrl, $storeAuth) + { + $store = false; + $configSource = $this->config->getAuthConfigSource(); + if ($storeAuth === true) { + $store = $configSource; + } elseif ($storeAuth === 'prompt') { + $answer = $this->io->askAndValidate( + 'Do you want to store credentials for '.$originUrl.' in '.$configSource->getName().' ? [Yn] ', + function ($value) { + $input = strtolower(substr(trim($value), 0, 1)); + if (in_array($input, array('y','n'))) { + return $input; + } + throw new \RuntimeException('Please answer (y)es or (n)o'); + }, + false, + 'y' + ); + + if ($answer === 'y') { + $store = $configSource; + } + } + if ($store) { + $store->addConfigSetting( + 'http-basic.'.$originUrl, + $this->io->getAuthentication($originUrl) + ); + } + } +} diff --git a/src/Composer/Util/ComposerMirror.php b/src/Composer/Util/ComposerMirror.php new file mode 100644 index 000000000..036444d20 --- /dev/null +++ b/src/Composer/Util/ComposerMirror.php @@ -0,0 +1,57 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +/** + * Composer mirror utilities + * + * @author Jordi Boggiano + */ +class ComposerMirror +{ + public static function processUrl($mirrorUrl, $packageName, $version, $reference, $type) + { + if ($reference) { + $reference = preg_match('{^([a-f0-9]*|%reference%)$}', $reference) ? $reference : md5($reference); + } + $version = strpos($version, '/') === false ? $version : md5($version); + + return str_replace( + array('%package%', '%version%', '%reference%', '%type%'), + array($packageName, $version, $reference, $type), + $mirrorUrl + ); + } + + public static function processGitUrl($mirrorUrl, $packageName, $url, $type) + { + if (preg_match('#^(?:(?:https?|git)://github\.com/|git@github\.com:)([^/]+)/(.+?)(?:\.git)?$#', $url, $match)) { + $url = 'gh-'.$match[1].'/'.$match[2]; + } elseif (preg_match('#^https://bitbucket\.org/([^/]+)/(.+?)(?:\.git)?/?$#', $url, $match)) { + $url = 'bb-'.$match[1].'/'.$match[2]; + } else { + $url = preg_replace('{[^a-z0-9_.-]}i', '-', trim($url, '/')); + } + + return str_replace( + array('%package%', '%normalizedUrl%', '%type%'), + array($packageName, $url, $type), + $mirrorUrl + ); + } + + public static function processHgUrl($mirrorUrl, $packageName, $url, $type) + { + return self::processGitUrl($mirrorUrl, $packageName, $url, $type); + } +} diff --git a/src/Composer/Util/ConfigValidator.php b/src/Composer/Util/ConfigValidator.php index 5bd915388..71b0ac418 100644 --- a/src/Composer/Util/ConfigValidator.php +++ b/src/Composer/Util/ConfigValidator.php @@ -37,11 +37,12 @@ class ConfigValidator /** * Validates the config, and returns the result. * - * @param string $file The path to the file + * @param string $file The path to the file + * @param integer $arrayLoaderValidationFlags Flags for ArrayLoader validation * * @return array a triple containing the errors, publishable errors, and warnings */ - public function validate($file) + public function validate($file, $arrayLoaderValidationFlags = ValidatingArrayLoader::CHECK_ALL) { $errors = array(); $publishErrors = array(); @@ -93,6 +94,10 @@ class ConfigValidator $warnings[] = 'No license specified, it is recommended to do so. For closed-source software you may use "proprietary" as license.'; } + if (isset($manifest['version'])) { + $warnings[] = 'The version field is present, it is recommended to leave it out if the package is published on Packagist.'; + } + if (!empty($manifest['name']) && preg_match('{[A-Z]}', $manifest['name'])) { $suggestName = preg_replace('{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}', '\\1\\3-\\2\\4', $manifest['name']); $suggestName = strtolower($suggestName); @@ -119,7 +124,7 @@ class ConfigValidator } try { - $loader = new ValidatingArrayLoader(new ArrayLoader()); + $loader = new ValidatingArrayLoader(new ArrayLoader(), true, null, $arrayLoaderValidationFlags); if (!isset($manifest['version'])) { $manifest['version'] = '1.0.0'; } diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 8771776fc..ca395736f 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -14,6 +14,7 @@ namespace Composer\Util; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; +use Symfony\Component\Finder\Finder; /** * @author Jordi Boggiano @@ -35,7 +36,7 @@ class Filesystem } if (file_exists($file)) { - return unlink($file); + return $this->unlink($file); } return false; @@ -49,9 +50,36 @@ class Filesystem */ public function isDirEmpty($dir) { - $dir = rtrim($dir, '/\\'); + $finder = Finder::create() + ->ignoreVCS(false) + ->ignoreDotFiles(false) + ->depth(0) + ->in($dir); - return count(glob($dir.'/*') ?: array()) === 0 && count(glob($dir.'/.*') ?: array()) === 2; + return count($finder) === 0; + } + + public function emptyDirectory($dir, $ensureDirectoryExists = true) + { + if (file_exists($dir) && is_link($dir)) { + $this->unlink($dir); + } + + if ($ensureDirectoryExists) { + $this->ensureDirectoryExists($dir); + } + + if (is_dir($dir)) { + $finder = Finder::create() + ->ignoreVCS(false) + ->ignoreDotFiles(false) + ->depth(0) + ->in($dir); + + foreach ($finder as $path) { + $this->remove((string) $path); + } + } } /** @@ -62,10 +90,16 @@ class Filesystem * * @param string $directory * @return bool + * + * @throws \RuntimeException */ public function removeDirectory($directory) { - if (!is_dir($directory)) { + if ($this->isSymlinkedDirectory($directory)) { + return $this->unlinkSymlinkedDirectory($directory); + } + + if (!file_exists($directory) || !is_dir($directory)) { return true; } @@ -78,9 +112,9 @@ class Filesystem } if (defined('PHP_WINDOWS_VERSION_BUILD')) { - $cmd = sprintf('rmdir /S /Q %s', escapeshellarg(realpath($directory))); + $cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory))); } else { - $cmd = sprintf('rm -rf %s', escapeshellarg($directory)); + $cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory)); } $result = $this->getProcess()->execute($cmd, $output) === 0; @@ -88,7 +122,11 @@ class Filesystem // clear stat cache because external processes aren't tracked by the php stat cache clearstatcache(); - return $result && !is_dir($directory); + if ($result && !file_exists($directory)) { + return true; + } + + return $this->removeDirectoryPhp($directory); } /** @@ -108,13 +146,13 @@ class Filesystem foreach ($ri as $file) { if ($file->isDir()) { - rmdir($file->getPathname()); + $this->rmdir($file->getPathname()); } else { - unlink($file->getPathname()); + $this->unlink($file->getPathname()); } } - return rmdir($directory); + return $this->rmdir($directory); } public function ensureDirectoryExists($directory) @@ -133,6 +171,58 @@ class Filesystem } } + /** + * Attempts to unlink a file and in case of failure retries after 350ms on windows + * + * @param string $path + * @return bool + * + * @throws \RuntimeException + */ + public function unlink($path) + { + if (!@$this->unlinkImplementation($path)) { + // retry after a bit on windows since it tends to be touchy with mass removals + if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(350000) && !@$this->unlinkImplementation($path))) { + $error = error_get_last(); + $message = 'Could not delete '.$path.': ' . @$error['message']; + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $message .= "\nThis can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed"; + } + + throw new \RuntimeException($message); + } + } + + return true; + } + + /** + * Attempts to rmdir a file and in case of failure retries after 350ms on windows + * + * @param string $path + * @return bool + * + * @throws \RuntimeException + */ + public function rmdir($path) + { + if (!@rmdir($path)) { + // retry after a bit on windows since it tends to be touchy with mass removals + if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(350000) && !@rmdir($path))) { + $error = error_get_last(); + $message = 'Could not delete '.$path.': ' . @$error['message']; + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $message .= "\nThis can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed"; + } + + throw new \RuntimeException($message); + } + } + + return true; + } + /** * Copy then delete is a non-atomic version of {@link rename}. * @@ -144,6 +234,13 @@ class Filesystem */ public function copyThenRemove($source, $target) { + if (!is_dir($source)) { + copy($source, $target); + $this->unlink($source); + + return; + } + $it = new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS); $ri = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::SELF_FIRST); $this->ensureDirectoryExists($target); @@ -172,7 +269,7 @@ class Filesystem if (defined('PHP_WINDOWS_VERSION_BUILD')) { // Try to copy & delete - this is a workaround for random "Access denied" errors. - $command = sprintf('xcopy %s %s /E /I /Q', escapeshellarg($source), escapeshellarg($target)); + $command = sprintf('xcopy %s %s /E /I /Q', ProcessExecutor::escape($source), ProcessExecutor::escape($target)); $result = $this->processExecutor->execute($command, $output); // clear stat cache because external processes aren't tracked by the php stat cache @@ -186,7 +283,7 @@ class Filesystem } else { // We do not use PHP's "rename" function here since it does not support // the case where $source, and $target are located on different partitions. - $command = sprintf('mv %s %s', escapeshellarg($source), escapeshellarg($target)); + $command = sprintf('mv %s %s', ProcessExecutor::escape($source), ProcessExecutor::escape($target)); $result = $this->processExecutor->execute($command, $output); // clear stat cache because external processes aren't tracked by the php stat cache @@ -353,6 +450,26 @@ class Filesystem return $prefix.($absolute ? '/' : '').implode('/', $parts); } + /** + * Return if the given path is local + * + * @param string $path + * @return bool + */ + public static function isLocalPath($path) + { + return (bool) preg_match('{^(file://|/|[a-z]:[\\\\/]|\.\.[\\\\/]|[a-z0-9_.-]+[\\\\/])}i', $path); + } + + public static function getPlatformPath($path) + { + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $path = preg_replace('{^(?:file:///([a-z])/)}i', 'file://$1:/', $path); + } + + return preg_replace('{^file://}i', '', $path); + } + protected function directorySize($directory) { $it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS); @@ -372,4 +489,67 @@ class Filesystem { return new ProcessExecutor; } + + /** + * delete symbolic link implementation (commonly known as "unlink()") + * + * symbolic links on windows which link to directories need rmdir instead of unlink + * + * @param string $path + * + * @return bool + */ + private function unlinkImplementation($path) + { + if (defined('PHP_WINDOWS_VERSION_BUILD') && is_dir($path) && is_link($path)) { + return rmdir($path); + } + + return unlink($path); + } + + private function isSymlinkedDirectory($directory) + { + if (!is_dir($directory)) { + return false; + } + + $resolved = $this->resolveSymlinkedDirectorySymlink($directory); + + return is_link($resolved); + } + + /** + * @param string $directory + * + * @return bool + */ + private function unlinkSymlinkedDirectory($directory) + { + $resolved = $this->resolveSymlinkedDirectorySymlink($directory); + + return $this->unlink($resolved); + } + + /** + * resolve pathname to symbolic link of a directory + * + * @param string $pathname directory path to resolve + * + * @return string resolved path to symbolic link or original pathname (unresolved) + */ + private function resolveSymlinkedDirectorySymlink($pathname) + { + if (!is_dir($pathname)) { + return $pathname; + } + + $resolved = rtrim($pathname, '/'); + + if (!strlen($resolved)) { + return $pathname; + } + + return $resolved; + } } diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index 4b6fcd6f2..c3c5eb02c 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -12,12 +12,146 @@ namespace Composer\Util; +use Composer\Config; +use Composer\IO\IOInterface; + /** * @author Jordi Boggiano */ class Git { - public function cleanEnv() + protected $io; + protected $config; + protected $process; + protected $filesystem; + + public function __construct(IOInterface $io, Config $config, ProcessExecutor $process, Filesystem $fs) + { + $this->io = $io; + $this->config = $config; + $this->process = $process; + $this->filesystem = $fs; + } + + public function runCommand($commandCallable, $url, $cwd, $initialClone = false) + { + if ($initialClone) { + $origCwd = $cwd; + $cwd = null; + } + + if (preg_match('{^ssh://[^@]+@[^:]+:[^0-9]+}', $url)) { + throw new \InvalidArgumentException('The source URL '.$url.' is invalid, ssh URLs should have a port number after ":".'."\n".'Use ssh://git@example.com:22/path or just git@example.com:path if you do not want to provide a password or custom port.'); + } + + if (!$initialClone) { + // capture username/password from URL if there is one + $this->process->execute('git remote -v', $output, $cwd); + if (preg_match('{^(?:composer|origin)\s+https?://(.+):(.+)@([^/]+)}im', $output, $match)) { + $this->io->setAuthentication($match[3], urldecode($match[1]), urldecode($match[2])); + } + } + + // public github, autoswitch protocols + if (preg_match('{^(?:https?|git)://'.self::getGitHubDomainsRegex($this->config).'/(.*)}', $url, $match)) { + $protocols = $this->config->get('github-protocols'); + if (!is_array($protocols)) { + throw new \RuntimeException('Config value "github-protocols" must be an array, got '.gettype($protocols)); + } + $messages = array(); + foreach ($protocols as $protocol) { + if ('ssh' === $protocol) { + $url = "git@" . $match[1] . ":" . $match[2]; + } else { + $url = $protocol ."://" . $match[1] . "/" . $match[2]; + } + + if (0 === $this->process->execute(call_user_func($commandCallable, $url), $ignoredOutput, $cwd)) { + return; + } + $messages[] = '- ' . $url . "\n" . preg_replace('#^#m', ' ', $this->process->getErrorOutput()); + if ($initialClone) { + $this->filesystem->removeDirectory($origCwd); + } + } + + // failed to checkout, first check git accessibility + $this->throwException('Failed to clone ' . self::sanitizeUrl($url) .' via '.implode(', ', $protocols).' protocols, aborting.' . "\n\n" . implode("\n", $messages), $url); + } + + $command = call_user_func($commandCallable, $url); + if (0 !== $this->process->execute($command, $ignoredOutput, $cwd)) { + // private github repository without git access, try https with auth + if (preg_match('{^git@'.self::getGitHubDomainsRegex($this->config).':(.+?)\.git$}i', $url, $match)) { + if (!$this->io->hasAuthentication($match[1])) { + $gitHubUtil = new GitHub($this->io, $this->config, $this->process); + $message = 'Cloning failed using an ssh key for authentication, enter your GitHub credentials to access private repos'; + + if (!$gitHubUtil->authorizeOAuth($match[1]) && $this->io->isInteractive()) { + $gitHubUtil->authorizeOAuthInteractively($match[1], $message); + } + } + + if ($this->io->hasAuthentication($match[1])) { + $auth = $this->io->getAuthentication($match[1]); + $url = 'https://'.rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@'.$match[1].'/'.$match[2].'.git'; + + $command = call_user_func($commandCallable, $url); + if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { + return; + } + } + } elseif ( // private non-github repo that failed to authenticate + preg_match('{(https?://)([^/]+)(.*)$}i', $url, $match) && + strpos($this->process->getErrorOutput(), 'fatal: Authentication failed') !== false + ) { + if (strpos($match[2], '@')) { + list($authParts, $match[2]) = explode('@', $match[2], 2); + } + + $storeAuth = false; + if ($this->io->hasAuthentication($match[2])) { + $auth = $this->io->getAuthentication($match[2]); + } elseif ($this->io->isInteractive()) { + $defaultUsername = null; + if (isset($authParts) && $authParts) { + if (false !== strpos($authParts, ':')) { + list($defaultUsername,) = explode(':', $authParts, 2); + } else { + $defaultUsername = $authParts; + } + } + + $this->io->write(' Authentication required ('.parse_url($url, PHP_URL_HOST).'):'); + $auth = array( + 'username' => $this->io->ask(' Username: ', $defaultUsername), + 'password' => $this->io->askAndHideAnswer(' Password: '), + ); + $storeAuth = $this->config->get('store-auths'); + } + + if ($auth) { + $url = $match[1].rawurlencode($auth['username']).':'.rawurlencode($auth['password']).'@'.$match[2].$match[3]; + + $command = call_user_func($commandCallable, $url); + if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { + $this->io->setAuthentication($match[2], $auth['username'], $auth['password']); + $authHelper = new AuthHelper($this->io, $this->config); + $authHelper->storeAuth($match[2], $storeAuth); + + return; + } + } + } + + if ($initialClone) { + $this->filesystem->removeDirectory($origCwd); + } + $this->throwException('Failed to execute ' . self::sanitizeUrl($command) . "\n\n" . $this->process->getErrorOutput(), $url); + } + } + + public static function cleanEnv() { if (ini_get('safe_mode') && false === strpos(ini_get('safe_mode_allowed_env_vars'), 'GIT_ASKPASS')) { throw new \RuntimeException('safe_mode is enabled and safe_mode_allowed_env_vars does not contain GIT_ASKPASS, can not set env var. You can disable safe_mode with "-dsafe_mode=0" when running composer'); @@ -35,5 +169,27 @@ class Git if (getenv('GIT_WORK_TREE')) { putenv('GIT_WORK_TREE'); } + + // clean up env for OSX, see https://github.com/composer/composer/issues/2146#issuecomment-35478940 + putenv("DYLD_LIBRARY_PATH"); + } + + public static function getGitHubDomainsRegex(Config $config) + { + return '('.implode('|', array_map('preg_quote', $config->get('github-domains'))).')'; + } + + public static function sanitizeUrl($message) + { + return preg_replace('{://([^@]+?):.+?@}', '://$1:***@', $message); + } + + private function throwException($message, $url) + { + if (0 !== $this->process->execute('git --version', $ignoredOutput)) { + throw new \RuntimeException('Failed to clone '.self::sanitizeUrl($url).', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput()); + } + + throw new \RuntimeException($message); } } diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 49e56f8c9..6008dc062 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -40,7 +40,7 @@ class GitHub $this->io = $io; $this->config = $config; $this->process = $process ?: new ProcessExecutor; - $this->remoteFilesystem = $remoteFilesystem ?: new RemoteFilesystem($io); + $this->remoteFilesystem = $remoteFilesystem ?: new RemoteFilesystem($io, $config); } /** @@ -76,58 +76,128 @@ class GitHub */ public function authorizeOAuthInteractively($originUrl, $message = null) { - $attemptCounter = 0; - - $apiUrl = ('github.com' === $originUrl) ? 'api.github.com' : $originUrl . '/api/v3'; - if ($message) { $this->io->write($message); } - $this->io->write('The credentials will be swapped for an OAuth token stored in '.$this->config->get('home').'/config.json, your password will not be stored'); + + $this->io->write(sprintf('A token will be created and stored in "%s", your password will never be stored', $this->config->getAuthConfigSource()->getName())); $this->io->write('To revoke access to this token you can visit https://github.com/settings/applications'); + + $otp = null; + $attemptCounter = 0; + while ($attemptCounter++ < 5) { try { - $username = $this->io->ask('Username: '); - $password = $this->io->askAndHideAnswer('Password: '); - $this->io->setAuthentication($originUrl, $username, $password); - - // build up OAuth app name - $appName = 'Composer'; - if (0 === $this->process->execute('hostname', $output)) { - $appName .= ' on ' . trim($output); - } - - $contents = JsonFile::parseJson($this->remoteFilesystem->getContents($originUrl, 'https://'. $apiUrl . '/authorizations', false, array( - 'http' => array( - 'method' => 'POST', - 'follow_location' => false, - 'header' => "Content-Type: application/json\r\n", - 'content' => json_encode(array( - 'scopes' => array('repo'), - 'note' => $appName, - 'note_url' => 'https://getcomposer.org/', - )), - ) - ))); + $response = $this->createToken($originUrl, $otp); } catch (TransportException $e) { + // https://developer.github.com/v3/#authentication && https://developer.github.com/v3/auth/#working-with-two-factor-authentication + // 401 is bad credentials, or missing otp code + // 403 is max login attempts exceeded if (in_array($e->getCode(), array(403, 401))) { - $this->io->write('Invalid credentials.'); + // in case of a 401, and authentication was previously provided + if (401 === $e->getCode() && $this->io->hasAuthentication($originUrl)) { + // check for the presence of otp headers and get otp code from user + $otp = $this->checkTwoFactorAuthentication($e->getHeaders()); + // if given, retry creating a token using the user provided code + if (null !== $otp) { + continue; + } + } + + if (401 === $e->getCode()) { + $this->io->write('Bad credentials.'); + } else { + $this->io->write('Maximum number of login attempts exceeded. Please try again later.'); + } + + $this->io->write('You can also manually create a personal token at https://github.com/settings/applications'); + $this->io->write('Add it using "composer config github-oauth.github.com "'); + continue; } throw $e; } - $this->io->setAuthentication($originUrl, $contents['token'], 'x-oauth-basic'); - + $this->io->setAuthentication($originUrl, $response['token'], 'x-oauth-basic'); + $this->config->getConfigSource()->removeConfigSetting('github-oauth.'.$originUrl); // store value in user config - $githubTokens = $this->config->get('github-oauth') ?: array(); - $githubTokens[$originUrl] = $contents['token']; - $this->config->getConfigSource()->addConfigSetting('github-oauth', $githubTokens); + $this->config->getAuthConfigSource()->addConfigSetting('github-oauth.'.$originUrl, $response['token']); return true; } throw new \RuntimeException("Invalid GitHub credentials 5 times in a row, aborting."); } + + private function createToken($originUrl, $otp = null) + { + if (null === $otp || !$this->io->hasAuthentication($originUrl)) { + $username = $this->io->ask('Username: '); + $password = $this->io->askAndHideAnswer('Password: '); + + $this->io->setAuthentication($originUrl, $username, $password); + } + + $headers = array('Content-Type: application/json'); + if ($otp) { + $headers[] = 'X-GitHub-OTP: ' . $otp; + } + + $note = 'Composer'; + if ($this->config->get('github-expose-hostname') === true && 0 === $this->process->execute('hostname', $output)) { + $note .= ' on ' . trim($output); + } + $note .= ' [' . date('YmdHis') . ']'; + + $apiUrl = ('github.com' === $originUrl) ? 'api.github.com' : $originUrl . '/api/v3'; + + $json = $this->remoteFilesystem->getContents($originUrl, 'https://'. $apiUrl . '/authorizations', false, array( + 'retry-auth-failure' => false, + 'http' => array( + 'method' => 'POST', + 'follow_location' => false, + 'header' => $headers, + 'content' => json_encode(array( + 'scopes' => array('repo'), + 'note' => $note, + 'note_url' => 'https://getcomposer.org/', + )), + ) + )); + + $this->io->write('Token successfully created'); + + return JsonFile::parseJson($json); + } + + private function checkTwoFactorAuthentication(array $headers) + { + $headerNames = array_map( + function ($header) { + return strtolower(strstr($header, ':', true)); + }, + $headers + ); + + if (false !== ($key = array_search('x-github-otp', $headerNames))) { + list($required, $method) = array_map('trim', explode(';', substr(strstr($headers[$key], ':'), 1))); + + if ('required' === $required) { + $this->io->write('Two-factor Authentication'); + + if ('app' === $method) { + $this->io->write('Open the two-factor authentication app on your device to view your authentication code and verify your identity.'); + } + + if ('sms' === $method) { + $this->io->write('You have been sent an SMS message with an authentication code to verify your identity.'); + } + + return $this->io->ask('Authentication Code: '); + } + } + + return null; + } } diff --git a/src/Composer/Util/NoProxyPattern.php b/src/Composer/Util/NoProxyPattern.php index 533dbc19c..4e5c0c76b 100644 --- a/src/Composer/Util/NoProxyPattern.php +++ b/src/Composer/Util/NoProxyPattern.php @@ -126,7 +126,7 @@ class NoProxyPattern // Now do some bit shifting/switching to convert to ints $i = ($a << 24) + ($b << 16) + ($c << 8) + $d; - $mask = $bits == 0 ? 0: (~0 << (32 - $bits)); + $mask = $bits == 0 ? 0 : (~0 << (32 - $bits)); // Here's our lowest int $low = $i & $mask; diff --git a/src/Composer/Util/Perforce.php b/src/Composer/Util/Perforce.php index 94b9730eb..8e79f87b3 100644 --- a/src/Composer/Util/Perforce.php +++ b/src/Composer/Util/Perforce.php @@ -35,27 +35,35 @@ class Perforce protected $windowsFlag; protected $commandResult; - public function __construct($repoConfig, $port, $path, ProcessExecutor $process, $isWindows) + protected $io; + + protected $filesystem; + + public function __construct($repoConfig, $port, $path, ProcessExecutor $process, $isWindows, IOInterface $io) { $this->windowsFlag = $isWindows; $this->p4Port = $port; $this->initializePath($path); $this->process = $process; $this->initialize($repoConfig); + $this->io = $io; } - public static function create($repoConfig, $port, $path, ProcessExecutor $process = null) + public static function create($repoConfig, $port, $path, ProcessExecutor $process, IOInterface $io) { - if (!isset($process)) { - $process = new ProcessExecutor; - } $isWindows = defined('PHP_WINDOWS_VERSION_BUILD'); - - $perforce = new Perforce($repoConfig, $port, $path, $process, $isWindows); + $perforce = new Perforce($repoConfig, $port, $path, $process, $isWindows, $io); return $perforce; } + public static function checkServerExists($url, ProcessExecutor $processExecutor) + { + $output = null; + + return 0 === $processExecutor->execute('p4 -p ' . $url . ' info -s', $output); + } + public function initialize($repoConfig) { $this->uniquePerforceClientName = $this->generateUniquePerforceClientName(); @@ -100,10 +108,12 @@ class Perforce public function cleanupClientSpec() { $client = $this->getClient(); - $command = 'p4 client -d $client'; + $task = 'client -d ' . $client; + $useP4Client = false; + $command = $this->generateP4Command($task, $useP4Client); $this->executeCommand($command); $clientSpec = $this->getP4ClientSpec(); - $fileSystem = new FileSystem($this->process); + $fileSystem = $this->getFilesystem(); $fileSystem->remove($clientSpec); } @@ -111,6 +121,7 @@ class Perforce { $this->commandResult = ""; $exit_code = $this->process->execute($command, $this->commandResult); + return $exit_code; } @@ -132,7 +143,7 @@ class Perforce public function initializePath($path) { $this->path = $path; - $fs = new Filesystem(); + $fs = $this->getFilesystem(); $fs->ensureDirectoryExists($path); } @@ -191,7 +202,12 @@ class Perforce return $this->p4User; } - public function queryP4User(IOInterface $io) + public function setUser($user) + { + $this->p4User = $user; + } + + public function queryP4User() { $this->getUser(); if (strlen($this->p4User) > 0) { @@ -201,7 +217,7 @@ class Perforce if (strlen($this->p4User) > 0) { return; } - $this->p4User = $io->ask('Enter P4 User:'); + $this->p4User = $this->io->ask('Enter P4 User:'); if ($this->windowsFlag) { $command = 'p4 set P4USER=' . $this->p4User; } else { @@ -235,18 +251,19 @@ class Perforce $command = 'echo $' . $name; $this->executeCommand($command); $result = trim($this->commandResult); + return $result; } } - public function queryP4Password(IOInterface $io) + public function queryP4Password() { if (isset($this->p4Password)) { return $this->p4Password; } $password = $this->getP4variable('P4PASSWD'); if (strlen($password) <= 0) { - $password = $io->askAndHideAnswer('Enter password for Perforce user ' . $this->getUser() . ': '); + $password = $this->io->askAndHideAnswer('Enter password for Perforce user ' . $this->getUser() . ': '); } $this->p4Password = $password; @@ -270,18 +287,19 @@ class Perforce { $command = $this->generateP4Command('login -s', false); $exitCode = $this->executeCommand($command); - if ($exitCode){ + if ($exitCode) { $errorOutput = $this->process->getErrorOutput(); $index = strpos($errorOutput, $this->getUser()); - if ($index === false){ + if ($index === false) { $index = strpos($errorOutput, 'p4'); - if ($index===false){ + if ($index === false) { return false; } throw new \Exception('p4 command not found in path: ' . $errorOutput); } throw new \Exception('Invalid user name: ' . $this->getUser() ); } + return true; } @@ -291,19 +309,15 @@ class Perforce $this->executeCommand($p4CreateClientCommand); } - public function syncCodeBase($label) + public function syncCodeBase($sourceReference) { $prevDir = getcwd(); chdir($this->path); - $p4SyncCommand = $this->generateP4Command('sync -f '); - if (isset($label)) { - if (strcmp($label, 'dev-master') != 0) { - $p4SyncCommand = $p4SyncCommand . '@' . $label; - } + if (null != $sourceReference) { + $p4SyncCommand = $p4SyncCommand . '@' . $sourceReference; } $this->executeCommand($p4SyncCommand); - chdir($prevDir); } @@ -364,30 +378,24 @@ class Perforce return $process->run(); } - public function p4Login(IOInterface $io) + public function p4Login() { - $this->queryP4User($io); + $this->queryP4User(); if (!$this->isLoggedIn()) { - $password = $this->queryP4Password($io); + $password = $this->queryP4Password(); if ($this->windowsFlag) { $this->windowsLogin($password); } else { $command = 'echo ' . $password . ' | ' . $this->generateP4Command(' login -a', false); $exitCode = $this->executeCommand($command); $result = trim($this->commandResult); - if ($exitCode){ + if ($exitCode) { throw new \Exception("Error logging in:" . $this->process->getErrorOutput()); } } } } - public static function checkServerExists($url, ProcessExecutor $processExecutor) - { - $output = null; - return 0 === $processExecutor->execute('p4 -p ' . $url . ' info -s', $output); - } - public function getComposerInformation($identifier) { $index = strpos($identifier, '@'); @@ -459,8 +467,15 @@ class Perforce } } } - $branches = array(); - $branches['master'] = $possibleBranches[$this->p4Branch]; + $command = $this->generateP4Command('changes '. $this->getStream() . '/...', false); + $this->executeCommand($command); + $result = $this->commandResult; + $resArray = explode(PHP_EOL, $result); + $lastCommit = $resArray[0]; + $lastCommitArr = explode(' ', $lastCommit); + $lastCommitNum = $lastCommitArr[1]; + + $branches = array('master' => $possibleBranches[$this->p4Branch] . '@'. $lastCommitNum); return $branches; } @@ -541,4 +556,18 @@ class Perforce return $result; } + + public function getFilesystem() + { + if (empty($this->filesystem)) { + $this->filesystem = new Filesystem($this->process); + } + + return $this->filesystem; + } + + public function setFilesystem(Filesystem $fs) + { + $this->filesystem = $fs; + } } diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index 1ad2b1dfa..46dafb0ae 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -13,6 +13,7 @@ namespace Composer\Util; use Symfony\Component\Process\Process; +use Symfony\Component\Process\ProcessUtils; use Composer\IO\IOInterface; /** @@ -34,10 +35,10 @@ class ProcessExecutor /** * runs a process on the commandline * - * @param string $command the command to execute - * @param mixed $output the output will be written into this var if passed by ref - * if a callable is passed it will be used as output handler - * @param string $cwd the working directory + * @param string $command the command to execute + * @param mixed $output the output will be written into this var if passed by ref + * if a callable is passed it will be used as output handler + * @param string $cwd the working directory * @return int statuscode */ public function execute($command, &$output = null, $cwd = null) @@ -104,4 +105,17 @@ class ProcessExecutor { static::$timeout = $timeout; } + + /** + * Escapes a string to be used as a shell argument. + * + * @param string $argument The argument that will be escaped + * + * @return string The escaped argument + */ + + public static function escape($argument) + { + return ProcessUtils::escapeArgument($argument); + } } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index d3ecec03d..455bda92e 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -13,6 +13,7 @@ namespace Composer\Util; use Composer\Composer; +use Composer\Config; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; @@ -24,7 +25,7 @@ use Composer\Downloader\TransportException; class RemoteFilesystem { private $io; - private $firstCall; + private $config; private $bytesMax; private $originUrl; private $fileUrl; @@ -33,16 +34,21 @@ class RemoteFilesystem private $progress; private $lastProgress; private $options; + private $retryAuthFailure; + private $lastHeaders; + private $storeAuth; /** * Constructor. * * @param IOInterface $io The IO instance + * @param Config $config The config * @param array $options The options */ - public function __construct(IOInterface $io, $options = array()) + public function __construct(IOInterface $io, Config $config = null, array $options = array()) { $this->io = $io; + $this->config = $config; $this->options = $options; } @@ -70,7 +76,7 @@ class RemoteFilesystem * @param boolean $progress Display the progression * @param array $options Additional context options * - * @return string The content + * @return bool|string The content */ public function getContents($originUrl, $fileUrl, $progress = true, $options = array()) { @@ -87,6 +93,16 @@ class RemoteFilesystem return $this->options; } + /** + * Returns the headers of the last request + * + * @return array + */ + public function getLastHeaders() + { + return $this->lastHeaders; + } + /** * Get file content or copy action. * @@ -103,18 +119,30 @@ class RemoteFilesystem */ protected function get($originUrl, $fileUrl, $additionalOptions = array(), $fileName = null, $progress = true) { + if (strpos($originUrl, '.github.com') === (strlen($originUrl) - 11)) { + $originUrl = 'github.com'; + } + $this->bytesMax = 0; $this->originUrl = $originUrl; $this->fileUrl = $fileUrl; $this->fileName = $fileName; $this->progress = $progress; $this->lastProgress = null; + $this->retryAuthFailure = true; + $this->lastHeaders = array(); // capture username/password from URL if there is one if (preg_match('{^https?://(.+):(.+)@([^/]+)}i', $fileUrl, $match)) { $this->io->setAuthentication($originUrl, urldecode($match[1]), urldecode($match[2])); } + if (isset($additionalOptions['retry-auth-failure'])) { + $this->retryAuthFailure = (bool) $additionalOptions['retry-auth-failure']; + + unset($additionalOptions['retry-auth-failure']); + } + $options = $this->getOptionsForUrl($originUrl, $additionalOptions); if ($this->io->isDebug()) { @@ -124,6 +152,9 @@ class RemoteFilesystem $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['github-token']; unset($options['github-token']); } + if (isset($options['http'])) { + $options['http']['ignore_errors'] = true; + } $ctx = StreamContextFactory::getContext($fileUrl, $options, array('notification' => array($this, 'callbackGet'))); if ($this->progress) { @@ -145,6 +176,10 @@ class RemoteFilesystem if ($e instanceof TransportException && !empty($http_response_header[0])) { $e->setHeaders($http_response_header); } + if ($e instanceof TransportException && $result !== false) { + $e->setResponse($result); + } + $result = false; } if ($errorMessage && !ini_get('allow_url_fopen')) { $errorMessage = 'allow_url_fopen must be enabled in php.ini ('.$errorMessage.')'; @@ -154,10 +189,16 @@ class RemoteFilesystem throw $e; } - // fix for 5.4.0 https://bugs.php.net/bug.php?id=61336 + // fail 4xx and 5xx responses and capture the response if (!empty($http_response_header[0]) && preg_match('{^HTTP/\S+ ([45]\d\d)}i', $http_response_header[0], $match)) { - $result = false; $errorCode = $match[1]; + if (!$this->retry) { + $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.$http_response_header[0].')', $errorCode); + $e->setHeaders($http_response_header); + $e->setResponse($result); + throw $e; + } + $result = false; } // decode gzip @@ -179,10 +220,14 @@ class RemoteFilesystem // work around issue with gzuncompress & co that do not work with all gzip checksums $result = file_get_contents('compress.zlib://data:application/octet-stream;base64,'.base64_encode($result)); } + + if (!$result) { + throw new TransportException('Failed to decode zlib stream'); + } } } - if ($this->progress) { + if ($this->progress && !$this->retry) { $this->io->overwrite(" Downloading: 100%"); } @@ -209,7 +254,13 @@ class RemoteFilesystem if ($this->retry) { $this->retry = false; - return $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); + $result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); + + $authHelper = new AuthHelper($this->io, $this->config); + $authHelper->storeAuth($this->originUrl, $this->storeAuth); + $this->storeAuth = false; + + return $result; } if (false === $result) { @@ -221,6 +272,10 @@ class RemoteFilesystem throw $e; } + if (!empty($http_response_header[0])) { + $this->lastHeaders = $http_response_header; + } + return $result; } @@ -241,31 +296,19 @@ class RemoteFilesystem case STREAM_NOTIFY_FAILURE: case STREAM_NOTIFY_AUTH_REQUIRED: if (401 === $messageCode) { - if (!$this->io->isInteractive()) { - $message = "The '" . $this->fileUrl . "' URL required authentication.\nYou must be using the interactive console"; - - throw new TransportException($message, 401); + // Bail if the caller is going to handle authentication failures itself. + if (!$this->retryAuthFailure) { + break; } - $this->promptAuthAndRetry(); - break; - } - - if ($notificationCode === STREAM_NOTIFY_AUTH_REQUIRED) { + $this->promptAuthAndRetry($messageCode); break; } - - throw new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.trim($message).')', $messageCode); + break; case STREAM_NOTIFY_AUTH_RESULT: if (403 === $messageCode) { - if (!$this->io->isInteractive() || $this->io->hasAuthentication($this->originUrl)) { - $message = "The '" . $this->fileUrl . "' URL could not be accessed: " . $message; - - throw new TransportException($message, 403); - } - - $this->promptAuthAndRetry(); + $this->promptAuthAndRetry($messageCode, $message); break; } break; @@ -296,12 +339,44 @@ class RemoteFilesystem } } - protected function promptAuthAndRetry() + protected function promptAuthAndRetry($httpStatus, $reason = null) { - $this->io->overwrite(' Authentication required ('.parse_url($this->fileUrl, PHP_URL_HOST).'):'); - $username = $this->io->ask(' Username: '); - $password = $this->io->askAndHideAnswer(' Password: '); - $this->io->setAuthentication($this->originUrl, $username, $password); + if ($this->config && in_array($this->originUrl, $this->config->get('github-domains'), true)) { + $message = "\n".'Could not fetch '.$this->fileUrl.', enter your GitHub credentials '.($httpStatus === 404 ? 'to access private repos' : 'to go over the API rate limit'); + $gitHubUtil = new GitHub($this->io, $this->config, null); + if (!$gitHubUtil->authorizeOAuth($this->originUrl) + && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($this->originUrl, $message)) + ) { + throw new TransportException('Could not authenticate against '.$this->originUrl, 401); + } + } else { + // 404s are only handled for github + if ($httpStatus === 404) { + return; + } + + // fail if the console is not interactive + if (!$this->io->isInteractive()) { + if ($httpStatus === 401) { + $message = "The '" . $this->fileUrl . "' URL required authentication.\nYou must be using the interactive console to authenticate"; + } + if ($httpStatus === 403) { + $message = "The '" . $this->fileUrl . "' URL could not be accessed: " . $reason; + } + + throw new TransportException($message, $httpStatus); + } + // fail if we already have auth + if ($this->io->hasAuthentication($this->originUrl)) { + throw new TransportException("Invalid credentials for '" . $this->fileUrl . "', aborting.", $httpStatus); + } + + $this->io->overwrite(' Authentication required ('.parse_url($this->fileUrl, PHP_URL_HOST).'):'); + $username = $this->io->ask(' Username: '); + $password = $this->io->askAndHideAnswer(' Password: '); + $this->io->setAuthentication($this->originUrl, $username, $password); + $this->storeAuth = $this->config->get('store-auths'); + } $this->retry = true; throw new TransportException('RETRY'); @@ -309,15 +384,19 @@ class RemoteFilesystem protected function getOptionsForUrl($originUrl, $additionalOptions) { + if (defined('HHVM_VERSION')) { + $phpVersion = 'HHVM ' . HHVM_VERSION; + } else { + $phpVersion = 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION; + } + $headers = array( sprintf( - 'User-Agent: Composer/%s (%s; %s; PHP %s.%s.%s)', + 'User-Agent: Composer/%s (%s; %s; %s)', Composer::VERSION === '@package_version@' ? 'source' : Composer::VERSION, php_uname('s'), php_uname('r'), - PHP_MAJOR_VERSION, - PHP_MINOR_VERSION, - PHP_RELEASE_VERSION + $phpVersion ) ); diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index 1c3c20e44..5bcb431ca 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -43,6 +43,19 @@ final class StreamContextFactory $proxy = parse_url(!empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']); } + // Override with HTTPS proxy if present and URL is https + if (preg_match('{^https://}i', $url) && (!empty($_SERVER['HTTPS_PROXY']) || !empty($_SERVER['https_proxy']))) { + $proxy = parse_url(!empty($_SERVER['https_proxy']) ? $_SERVER['https_proxy'] : $_SERVER['HTTPS_PROXY']); + } + + // Remove proxy if URL matches no_proxy directive + if (!empty($_SERVER['no_proxy']) && parse_url($url, PHP_URL_HOST)) { + $pattern = new NoProxyPattern($_SERVER['no_proxy']); + if ($pattern->test($url)) { + unset($proxy); + } + } + if (!empty($proxy)) { $proxyURL = isset($proxy['scheme']) ? $proxy['scheme'] . '://' : ''; $proxyURL .= isset($proxy['host']) ? $proxy['host'] : ''; @@ -64,48 +77,46 @@ final class StreamContextFactory $options['http']['proxy'] = $proxyURL; - // Handle no_proxy directive - if (!empty($_SERVER['no_proxy']) && parse_url($url, PHP_URL_HOST)) { - $pattern = new NoProxyPattern($_SERVER['no_proxy']); - if ($pattern->test($url)) { - unset($options['http']['proxy']); + // enabled request_fulluri unless it is explicitly disabled + switch (parse_url($url, PHP_URL_SCHEME)) { + case 'http': // default request_fulluri to true + $reqFullUriEnv = getenv('HTTP_PROXY_REQUEST_FULLURI'); + if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) { + $options['http']['request_fulluri'] = true; + } + break; + case 'https': // default request_fulluri to true + $reqFullUriEnv = getenv('HTTPS_PROXY_REQUEST_FULLURI'); + if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) { + $options['http']['request_fulluri'] = true; + } + break; + } + + // add SNI opts for https URLs + if ('https' === parse_url($url, PHP_URL_SCHEME)) { + $options['ssl']['SNI_enabled'] = true; + if (version_compare(PHP_VERSION, '5.6.0', '<')) { + $options['ssl']['SNI_server_name'] = parse_url($url, PHP_URL_HOST); } } - // add request_fulluri and authentication if we still have a proxy to connect to - if (!empty($options['http']['proxy'])) { - // enabled request_fulluri unless it is explicitly disabled - switch (parse_url($url, PHP_URL_SCHEME)) { - case 'http': // default request_fulluri to true - $reqFullUriEnv = getenv('HTTP_PROXY_REQUEST_FULLURI'); - if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) { - $options['http']['request_fulluri'] = true; - } - break; - case 'https': // default request_fulluri to true - $reqFullUriEnv = getenv('HTTPS_PROXY_REQUEST_FULLURI'); - if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) { - $options['http']['request_fulluri'] = true; - } - break; + // handle proxy auth if present + if (isset($proxy['user'])) { + $auth = urldecode($proxy['user']); + if (isset($proxy['pass'])) { + $auth .= ':' . urldecode($proxy['pass']); } + $auth = base64_encode($auth); - if (isset($proxy['user'])) { - $auth = urldecode($proxy['user']); - if (isset($proxy['pass'])) { - $auth .= ':' . urldecode($proxy['pass']); - } - $auth = base64_encode($auth); - - // Preserve headers if already set in default options - if (isset($defaultOptions['http']['header'])) { - if (is_string($defaultOptions['http']['header'])) { - $defaultOptions['http']['header'] = array($defaultOptions['http']['header']); - } - $defaultOptions['http']['header'][] = "Proxy-Authorization: Basic {$auth}"; - } else { - $options['http']['header'] = array("Proxy-Authorization: Basic {$auth}"); + // Preserve headers if already set in default options + if (isset($defaultOptions['http']['header'])) { + if (is_string($defaultOptions['http']['header'])) { + $defaultOptions['http']['header'] = array($defaultOptions['http']['header']); } + $defaultOptions['http']['header'][] = "Proxy-Authorization: Basic {$auth}"; + } else { + $options['http']['header'] = array("Proxy-Authorization: Basic {$auth}"); } } } diff --git a/src/Composer/Util/Svn.php b/src/Composer/Util/Svn.php index 510b45daa..4949aa271 100644 --- a/src/Composer/Util/Svn.php +++ b/src/Composer/Util/Svn.php @@ -12,6 +12,7 @@ namespace Composer\Util; +use Composer\Config; use Composer\IO\IOInterface; /** @@ -57,18 +58,31 @@ class Svn */ protected $qtyAuthTries = 0; + /** + * @var \Composer\Config + */ + protected $config; + /** * @param string $url * @param \Composer\IO\IOInterface $io + * @param Config $config * @param ProcessExecutor $process */ - public function __construct($url, IOInterface $io, ProcessExecutor $process = null) + public function __construct($url, IOInterface $io, Config $config, ProcessExecutor $process = null) { $this->url = $url; $this->io = $io; + $this->config = $config; $this->process = $process ?: new ProcessExecutor; } + public static function cleanEnv() + { + // clean up env for OSX, see https://github.com/composer/composer/issues/2146#issuecomment-35478940 + putenv("DYLD_LIBRARY_PATH"); + } + /** * Execute an SVN command and try to fix up the process with credentials * if necessary. @@ -117,17 +131,12 @@ class Svn throw new \RuntimeException($output); } - // no auth supported for non interactive calls - if (!$this->io->isInteractive()) { - throw new \RuntimeException( - 'can not ask for authentication in non interactive mode ('.$output.')' - ); + if (!$this->hasAuth()) { + $this->doAuthDance(); } // try to authenticate if maximum quantity of tries not reached - if ($this->qtyAuthTries++ < self::MAX_QTY_AUTH_TRIES || !$this->hasAuth()) { - $this->doAuthDance(); - + if ($this->qtyAuthTries++ < self::MAX_QTY_AUTH_TRIES) { // restart the process return $this->execute($command, $url, $cwd, $path, $verbose); } @@ -137,13 +146,29 @@ class Svn ); } + /** + * @param boolean $cacheCredentials + */ + public function setCacheCredentials($cacheCredentials) + { + $this->cacheCredentials = $cacheCredentials; + } + /** * Repositories requests credentials, let's put them in. * * @return \Composer\Util\Svn + * @throws \RuntimeException */ protected function doAuthDance() { + // cannot ask for credentials in non interactive mode + if (!$this->io->isInteractive()) { + throw new \RuntimeException( + 'can not ask for authentication in non interactive mode' + ); + } + $this->io->write("The Subversion server ({$this->url}) requested credentials:"); $this->hasAuth = true; @@ -170,11 +195,11 @@ class Svn $cmd, '--non-interactive ', $this->getCredentialString(), - escapeshellarg($url) + ProcessExecutor::escape($url) ); if ($path) { - $cmd .= ' ' . escapeshellarg($path); + $cmd .= ' ' . ProcessExecutor::escape($path); } return $cmd; @@ -196,8 +221,8 @@ class Svn return sprintf( ' %s--username %s --password %s ', $this->getAuthCache(), - escapeshellarg($this->getUsername()), - escapeshellarg($this->getPassword()) + ProcessExecutor::escape($this->getUsername()), + ProcessExecutor::escape($this->getPassword()) ); } @@ -242,17 +267,11 @@ class Svn return $this->hasAuth; } - $uri = parse_url($this->url); - if (empty($uri['user'])) { - return $this->hasAuth = false; - } - - $this->credentials['username'] = $uri['user']; - if (!empty($uri['pass'])) { - $this->credentials['password'] = $uri['pass']; + if (false === $this->createAuthFromConfig()) { + $this->createAuthFromUrl(); } - return $this->hasAuth = true; + return $this->hasAuth; } /** @@ -264,4 +283,48 @@ class Svn { return $this->cacheCredentials ? '' : '--no-auth-cache '; } + + /** + * Create the auth params from the configuration file. + * + * @return bool + */ + private function createAuthFromConfig() + { + if (!$this->config->has('http-basic')) { + return $this->hasAuth = false; + } + + $authConfig = $this->config->get('http-basic'); + + $host = parse_url($this->url, PHP_URL_HOST); + if (isset($authConfig[$host])) { + $this->credentials['username'] = $authConfig[$host]['username']; + $this->credentials['password'] = $authConfig[$host]['password']; + + return $this->hasAuth = true; + } + + return $this->hasAuth = false; + } + + /** + * Create the auth params from the url + * + * @return bool + */ + private function createAuthFromUrl() + { + $uri = parse_url($this->url); + if (empty($uri['user'])) { + return $this->hasAuth = false; + } + + $this->credentials['username'] = $uri['user']; + if (!empty($uri['pass'])) { + $this->credentials['password'] = $uri['pass']; + } + + return $this->hasAuth = true; + } } diff --git a/tests/Composer/Test/AllFunctionalTest.php b/tests/Composer/Test/AllFunctionalTest.php index 03ec4305f..4a23c5717 100644 --- a/tests/Composer/Test/AllFunctionalTest.php +++ b/tests/Composer/Test/AllFunctionalTest.php @@ -60,6 +60,10 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase public function testBuildPhar() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Building the phar does not work on HHVM.'); + } + $fs = new Filesystem; $fs->removeDirectory(dirname(self::$pharPath)); $fs->ensureDirectoryExists(dirname(self::$pharPath)); @@ -124,7 +128,7 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase $testDir = sys_get_temp_dir().'/composer_functional_test'.uniqid(mt_rand(), true); $this->testDir = $testDir; $varRegex = '#%([a-zA-Z_-]+)%#'; - $variableReplacer = function($match) use (&$data, $testDir) { + $variableReplacer = function ($match) use (&$data, $testDir) { list(, $var) = $match; switch ($var) { diff --git a/tests/Composer/Test/ApplicationTest.php b/tests/Composer/Test/ApplicationTest.php new file mode 100644 index 000000000..c99022671 --- /dev/null +++ b/tests/Composer/Test/ApplicationTest.php @@ -0,0 +1,76 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test; + +use Composer\Console\Application; +use Composer\TestCase; + +class ApplicationTest extends TestCase +{ + public function testDevWarning() + { + $application = new Application; + + $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface'); + $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); + + $inputMock->expects($this->once()) + ->method('getFirstArgument') + ->will($this->returnValue('list')); + + $outputMock->expects($this->once()) + ->method("writeln") + ->with($this->equalTo(sprintf('Warning: This development build of composer is over 30 days old. It is recommended to update it by running "%s self-update" to get the latest version.', $_SERVER['PHP_SELF']))); + + if (!defined('COMPOSER_DEV_WARNING_TIME')) { + define('COMPOSER_DEV_WARNING_TIME', time() - 1); + } + + $this->setExpectedException('RuntimeException'); + $application->doRun($inputMock, $outputMock); + } + + public function ensureNoDevWarning($command) + { + $application = new Application; + + $application->add(new \Composer\Command\SelfUpdateCommand); + + $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface'); + $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); + + $inputMock->expects($this->once()) + ->method('getFirstArgument') + ->will($this->returnValue($command)); + + $outputMock->expects($this->never()) + ->method("writeln"); + + if (!defined('COMPOSER_DEV_WARNING_TIME')) { + define('COMPOSER_DEV_WARNING_TIME', time() - 1); + } + + $this->setExpectedException('RuntimeException'); + $application->doRun($inputMock, $outputMock); + } + + public function testDevWarningPrevented() + { + $this->ensureNoDevWarning('self-update'); + } + + public function testDevWarningPreventedAlias() + { + $this->ensureNoDevWarning('self-up'); + } +} diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index 4f91903cf..11639fddc 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -19,18 +19,65 @@ use Composer\Package\AliasPackage; use Composer\Package\Package; use Composer\TestCase; use Composer\Script\ScriptEvents; +use Composer\Repository\InstalledRepositoryInterface; +use Composer\Installer\InstallationManager; +use Composer\Config; +use Composer\EventDispatcher\EventDispatcher; +use PHPUnit_Framework_MockObject_MockObject as MockObject; class AutoloadGeneratorTest extends TestCase { + /** + * @var string + */ public $vendorDir; + + /** + * @var Config|MockObject + */ private $config; + + /** + * @var string + */ private $workingDir; + + /** + * @var InstallationManager|MockObject + */ private $im; + + /** + * @var InstalledRepositoryInterface|MockObject + */ private $repository; + + /** + * @var AutoloadGenerator + */ private $generator; + + /** + * @var Filesystem + */ private $fs; + + /** + * @var EventDispatcher|MockObject + */ private $eventDispatcher; + /** + * Map of setting name => return value configuration for the stub Config + * object. + * + * Note: must be public for compatibility with PHP 5.3 runtimes where + * closures cannot access private members of the classes they are created + * in. + * @var array + */ + public $configValueMap; + protected function setUp() { $this->fs = new Filesystem; @@ -43,18 +90,23 @@ class AutoloadGeneratorTest extends TestCase $this->config = $this->getMock('Composer\Config'); - $this->config->expects($this->at(0)) - ->method('get') - ->with($this->equalTo('vendor-dir')) - ->will($this->returnCallback(function () use ($that) { + $this->configValueMap = array( + 'vendor-dir' => function () use ($that) { return $that->vendorDir; - })); + }, + ); - $this->config->expects($this->at(1)) + $this->config->expects($this->atLeastOnce()) ->method('get') - ->with($this->equalTo('vendor-dir')) - ->will($this->returnCallback(function () use ($that) { - return $that->vendorDir; + ->will($this->returnCallback(function ($arg) use ($that) { + $ret = null; + if (isset($that->configValueMap[$arg])) { + $ret = $that->configValueMap[$arg]; + if (is_callable($ret)) { + $ret = $ret(); + } + } + return $ret; })); $this->origDir = getcwd(); @@ -111,17 +163,19 @@ class AutoloadGeneratorTest extends TestCase ->will($this->returnValue(array())); $this->fs->ensureDirectoryExists($this->workingDir.'/composer'); - $this->fs->ensureDirectoryExists($this->workingDir.'/src'); + $this->fs->ensureDirectoryExists($this->workingDir.'/src/Lala'); $this->fs->ensureDirectoryExists($this->workingDir.'/lib'); + file_put_contents($this->workingDir.'/src/Lala/ClassMapMain.php', 'fs->ensureDirectoryExists($this->workingDir.'/src-fruit'); $this->fs->ensureDirectoryExists($this->workingDir.'/src-cake'); $this->fs->ensureDirectoryExists($this->workingDir.'/lib-cake'); + file_put_contents($this->workingDir.'/src-cake/ClassMapBar.php', 'fs->ensureDirectoryExists($this->workingDir.'/composersrc'); file_put_contents($this->workingDir.'/composersrc/foo.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_1'); + $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_1'); // Assert that autoload_namespaces.php was correctly generated. $this->assertAutoloadFiles('main', $this->vendorDir.'/composer'); @@ -133,6 +187,77 @@ class AutoloadGeneratorTest extends TestCase $this->assertAutoloadFiles('classmap', $this->vendorDir.'/composer', 'classmap'); } + public function testMainPackageDevAutoloading() + { + $package = new Package('a', '1.0', '1.0'); + $package->setAutoload(array( + 'psr-0' => array( + 'Main' => 'src/', + ), + )); + $package->setDevAutoload(array( + 'files' => array('devfiles/foo.php'), + 'psr-0' => array( + 'Main' => 'tests/' + ), + )); + + $this->repository->expects($this->once()) + ->method('getCanonicalPackages') + ->will($this->returnValue(array())); + + $this->fs->ensureDirectoryExists($this->workingDir.'/composer'); + $this->fs->ensureDirectoryExists($this->workingDir.'/src/Main'); + file_put_contents($this->workingDir.'/src/Main/ClassMain.php', 'fs->ensureDirectoryExists($this->workingDir.'/devfiles'); + file_put_contents($this->workingDir.'/devfiles/foo.php', 'generator->setDevMode(true); + $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_1'); + + // check standard autoload + $this->assertAutoloadFiles('main5', $this->vendorDir.'/composer'); + $this->assertAutoloadFiles('classmap7', $this->vendorDir.'/composer', 'classmap'); + + // make sure dev autoload is correctly dumped + $this->assertAutoloadFiles('files2', $this->vendorDir.'/composer', 'files'); + } + + public function testMainPackageDevAutoloadingDisabledByDefault() + { + $package = new Package('a', '1.0', '1.0'); + $package->setAutoload(array( + 'psr-0' => array( + 'Main' => 'src/', + ), + )); + $package->setDevAutoload(array( + 'files' => array('devfiles/foo.php'), + )); + + $this->repository->expects($this->once()) + ->method('getCanonicalPackages') + ->will($this->returnValue(array())); + + $this->fs->ensureDirectoryExists($this->workingDir.'/composer'); + $this->fs->ensureDirectoryExists($this->workingDir.'/src/Main'); + file_put_contents($this->workingDir.'/src/Main/ClassMain.php', 'fs->ensureDirectoryExists($this->workingDir.'/devfiles'); + file_put_contents($this->workingDir.'/devfiles/foo.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_1'); + + // check standard autoload + $this->assertAutoloadFiles('main4', $this->vendorDir.'/composer'); + $this->assertAutoloadFiles('classmap7', $this->vendorDir.'/composer', 'classmap'); + + // make sure dev autoload is disabled when dev mode is set to false + $this->assertFalse(is_file($this->vendorDir.'/composer/autoload_files.php')); + } + public function testVendorDirSameAsWorkingDir() { $this->vendorDir = $this->workingDir; @@ -198,10 +323,6 @@ class AutoloadGeneratorTest extends TestCase $package = new Package('a', '1.0', '1.0'); $package->setAutoload(array( 'psr-0' => array('Main\\Foo' => '', 'Main\\Bar' => ''), - 'psr-4' => array( - 'Acme\Fruit\\' => 'src-fruit/', - 'Acme\Cake\\' => array('src-cake/', 'lib-cake/'), - ), 'classmap' => array('Main/Foo/src', 'lib'), 'files' => array('foo.php', 'Main/Foo/bar.php'), )); @@ -378,6 +499,48 @@ class AutoloadGeneratorTest extends TestCase include $this->vendorDir.'/composer/autoload_classmap.php' ); $this->assertAutoloadFiles('classmap5', $this->vendorDir.'/composer', 'classmap'); + $this->assertNotContains('$loader->setClassMapAuthoritative(true);', file_get_contents($this->vendorDir.'/composer/autoload_real.php')); + } + + public function testClassMapAutoloadingAuthoritative() + { + $package = new Package('a', '1.0', '1.0'); + + $packages = array(); + $packages[] = $a = new Package('a/a', '1.0', '1.0'); + $packages[] = $b = new Package('b/b', '1.0', '1.0'); + $packages[] = $c = new Package('c/c', '1.0', '1.0'); + $a->setAutoload(array('classmap' => array(''))); + $b->setAutoload(array('classmap' => array('test.php'))); + $c->setAutoload(array('classmap' => array('./'))); + + $this->repository->expects($this->once()) + ->method('getCanonicalPackages') + ->will($this->returnValue($packages)); + + $this->configValueMap['classmap-authoritative'] = true; + + $this->fs->ensureDirectoryExists($this->vendorDir.'/composer'); + $this->fs->ensureDirectoryExists($this->vendorDir.'/a/a/src'); + $this->fs->ensureDirectoryExists($this->vendorDir.'/b/b'); + $this->fs->ensureDirectoryExists($this->vendorDir.'/c/c/foo'); + file_put_contents($this->vendorDir.'/a/a/src/a.php', 'vendorDir.'/b/b/test.php', 'vendorDir.'/c/c/foo/test.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_7'); + $this->assertTrue(file_exists($this->vendorDir.'/composer/autoload_classmap.php'), "ClassMap file needs to be generated."); + $this->assertEquals( + array( + 'ClassMapBar' => $this->vendorDir.'/b/b/test.php', + 'ClassMapBaz' => $this->vendorDir.'/c/c/foo/test.php', + 'ClassMapFoo' => $this->vendorDir.'/a/a/src/a.php', + ), + include $this->vendorDir.'/composer/autoload_classmap.php' + ); + $this->assertAutoloadFiles('classmap5', $this->vendorDir.'/composer', 'classmap'); + + $this->assertContains('$loader->setClassMapAuthoritative(true);', file_get_contents($this->vendorDir.'/composer/autoload_real.php')); } public function testFilesAutoloadGeneration() @@ -479,17 +642,31 @@ class AutoloadGeneratorTest extends TestCase $this->assertTrue(function_exists('testFilesAutoloadOrderByDependencyRoot')); } + /** + * Test that PSR-0 and PSR-4 mappings are processed in the correct order for + * autoloading and for classmap generation: + * - The main package has priority over other packages. + * - Longer namespaces have priority over shorter namespaces. + */ public function testOverrideVendorsAutoloading() { - $package = new Package('z', '1.0', '1.0'); - $package->setAutoload(array('psr-0' => array('A\\B' => $this->workingDir.'/lib'), 'classmap' => array($this->workingDir.'/src'))); - $package->setRequires(array(new Link('z', 'a/a'))); + $mainPackage = new Package('z', '1.0', '1.0'); + $mainPackage->setAutoload(array( + 'psr-0' => array('A\\B' => $this->workingDir.'/lib'), + 'classmap' => array($this->workingDir.'/src') + )); + $mainPackage->setRequires(array(new Link('z', 'a/a'))); $packages = array(); $packages[] = $a = new Package('a/a', '1.0', '1.0'); $packages[] = $b = new Package('b/b', '1.0', '1.0'); - $a->setAutoload(array('psr-0' => array('A' => 'src/', 'A\\B' => 'lib/'), 'classmap' => array('classmap'))); - $b->setAutoload(array('psr-0' => array('B\\Sub\\Name' => 'src/'))); + $a->setAutoload(array( + 'psr-0' => array('A' => 'src/', 'A\\B' => 'lib/'), + 'classmap' => array('classmap'), + )); + $b->setAutoload(array( + 'psr-0' => array('B\\Sub\\Name' => 'src/'), + )); $this->repository->expects($this->once()) ->method('getCanonicalPackages') @@ -502,8 +679,12 @@ class AutoloadGeneratorTest extends TestCase $this->fs->ensureDirectoryExists($this->vendorDir.'/a/a/src'); $this->fs->ensureDirectoryExists($this->vendorDir.'/a/a/lib/A/B'); $this->fs->ensureDirectoryExists($this->vendorDir.'/b/b/src'); + + // Define the classes A\B\C and Foo\Bar in the main package. file_put_contents($this->workingDir.'/lib/A/B/C.php', 'workingDir.'/src/classes.php', 'vendorDir.'/a/a/lib/A/B/C.php', 'vendorDir.'/a/a/classmap/classes.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_9'); + $this->generator->dump($this->config, $this->repository, $mainPackage, $this->im, 'composer', true, '_9'); $this->assertEquals($expectedNamespace, file_get_contents($this->vendorDir.'/composer/autoload_namespaces.php')); $this->assertEquals($expectedPsr4, file_get_contents($this->vendorDir.'/composer/autoload_psr4.php')); $this->assertEquals($expectedClassmap, file_get_contents($this->vendorDir.'/composer/autoload_classmap.php')); @@ -706,10 +887,7 @@ EOF; ->method('getCanonicalPackages') ->will($this->returnValue(array())); - $this->config->expects($this->at(2)) - ->method('get') - ->with($this->equalTo('use-include-path')) - ->will($this->returnValue(true)); + $this->configValueMap['use-include-path'] = true; $this->fs->ensureDirectoryExists($this->vendorDir.'/a'); diff --git a/tests/Composer/Test/Autoload/ClassLoaderTest.php b/tests/Composer/Test/Autoload/ClassLoaderTest.php index 0b9c1934e..50772101e 100644 --- a/tests/Composer/Test/Autoload/ClassLoaderTest.php +++ b/tests/Composer/Test/Autoload/ClassLoaderTest.php @@ -14,11 +14,11 @@ class ClassLoaderTest extends \PHPUnit_Framework_TestCase * * @dataProvider getLoadClassTests * - * @param string $class The fully-qualified class name to test, without preceding namespace separator. - * @param bool $prependSeparator Whether to call ->loadClass() with a class name with preceding - * namespace separator, as it happens in PHP 5.3.0 - 5.3.2. See https://bugs.php.net/50731 + * @param string $class The fully-qualified class name to test, without preceding namespace separator. + * @param bool $prependSeparator Whether to call ->loadClass() with a class name with preceding + * namespace separator, as it happens in PHP 5.3.0 - 5.3.2. See https://bugs.php.net/50731 */ - public function testLoadClass($class, $prependSeparator = FALSE) + public function testLoadClass($class, $prependSeparator = false) { $loader = new ClassLoader(); $loader->add('Namespaced\\', __DIR__ . '/Fixtures'); @@ -55,4 +55,13 @@ class ClassLoaderTest extends \PHPUnit_Framework_TestCase array('ShinyVendor\\ShinyPackage\\SubNamespace\\Bar', true), ); } + + /** + * getPrefixes method should return empty array if ClassLoader does not have any psr-0 configuration + */ + public function testGetPrefixesWithNoPSR0Configuration() + { + $loader = new ClassLoader(); + $this->assertEmpty($loader->getPrefixes()); + } } diff --git a/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php b/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php index ddadc89c5..1ef68d459 100644 --- a/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php +++ b/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php @@ -12,6 +12,8 @@ namespace Composer\Test\Autoload; use Composer\Autoload\ClassMapGenerator; +use Symfony\Component\Finder\Finder; +use Composer\Util\Filesystem; class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase { @@ -78,11 +80,9 @@ class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase public function testCreateMapFinderSupport() { - if (!class_exists('Symfony\\Component\\Finder\\Finder')) { - $this->markTestSkipped('Finder component is not available'); - } + $this->checkIfFinderIsAvailable(); - $finder = new \Symfony\Component\Finder\Finder(); + $finder = new Finder(); $finder->files()->in(__DIR__ . '/Fixtures/beta/NamespaceCollision'); $this->assertEqualsNormalized(array( @@ -104,6 +104,92 @@ class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase $find->invoke(null, __DIR__.'/no-file'); } + public function testAmbiguousReference() + { + $this->checkIfFinderIsAvailable(); + + $tempDir = sys_get_temp_dir().'/ComposerTestAmbiguousRefs'; + if (!is_dir($tempDir.'/other')) { + mkdir($tempDir.'/other', 0777, true); + } + + $finder = new Finder(); + $finder->files()->in($tempDir); + + $io = $this->getMockBuilder('Composer\IO\ConsoleIO') + ->disableOriginalConstructor() + ->getMock(); + + file_put_contents($tempDir.'/A.php', "expects($this->once()) + ->method('write') + ->will($this->returnCallback(function ($text) use (&$msg) { + $msg = $text; + })); + + $messages = array( + 'Warning: Ambiguous class resolution, "A" was found in both "'.$a.'" and "'.$b.'", the first will be used.', + 'Warning: Ambiguous class resolution, "A" was found in both "'.$b.'" and "'.$a.'", the first will be used.', + ); + + ClassMapGenerator::createMap($finder, null, $io); + + $this->assertTrue(in_array($msg, $messages, true), $msg.' not found in expected messages ('.var_export($messages, true).')'); + + $fs = new Filesystem(); + $fs->removeDirectory($tempDir); + } + + /** + * If one file has a class or interface defined more than once, + * an ambiguous reference warning should not be produced + */ + public function testUnambiguousReference() + { + $tempDir = sys_get_temp_dir().'/ComposerTestUnambiguousRefs'; + if (!is_dir($tempDir)) { + mkdir($tempDir, 0777, true); + } + + file_put_contents($tempDir.'/A.php', "getMockBuilder('Composer\IO\ConsoleIO') + ->disableOriginalConstructor() + ->getMock(); + + $io->expects($this->never()) + ->method('write'); + + ClassMapGenerator::createMap($tempDir, null, $io); + + $fs = new Filesystem(); + $fs->removeDirectory($tempDir); + } + /** * @expectedException \RuntimeException * @expectedExceptionMessage Could not scan for classes inside @@ -123,4 +209,11 @@ class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase } $this->assertEquals($expected, $actual, $message); } + + private function checkIfFinderIsAvailable() + { + if (!class_exists('Symfony\\Component\\Finder\\Finder')) { + $this->markTestSkipped('Finder component is not available'); + } + } } diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap.php b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap.php index b7b2a9656..2fe8ee07a 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap.php @@ -6,5 +6,7 @@ $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( + 'Acme\\Cake\\ClassMapBar' => $baseDir . '/src-cake/ClassMapBar.php', 'ClassMapFoo' => $baseDir . '/composersrc/foo.php', + 'Lala\\ClassMapMain' => $baseDir . '/src/Lala/ClassMapMain.php', ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap7.php b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap7.php new file mode 100644 index 000000000..5768726d1 --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap7.php @@ -0,0 +1,10 @@ + $baseDir . '/src/Main/ClassMain.php', +); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_files2.php b/tests/Composer/Test/Autoload/Fixtures/autoload_files2.php new file mode 100644 index 000000000..13cb9ecb3 --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_files2.php @@ -0,0 +1,10 @@ + array($baseDir . '/src'), +); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_main5.php b/tests/Composer/Test/Autoload/Fixtures/autoload_main5.php new file mode 100644 index 000000000..15cb2622b --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_main5.php @@ -0,0 +1,10 @@ + array($baseDir . '/src', $baseDir . '/tests'), +); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_real_files_by_dependency.php b/tests/Composer/Test/Autoload/Fixtures/autoload_real_files_by_dependency.php index e58e8d2fa..083070539 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_files_by_dependency.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_files_by_dependency.php @@ -23,9 +23,6 @@ class ComposerAutoloaderInitFilesAutoloadOrder self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInitFilesAutoloadOrder', 'loadClassLoader')); - $vendorDir = dirname(__DIR__); - $baseDir = dirname($vendorDir); - $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); @@ -45,9 +42,14 @@ class ComposerAutoloaderInitFilesAutoloadOrder $includeFiles = require __DIR__ . '/autoload_files.php'; foreach ($includeFiles as $file) { - require $file; + composerRequireFilesAutoloadOrder($file); } return $loader; } } + +function composerRequireFilesAutoloadOrder($file) +{ + require $file; +} diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php index a92e664cd..1c0154964 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php @@ -23,9 +23,6 @@ class ComposerAutoloaderInitFilesAutoload self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInitFilesAutoload', 'loadClassLoader')); - $vendorDir = dirname(__DIR__); - $baseDir = dirname($vendorDir); - $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); @@ -45,9 +42,14 @@ class ComposerAutoloaderInitFilesAutoload $includeFiles = require __DIR__ . '/autoload_files.php'; foreach ($includeFiles as $file) { - require $file; + composerRequireFilesAutoload($file); } return $loader; } } + +function composerRequireFilesAutoload($file) +{ + require $file; +} diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_real_include_path.php b/tests/Composer/Test/Autoload/Fixtures/autoload_real_include_path.php index e72ea108a..65ba6819e 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_include_path.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_include_path.php @@ -23,9 +23,6 @@ class ComposerAutoloaderInitIncludePath self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInitIncludePath', 'loadClassLoader')); - $vendorDir = dirname(__DIR__); - $baseDir = dirname($vendorDir); - $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); @@ -67,3 +64,8 @@ class ComposerAutoloaderInitIncludePath } } } + +function composerRequireIncludePath($file) +{ + require $file; +} diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_real_target_dir.php b/tests/Composer/Test/Autoload/Fixtures/autoload_real_target_dir.php index 4a6259da2..dc786f767 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_target_dir.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_target_dir.php @@ -23,9 +23,6 @@ class ComposerAutoloaderInitTargetDir self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInitTargetDir', 'loadClassLoader')); - $vendorDir = dirname(__DIR__); - $baseDir = dirname($vendorDir); - $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); @@ -47,7 +44,7 @@ class ComposerAutoloaderInitTargetDir $includeFiles = require __DIR__ . '/autoload_files.php'; foreach ($includeFiles as $file) { - require $file; + composerRequireTargetDir($file); } return $loader; @@ -71,3 +68,8 @@ class ComposerAutoloaderInitTargetDir } } } + +function composerRequireTargetDir($file) +{ + require $file; +} diff --git a/tests/Composer/Test/Autoload/Fixtures/classmap/StripNoise.php b/tests/Composer/Test/Autoload/Fixtures/classmap/StripNoise.php index 3fe5389be..4c344089b 100644 --- a/tests/Composer/Test/Autoload/Fixtures/classmap/StripNoise.php +++ b/tests/Composer/Test/Autoload/Fixtures/classmap/StripNoise.php @@ -15,12 +15,30 @@ class Fail2 } A -. <<<'TEST' +. <<< AB class Fail3 { } -TEST; +AB +. <<<'TEST' +class Fail4 +{ + +} +TEST +. <<< 'ANOTHER' +class Fail5 +{ + +} +ANOTHER +. <<< 'ONEMORE' +class Fail6 +{ + +} +ONEMORE; } public function test2() diff --git a/tests/Composer/Test/Autoload/Fixtures/template/notphp.inc b/tests/Composer/Test/Autoload/Fixtures/template/notphp.inc new file mode 100644 index 000000000..2f1f8733a --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/template/notphp.inc @@ -0,0 +1,11 @@ +/** + * AJAX interface for remembering user preferences set on the fly + * + * Match /preferences/ajax/ + */ +'preferences_ajax' => array +( + 'handler' => array ('midgard_admin_asgard_handler_preferences', 'ajax'), + 'fixed_args' => array('preferences', 'ajax'), + 'variable_args' => 0, +), diff --git a/tests/Composer/Test/CacheTest.php b/tests/Composer/Test/CacheTest.php index 80c7de6bf..b01dbfd89 100644 --- a/tests/Composer/Test/CacheTest.php +++ b/tests/Composer/Test/CacheTest.php @@ -21,6 +21,10 @@ class CacheTest extends TestCase public function setUp() { + if (getenv('TRAVIS')) { + $this->markTestSkipped('Test causes intermittent failures on Travis'); + } + $this->root = sys_get_temp_dir() . '/composer_testdir'; $this->ensureDirectoryExistsAndClear($this->root); diff --git a/tests/Composer/Test/ComposerTest.php b/tests/Composer/Test/ComposerTest.php index f667e88e5..df00c36f7 100644 --- a/tests/Composer/Test/ComposerTest.php +++ b/tests/Composer/Test/ComposerTest.php @@ -47,7 +47,8 @@ class ComposerTest extends TestCase public function testSetGetDownloadManager() { $composer = new Composer(); - $manager = $this->getMock('Composer\Downloader\DownloadManager'); + $io = $this->getMock('Composer\IO\IOInterface'); + $manager = $this->getMock('Composer\Downloader\DownloadManager', array(), array($io)); $composer->setDownloadManager($manager); $this->assertSame($manager, $composer->getDownloadManager()); diff --git a/tests/Composer/Test/Config/JsonConfigSourceTest.php b/tests/Composer/Test/Config/JsonConfigSourceTest.php index d0b78f3e3..b7a3b592c 100644 --- a/tests/Composer/Test/Config/JsonConfigSourceTest.php +++ b/tests/Composer/Test/Config/JsonConfigSourceTest.php @@ -48,7 +48,6 @@ class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase $value, $this->fixturePath('addLink/'.$fixtureBasename.'.json'), ); - } /** diff --git a/tests/Composer/Test/ConfigTest.php b/tests/Composer/Test/ConfigTest.php index c2cf82ca7..e50af5e87 100644 --- a/tests/Composer/Test/ConfigTest.php +++ b/tests/Composer/Test/ConfigTest.php @@ -21,7 +21,7 @@ class ConfigTest extends \PHPUnit_Framework_TestCase */ public function testAddPackagistRepository($expected, $localConfig, $systemConfig = null) { - $config = new Config(); + $config = new Config(false); if ($systemConfig) { $config->merge(array('repositories' => $systemConfig)); } @@ -97,21 +97,74 @@ class ConfigTest extends \PHPUnit_Framework_TestCase ), ); + $data['incorrect local config does not cause ErrorException'] = array( + array( + 'packagist' => array('type' => 'composer', 'url' => 'https?://packagist.org', 'allow_ssl_downgrade' => true), + 'type' => 'vcs', + 'url' => 'http://example.com', + ), + array( + 'type' => 'vcs', + 'url' => 'http://example.com', + ), + ); + return $data; } public function testMergeGithubOauth() { - $config = new Config(); + $config = new Config(false); $config->merge(array('config' => array('github-oauth' => array('foo' => 'bar')))); $config->merge(array('config' => array('github-oauth' => array('bar' => 'baz')))); $this->assertEquals(array('foo' => 'bar', 'bar' => 'baz'), $config->get('github-oauth')); } + public function testVarReplacement() + { + $config = new Config(false); + $config->merge(array('config' => array('a' => 'b', 'c' => '{$a}'))); + $config->merge(array('config' => array('bin-dir' => '$HOME', 'cache-dir' => '~/foo/'))); + + $home = rtrim(getenv('HOME') ?: getenv('USERPROFILE'), '\\/'); + $this->assertEquals('b', $config->get('c')); + $this->assertEquals($home.'/', $config->get('bin-dir')); + $this->assertEquals($home.'/foo', $config->get('cache-dir')); + } + + public function testRealpathReplacement() + { + $config = new Config(false, '/foo/bar'); + $config->merge(array('config' => array( + 'bin-dir' => '$HOME/foo', + 'cache-dir' => '/baz/', + 'vendor-dir' => 'vendor' + ))); + + $home = rtrim(getenv('HOME') ?: getenv('USERPROFILE'), '\\/'); + $this->assertEquals('/foo/bar/vendor', $config->get('vendor-dir')); + $this->assertEquals($home.'/foo', $config->get('bin-dir')); + $this->assertEquals('/baz', $config->get('cache-dir')); + } + + public function testFetchingRelativePaths() + { + $config = new Config(false, '/foo/bar'); + $config->merge(array('config' => array( + 'bin-dir' => '{$vendor-dir}/foo', + 'vendor-dir' => 'vendor' + ))); + + $this->assertEquals('/foo/bar/vendor', $config->get('vendor-dir')); + $this->assertEquals('/foo/bar/vendor/foo', $config->get('bin-dir')); + $this->assertEquals('vendor', $config->get('vendor-dir', Config::RELATIVE_PATHS)); + $this->assertEquals('vendor/foo', $config->get('bin-dir', Config::RELATIVE_PATHS)); + } + public function testOverrideGithubProtocols() { - $config = new Config(); + $config = new Config(false); $config->merge(array('config' => array('github-protocols' => array('https', 'git')))); $config->merge(array('config' => array('github-protocols' => array('https')))); diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index 9e9952228..f06ba4437 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -247,4 +247,20 @@ class DefaultPolicyTest extends TestCase return $map; } + + public function testSelectLowest() + { + $policy = new DefaultPolicy(false, true); + + $this->repo->addPackage($packageA1 = $this->getPackage('A', '1.0')); + $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.0')); + $this->pool->addRepository($this->repo); + + $literals = array($packageA1->getId(), $packageA2->getId()); + $expected = array($packageA1->getId()); + + $selected = $policy->selectPreferedPackages($this->pool, array(), $literals); + + $this->assertEquals($expected, $selected); + } } diff --git a/tests/Composer/Test/DependencyResolver/RequestTest.php b/tests/Composer/Test/DependencyResolver/RequestTest.php index d8cb865a0..78a1da962 100644 --- a/tests/Composer/Test/DependencyResolver/RequestTest.php +++ b/tests/Composer/Test/DependencyResolver/RequestTest.php @@ -34,14 +34,14 @@ class RequestTest extends TestCase $request = new Request($pool); $request->install('foo'); - $request->install('bar'); + $request->fix('bar'); $request->remove('foobar'); $this->assertEquals( array( - array('packages' => array($foo), 'cmd' => 'install', 'packageName' => 'foo', 'constraint' => null), - array('packages' => array($bar), 'cmd' => 'install', 'packageName' => 'bar', 'constraint' => null), - array('packages' => array($foobar), 'cmd' => 'remove', 'packageName' => 'foobar', 'constraint' => null), + array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => null, 'fixed' => false), + array('cmd' => 'install', 'packageName' => 'bar', 'constraint' => null, 'fixed' => true), + array('cmd' => 'remove', 'packageName' => 'foobar', 'constraint' => null, 'fixed' => false), ), $request->getJobs()); } @@ -66,7 +66,7 @@ class RequestTest extends TestCase $this->assertEquals( array( - array('packages' => array($foo1, $foo2), 'cmd' => 'install', 'packageName' => 'foo', 'constraint' => $constraint), + array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => $constraint, 'fixed' => false), ), $request->getJobs() ); @@ -80,7 +80,7 @@ class RequestTest extends TestCase $request->updateAll(); $this->assertEquals( - array(array('cmd' => 'update-all', 'packages' => array())), + array(array('cmd' => 'update-all')), $request->getJobs()); } } diff --git a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php index 1598e7717..e83b79c2f 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php @@ -27,11 +27,11 @@ class RuleSetIteratorTest extends \PHPUnit_Framework_TestCase $this->rules = array( RuleSet::TYPE_JOB => array( - new Rule($this->pool, array(), 'job1', null), - new Rule($this->pool, array(), 'job2', null), + new Rule(array(), 'job1', null), + new Rule(array(), 'job2', null), ), RuleSet::TYPE_LEARNED => array( - new Rule($this->pool, array(), 'update1', null), + new Rule(array(), 'update1', null), ), RuleSet::TYPE_PACKAGE => array(), ); diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index 35e3f17d6..c1eb83e5e 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -32,11 +32,11 @@ class RuleSetTest extends TestCase $rules = array( RuleSet::TYPE_PACKAGE => array(), RuleSet::TYPE_JOB => array( - new Rule($this->pool, array(), 'job1', null), - new Rule($this->pool, array(), 'job2', null), + new Rule(array(), 'job1', null), + new Rule(array(), 'job2', null), ), RuleSet::TYPE_LEARNED => array( - new Rule($this->pool, array(), 'update1', null), + new Rule(array(), 'update1', null), ), ); @@ -56,15 +56,15 @@ class RuleSetTest extends TestCase { $ruleSet = new RuleSet; - $ruleSet->add(new Rule($this->pool, array(), 'job1', null), 7); + $ruleSet->add(new Rule(array(), 'job1', null), 7); } public function testCount() { $ruleSet = new RuleSet; - $ruleSet->add(new Rule($this->pool, array(), 'job1', null), RuleSet::TYPE_JOB); - $ruleSet->add(new Rule($this->pool, array(), 'job2', null), RuleSet::TYPE_JOB); + $ruleSet->add(new Rule(array(), 'job1', null), RuleSet::TYPE_JOB); + $ruleSet->add(new Rule(array(), 'job2', null), RuleSet::TYPE_JOB); $this->assertEquals(2, $ruleSet->count()); } @@ -73,18 +73,18 @@ class RuleSetTest extends TestCase { $ruleSet = new RuleSet; - $rule = new Rule($this->pool, array(), 'job1', null); + $rule = new Rule(array(), 'job1', null); $ruleSet->add($rule, RuleSet::TYPE_JOB); - $this->assertSame($rule, $ruleSet->ruleById(0)); + $this->assertSame($rule, $ruleSet->ruleById[0]); } public function testGetIterator() { $ruleSet = new RuleSet; - $rule1 = new Rule($this->pool, array(), 'job1', null); - $rule2 = new Rule($this->pool, array(), 'job1', null); + $rule1 = new Rule(array(), 'job1', null); + $rule2 = new Rule(array(), 'job1', null); $ruleSet->add($rule1, RuleSet::TYPE_JOB); $ruleSet->add($rule2, RuleSet::TYPE_LEARNED); @@ -98,8 +98,8 @@ class RuleSetTest extends TestCase public function testGetIteratorFor() { $ruleSet = new RuleSet; - $rule1 = new Rule($this->pool, array(), 'job1', null); - $rule2 = new Rule($this->pool, array(), 'job1', null); + $rule1 = new Rule(array(), 'job1', null); + $rule2 = new Rule(array(), 'job1', null); $ruleSet->add($rule1, RuleSet::TYPE_JOB); $ruleSet->add($rule2, RuleSet::TYPE_LEARNED); @@ -112,8 +112,8 @@ class RuleSetTest extends TestCase public function testGetIteratorWithout() { $ruleSet = new RuleSet; - $rule1 = new Rule($this->pool, array(), 'job1', null); - $rule2 = new Rule($this->pool, array(), 'job1', null); + $rule1 = new Rule(array(), 'job1', null); + $rule2 = new Rule(array(), 'job1', null); $ruleSet->add($rule1, RuleSet::TYPE_JOB); $ruleSet->add($rule2, RuleSet::TYPE_LEARNED); @@ -155,7 +155,7 @@ class RuleSetTest extends TestCase $this->assertFalse($ruleSet->containsEqual($rule3)); } - public function testToString() + public function testPrettyString() { $repo = new ArrayRepository; $repo->addPackage($p = $this->getPackage('foo', '2.1')); @@ -163,11 +163,11 @@ class RuleSetTest extends TestCase $ruleSet = new RuleSet; $literal = $p->getId(); - $rule = new Rule($this->pool, array($literal), 'job1', null); + $rule = new Rule(array($literal), 'job1', null); $ruleSet->add($rule, RuleSet::TYPE_JOB); - $this->assertContains('JOB : (+foo-2.1.0.0)', $ruleSet->__toString()); + $this->assertContains('JOB : (install foo 2.1)', $ruleSet->getPrettyString($this->pool)); } private function getRuleMock() diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index 10667632d..6688b24aa 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -28,14 +28,14 @@ class RuleTest extends TestCase public function testGetHash() { - $rule = new Rule($this->pool, array(123), 'job1', null); + $rule = new Rule(array(123), 'job1', null); $this->assertEquals(substr(md5('123'), 0, 5), $rule->getHash()); } public function testSetAndGetId() { - $rule = new Rule($this->pool, array(), 'job1', null); + $rule = new Rule(array(), 'job1', null); $rule->setId(666); $this->assertEquals(666, $rule->getId()); @@ -43,31 +43,31 @@ class RuleTest extends TestCase public function testEqualsForRulesWithDifferentHashes() { - $rule = new Rule($this->pool, array(1, 2), 'job1', null); - $rule2 = new Rule($this->pool, array(1, 3), 'job1', null); + $rule = new Rule(array(1, 2), 'job1', null); + $rule2 = new Rule(array(1, 3), 'job1', null); $this->assertFalse($rule->equals($rule2)); } public function testEqualsForRulesWithDifferLiteralsQuantity() { - $rule = new Rule($this->pool, array(1, 12), 'job1', null); - $rule2 = new Rule($this->pool, array(1), 'job1', null); + $rule = new Rule(array(1, 12), 'job1', null); + $rule2 = new Rule(array(1), 'job1', null); $this->assertFalse($rule->equals($rule2)); } public function testEqualsForRulesWithSameLiterals() { - $rule = new Rule($this->pool, array(1, 12), 'job1', null); - $rule2 = new Rule($this->pool, array(1, 12), 'job1', null); + $rule = new Rule(array(1, 12), 'job1', null); + $rule2 = new Rule(array(1, 12), 'job1', null); $this->assertTrue($rule->equals($rule2)); } public function testSetAndGetType() { - $rule = new Rule($this->pool, array(), 'job1', null); + $rule = new Rule(array(), 'job1', null); $rule->setType('someType'); $this->assertEquals('someType', $rule->getType()); @@ -75,7 +75,7 @@ class RuleTest extends TestCase public function testEnable() { - $rule = new Rule($this->pool, array(), 'job1', null); + $rule = new Rule(array(), 'job1', null); $rule->disable(); $rule->enable(); @@ -85,7 +85,7 @@ class RuleTest extends TestCase public function testDisable() { - $rule = new Rule($this->pool, array(), 'job1', null); + $rule = new Rule(array(), 'job1', null); $rule->enable(); $rule->disable(); @@ -95,22 +95,22 @@ class RuleTest extends TestCase public function testIsAssertions() { - $rule = new Rule($this->pool, array(1, 12), 'job1', null); - $rule2 = new Rule($this->pool, array(1), 'job1', null); + $rule = new Rule(array(1, 12), 'job1', null); + $rule2 = new Rule(array(1), 'job1', null); $this->assertFalse($rule->isAssertion()); $this->assertTrue($rule2->isAssertion()); } - public function testToString() + public function testPrettyString() { $repo = new ArrayRepository; $repo->addPackage($p1 = $this->getPackage('foo', '2.1')); $repo->addPackage($p2 = $this->getPackage('baz', '1.1')); $this->pool->addRepository($repo); - $rule = new Rule($this->pool, array($p1->getId(), -$p2->getId()), 'job1', null); + $rule = new Rule(array($p1->getId(), -$p2->getId()), 'job1', null); - $this->assertEquals('(-baz-1.1.0.0|+foo-2.1.0.0)', $rule->__toString()); + $this->assertEquals('(don\'t install baz 1.1|install foo 2.1)', $rule->getPrettyString($this->pool)); } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 349f6e3b4..d3b540f47 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -441,10 +441,9 @@ class SolverTest extends TestCase $this->request->install('A'); - $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageQ), - array('job' => 'install', 'package' => $packageA), - )); + // must explicitly pick the provider, so error in this case + $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); + $this->solver->solve($this->request); } public function testSkipReplacerOfExistingPackage() @@ -465,7 +464,7 @@ class SolverTest extends TestCase )); } - public function testInstallReplacerOfMissingPackage() + public function testNoInstallReplacerOfMissingPackage() { $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageQ = $this->getPackage('Q', '1.0')); @@ -476,10 +475,8 @@ class SolverTest extends TestCase $this->request->install('A'); - $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageQ), - array('job' => 'install', 'package' => $packageA), - )); + $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); + $this->solver->solve($this->request); } public function testSkipReplacedPackageIfReplacerIsSelected() @@ -574,11 +571,12 @@ class SolverTest extends TestCase $this->reposComplete(); $this->request->install('A'); + $this->request->install('C'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageB), array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageC), + array('job' => 'install', 'package' => $packageB), )); } @@ -611,6 +609,7 @@ class SolverTest extends TestCase $this->reposComplete(); $this->request->install('A'); + $this->request->install('D'); $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageD2), @@ -655,8 +654,7 @@ class SolverTest extends TestCase public function testConflictResultEmpty() { $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));; - + $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); $packageA->setConflicts(array( 'b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'conflicts'), )); diff --git a/tests/Composer/Test/Downloader/DownloadManagerTest.php b/tests/Composer/Test/Downloader/DownloadManagerTest.php index 29b2edf90..1728c583e 100644 --- a/tests/Composer/Test/Downloader/DownloadManagerTest.php +++ b/tests/Composer/Test/Downloader/DownloadManagerTest.php @@ -17,16 +17,18 @@ use Composer\Downloader\DownloadManager; class DownloadManagerTest extends \PHPUnit_Framework_TestCase { protected $filesystem; + protected $io; public function setUp() { $this->filesystem = $this->getMock('Composer\Util\Filesystem'); + $this->io = $this->getMock('Composer\IO\IOInterface'); } public function testSetGetDownloader() { $downloader = $this->createDownloaderMock(); - $manager = new DownloadManager(false, $this->filesystem); + $manager = new DownloadManager($this->io, false, $this->filesystem); $manager->setDownloader('test', $downloader); $this->assertSame($downloader, $manager->getDownloader('test')); @@ -43,7 +45,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->method('getInstallationSource') ->will($this->returnValue(null)); - $manager = new DownloadManager(false, $this->filesystem); + $manager = new DownloadManager($this->io, false, $this->filesystem); $this->setExpectedException('InvalidArgumentException'); @@ -69,7 +71,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('dist')); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloader')) ->getMock(); @@ -101,7 +103,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('source')); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloader')) ->getMock(); @@ -135,7 +137,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('source')); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloader')) ->getMock(); @@ -167,7 +169,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('dist')); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloader')) ->getMock(); @@ -182,6 +184,19 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase $manager->getDownloaderForInstalledPackage($package); } + public function testGetDownloaderForMetapackage() + { + $package = $this->createPackageMock(); + $package + ->expects($this->once()) + ->method('getType') + ->will($this->returnValue('metapackage')); + + $manager = new DownloadManager($this->io, false, $this->filesystem); + + $this->assertNull($manager->getDownloaderForInstalledPackage($package)); + } + public function testFullPackageDownload() { $package = $this->createPackageMock(); @@ -206,7 +221,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->with($package, 'target_dir'); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloaderForInstalledPackage')) ->getMock(); $manager @@ -218,6 +233,62 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase $manager->download($package, 'target_dir'); } + public function testFullPackageDownloadFailover() + { + $package = $this->createPackageMock(); + $package + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue('git')); + $package + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue('pear')); + $package + ->expects($this->any()) + ->method('getPrettyString') + ->will($this->returnValue('prettyPackage')); + + $package + ->expects($this->at(3)) + ->method('setInstallationSource') + ->with('dist'); + $package + ->expects($this->at(5)) + ->method('setInstallationSource') + ->with('source'); + + $downloaderFail = $this->createDownloaderMock(); + $downloaderFail + ->expects($this->once()) + ->method('download') + ->with($package, 'target_dir') + ->will($this->throwException(new \RuntimeException("Foo"))); + + $downloaderSuccess = $this->createDownloaderMock(); + $downloaderSuccess + ->expects($this->once()) + ->method('download') + ->with($package, 'target_dir'); + + $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') + ->setConstructorArgs(array($this->io, false, $this->filesystem)) + ->setMethods(array('getDownloaderForInstalledPackage')) + ->getMock(); + $manager + ->expects($this->at(0)) + ->method('getDownloaderForInstalledPackage') + ->with($package) + ->will($this->returnValue($downloaderFail)); + $manager + ->expects($this->at(1)) + ->method('getDownloaderForInstalledPackage') + ->with($package) + ->will($this->returnValue($downloaderSuccess)); + + $manager->download($package, 'target_dir'); + } + public function testBadPackageDownload() { $package = $this->createPackageMock(); @@ -230,7 +301,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->method('getDistType') ->will($this->returnValue(null)); - $manager = new DownloadManager(false, $this->filesystem); + $manager = new DownloadManager($this->io, false, $this->filesystem); $this->setExpectedException('InvalidArgumentException'); $manager->download($package, 'target_dir'); @@ -260,7 +331,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->with($package, 'target_dir'); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloaderForInstalledPackage')) ->getMock(); $manager @@ -296,7 +367,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->with($package, 'target_dir'); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloaderForInstalledPackage')) ->getMock(); $manager @@ -308,6 +379,36 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase $manager->download($package, 'target_dir'); } + public function testMetapackagePackageDownload() + { + $package = $this->createPackageMock(); + $package + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue('git')); + $package + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue(null)); + + $package + ->expects($this->once()) + ->method('setInstallationSource') + ->with('source'); + + $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') + ->setConstructorArgs(array($this->io, false, $this->filesystem)) + ->setMethods(array('getDownloaderForInstalledPackage')) + ->getMock(); + $manager + ->expects($this->once()) + ->method('getDownloaderForInstalledPackage') + ->with($package) + ->will($this->returnValue(null)); // There is no downloader for Metapackages. + + $manager->download($package, 'target_dir'); + } + public function testFullPackageDownloadWithSourcePreferred() { $package = $this->createPackageMock(); @@ -332,7 +433,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->with($package, 'target_dir'); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloaderForInstalledPackage')) ->getMock(); $manager @@ -369,7 +470,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->with($package, 'target_dir'); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloaderForInstalledPackage')) ->getMock(); $manager @@ -406,7 +507,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->with($package, 'target_dir'); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloaderForInstalledPackage')) ->getMock(); $manager @@ -431,7 +532,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->method('getDistType') ->will($this->returnValue(null)); - $manager = new DownloadManager(false, $this->filesystem); + $manager = new DownloadManager($this->io, false, $this->filesystem); $manager->setPreferSource(true); $this->setExpectedException('InvalidArgumentException'); @@ -467,7 +568,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->with($initial, $target, 'vendor/bundles/FOS/UserBundle'); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloaderForInstalledPackage')) ->getMock(); $manager @@ -504,7 +605,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->with($initial, 'vendor/bundles/FOS/UserBundle'); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloaderForInstalledPackage', 'download')) ->getMock(); $manager @@ -545,7 +646,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->with($initial, $target, 'vendor/pkg'); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloaderForInstalledPackage', 'download')) ->getMock(); $manager @@ -582,7 +683,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->with($initial, 'vendor/pkg'); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloaderForInstalledPackage', 'download')) ->getMock(); $manager @@ -598,6 +699,24 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase $manager->update($initial, $target, 'vendor/pkg'); } + public function testUpdateMetapackage() + { + $initial = $this->createPackageMock(); + $target = $this->createPackageMock(); + + $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') + ->setConstructorArgs(array($this->io, false, $this->filesystem)) + ->setMethods(array('getDownloaderForInstalledPackage')) + ->getMock(); + $manager + ->expects($this->once()) + ->method('getDownloaderForInstalledPackage') + ->with($initial) + ->will($this->returnValue(null)); // There is no downloader for metapackages. + + $manager->update($initial, $target, 'vendor/pkg'); + } + public function testRemove() { $package = $this->createPackageMock(); @@ -609,7 +728,7 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase ->with($package, 'vendor/bundles/FOS/UserBundle'); $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->setConstructorArgs(array(false, $this->filesystem)) + ->setConstructorArgs(array($this->io, false, $this->filesystem)) ->setMethods(array('getDownloaderForInstalledPackage')) ->getMock(); $manager @@ -621,6 +740,23 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase $manager->remove($package, 'vendor/bundles/FOS/UserBundle'); } + public function testMetapackageRemove() + { + $package = $this->createPackageMock(); + + $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') + ->setConstructorArgs(array($this->io, false, $this->filesystem)) + ->setMethods(array('getDownloaderForInstalledPackage')) + ->getMock(); + $manager + ->expects($this->once()) + ->method('getDownloaderForInstalledPackage') + ->with($package) + ->will($this->returnValue(null)); // There is no downloader for metapackages. + + $manager->remove($package, 'vendor/bundles/FOS/UserBundle'); + } + private function createDownloaderMock() { return $this->getMockBuilder('Composer\Downloader\DownloaderInterface') diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index db6f81a5e..f0578f6be 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -48,6 +48,10 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase ->method('getDistUrl') ->will($this->returnValue('url')) ; + $packageMock->expects($this->once()) + ->method('getDistUrls') + ->will($this->returnValue(array('url'))) + ; $path = tempnam(sys_get_temp_dir(), 'c'); @@ -87,7 +91,15 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase $packageMock = $this->getMock('Composer\Package\PackageInterface'); $packageMock->expects($this->any()) ->method('getDistUrl') - ->will($this->returnValue('http://example.com/script.js')) + ->will($this->returnValue($distUrl = 'http://example.com/script.js')) + ; + $packageMock->expects($this->once()) + ->method('getDistUrls') + ->will($this->returnValue(array($distUrl))) + ; + $packageMock->expects($this->atLeastOnce()) + ->method('getTransportOptions') + ->will($this->returnValue(array())) ; do { @@ -97,7 +109,7 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase $ioMock = $this->getMock('Composer\IO\IOInterface'); $ioMock->expects($this->any()) ->method('write') - ->will($this->returnCallback(function($messages, $newline = true) use ($path) { + ->will($this->returnCallback(function ($messages, $newline = true) use ($path) { if (is_file($path.'/script.js')) { unlink($path.'/script.js'); } @@ -159,12 +171,20 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase $packageMock = $this->getMock('Composer\Package\PackageInterface'); $packageMock->expects($this->any()) ->method('getDistUrl') - ->will($this->returnValue('http://example.com/script.js')) + ->will($this->returnValue($distUrl = 'http://example.com/script.js')) + ; + $packageMock->expects($this->atLeastOnce()) + ->method('getTransportOptions') + ->will($this->returnValue(array())) ; $packageMock->expects($this->any()) ->method('getDistSha1Checksum') ->will($this->returnValue('invalid')) ; + $packageMock->expects($this->once()) + ->method('getDistUrls') + ->will($this->returnValue(array($distUrl))) + ; $filesystem = $this->getMock('Composer\Util\Filesystem'); do { diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 7a0816eaf..05cc8f30e 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -50,6 +50,9 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase $packageMock->expects($this->any()) ->method('getSourceReference') ->will($this->returnValue('1234567890123456789012345678901234567890')); + $packageMock->expects($this->any()) + ->method('getSourceUrls') + ->will($this->returnValue(array('https://example.com/composer/composer'))); $packageMock->expects($this->any()) ->method('getSourceUrl') ->will($this->returnValue('https://example.com/composer/composer')); @@ -58,7 +61,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('dev-master')); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); - $expectedGitCommand = $this->winCompat("git clone 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer"); $processExecutor->expects($this->at(0)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) @@ -89,6 +92,9 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase $packageMock->expects($this->any()) ->method('getSourceReference') ->will($this->returnValue('ref')); + $packageMock->expects($this->any()) + ->method('getSourceUrls') + ->will($this->returnValue(array('https://github.com/mirrors/composer', 'https://github.com/composer/composer'))); $packageMock->expects($this->any()) ->method('getSourceUrl') ->will($this->returnValue('https://github.com/composer/composer')); @@ -97,30 +103,36 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('1.0.0')); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); - $expectedGitCommand = $this->winCompat("git clone 'git://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'git://github.com/composer/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone --no-checkout 'git://github.com/mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'git://github.com/mirrors/composer' && git fetch composer"); $processExecutor->expects($this->at(0)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) ->will($this->returnValue(1)); - $expectedGitCommand = $this->winCompat("git clone 'https://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://github.com/composer/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://github.com/mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://github.com/mirrors/composer' && git fetch composer"); $processExecutor->expects($this->at(2)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) ->will($this->returnValue(0)); - $expectedGitCommand = $this->winCompat("git remote set-url --push origin 'git@github.com:composer/composer.git'"); + $expectedGitCommand = $this->winCompat("git remote set-url origin 'https://github.com/composer/composer'"); $processExecutor->expects($this->at(3)) ->method('execute') ->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); + $expectedGitCommand = $this->winCompat("git remote set-url --push origin 'git@github.com:composer/composer.git'"); $processExecutor->expects($this->at(4)) ->method('execute') - ->with($this->equalTo('git branch -r')) + ->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(5)) + ->method('execute') + ->with($this->equalTo('git branch -r')) + ->will($this->returnValue(0)); + + $processExecutor->expects($this->at(6)) ->method('execute') ->with($this->equalTo($this->winCompat("git checkout 'ref' && git reset --hard 'ref'")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); @@ -146,6 +158,9 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase $packageMock->expects($this->any()) ->method('getSourceReference') ->will($this->returnValue('ref')); + $packageMock->expects($this->any()) + ->method('getSourceUrls') + ->will($this->returnValue(array('https://github.com/composer/composer'))); $packageMock->expects($this->any()) ->method('getSourceUrl') ->will($this->returnValue('https://github.com/composer/composer')); @@ -154,7 +169,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('1.0.0')); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); - $expectedGitCommand = $this->winCompat("git clone '{$protocol}://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer '{$protocol}://github.com/composer/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone --no-checkout '{$protocol}://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer '{$protocol}://github.com/composer/composer' && git fetch composer"); $processExecutor->expects($this->at(0)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) @@ -182,14 +197,14 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase */ public function testDownloadThrowsRuntimeExceptionIfGitCommandFails() { - $expectedGitCommand = $this->winCompat("git clone 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone --no-checkout 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer"); $packageMock = $this->getMock('Composer\Package\PackageInterface'); $packageMock->expects($this->any()) ->method('getSourceReference') ->will($this->returnValue('ref')); $packageMock->expects($this->any()) - ->method('getSourceUrl') - ->will($this->returnValue('https://example.com/composer/composer')); + ->method('getSourceUrls') + ->will($this->returnValue(array('https://example.com/composer/composer'))); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); $processExecutor->expects($this->at(0)) ->method('execute') @@ -227,8 +242,8 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->method('getSourceReference') ->will($this->returnValue('ref')); $packageMock->expects($this->any()) - ->method('getSourceUrl') - ->will($this->returnValue('https://github.com/composer/composer')); + ->method('getSourceUrls') + ->will($this->returnValue(array('https://github.com/composer/composer'))); $packageMock->expects($this->any()) ->method('getPrettyVersion') ->will($this->returnValue('1.0.0')); @@ -273,8 +288,8 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->method('getSourceReference') ->will($this->returnValue('ref')); $packageMock->expects($this->any()) - ->method('getSourceUrl') - ->will($this->returnValue('https://github.com/composer/composer')); + ->method('getSourceUrls') + ->will($this->returnValue(array('https://github.com/composer/composer'))); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); $processExecutor->expects($this->at(0)) ->method('execute') diff --git a/tests/Composer/Test/Downloader/HgDownloaderTest.php b/tests/Composer/Test/Downloader/HgDownloaderTest.php index 37a895172..ab9ec28cd 100644 --- a/tests/Composer/Test/Downloader/HgDownloaderTest.php +++ b/tests/Composer/Test/Downloader/HgDownloaderTest.php @@ -48,8 +48,8 @@ class HgDownloaderTest extends \PHPUnit_Framework_TestCase ->method('getSourceReference') ->will($this->returnValue('ref')); $packageMock->expects($this->once()) - ->method('getSourceUrl') - ->will($this->returnValue('https://mercurial.dev/l3l0/composer')); + ->method('getSourceUrls') + ->will($this->returnValue(array('https://mercurial.dev/l3l0/composer'))); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); $expectedGitCommand = $this->getCmd('hg clone \'https://mercurial.dev/l3l0/composer\' \'composerPath\''); @@ -93,8 +93,8 @@ class HgDownloaderTest extends \PHPUnit_Framework_TestCase ->method('getSourceReference') ->will($this->returnValue('ref')); $packageMock->expects($this->any()) - ->method('getSourceUrl') - ->will($this->returnValue('https://github.com/l3l0/composer')); + ->method('getSourceUrls') + ->will($this->returnValue(array('https://github.com/l3l0/composer'))); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); $expectedHgCommand = $this->getCmd("hg st"); diff --git a/tests/Composer/Test/Downloader/PearPackageExtractorTest.php b/tests/Composer/Test/Downloader/PearPackageExtractorTest.php index fa393833d..c053976d7 100644 --- a/tests/Composer/Test/Downloader/PearPackageExtractorTest.php +++ b/tests/Composer/Test/Downloader/PearPackageExtractorTest.php @@ -25,19 +25,19 @@ class PearPackageExtractorTest extends \PHPUnit_Framework_TestCase $fileActions = $method->invoke($extractor, __DIR__ . '/Fixtures/Package_v1.0', array('php' => '/'), array()); $expectedFileActions = array( - 'Gtk.php' => Array( + 'Gtk.php' => array( 'from' => 'PEAR_Frontend_Gtk-0.4.0/Gtk.php', 'to' => 'PEAR/Frontend/Gtk.php', 'role' => 'php', 'tasks' => array(), ), - 'Gtk/Config.php' => Array( + 'Gtk/Config.php' => array( 'from' => 'PEAR_Frontend_Gtk-0.4.0/Gtk/Config.php', 'to' => 'PEAR/Frontend/Gtk/Config.php', 'role' => 'php', 'tasks' => array(), ), - 'Gtk/xpm/black_close_icon.xpm' => Array( + 'Gtk/xpm/black_close_icon.xpm' => array( 'from' => 'PEAR_Frontend_Gtk-0.4.0/Gtk/xpm/black_close_icon.xpm', 'to' => 'PEAR/Frontend/Gtk/xpm/black_close_icon.xpm', 'role' => 'php', @@ -56,7 +56,7 @@ class PearPackageExtractorTest extends \PHPUnit_Framework_TestCase $fileActions = $method->invoke($extractor, __DIR__ . '/Fixtures/Package_v2.0', array('php' => '/'), array()); $expectedFileActions = array( - 'URL.php' => Array( + 'URL.php' => array( 'from' => 'Net_URL-1.0.15/URL.php', 'to' => 'Net/URL.php', 'role' => 'php', @@ -75,13 +75,13 @@ class PearPackageExtractorTest extends \PHPUnit_Framework_TestCase $fileActions = $method->invoke($extractor, __DIR__ . '/Fixtures/Package_v2.1', array('php' => '/', 'script' => '/bin'), array()); $expectedFileActions = array( - 'php/Zend/Authentication/Storage/StorageInterface.php' => Array( + 'php/Zend/Authentication/Storage/StorageInterface.php' => array( 'from' => 'Zend_Authentication-2.0.0beta4/php/Zend/Authentication/Storage/StorageInterface.php', 'to' => '/php/Zend/Authentication/Storage/StorageInterface.php', 'role' => 'php', 'tasks' => array(), ), - 'php/Zend/Authentication/Result.php' => Array( + 'php/Zend/Authentication/Result.php' => array( 'from' => 'Zend_Authentication-2.0.0beta4/php/Zend/Authentication/Result.php', 'to' => '/php/Zend/Authentication/Result.php', 'role' => 'php', @@ -98,7 +98,7 @@ class PearPackageExtractorTest extends \PHPUnit_Framework_TestCase ) ) ), - 'renamedFile.php' => Array( + 'renamedFile.php' => array( 'from' => 'Zend_Authentication-2.0.0beta4/renamedFile.php', 'to' => 'correctFile.php', 'role' => 'php', diff --git a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php index 8a20f67cc..3e2bce2b4 100644 --- a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php +++ b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php @@ -15,86 +15,145 @@ namespace Composer\Test\Downloader; use Composer\Downloader\PerforceDownloader; use Composer\Config; use Composer\Repository\VcsRepository; +use Composer\IO\IOInterface; /** * @author Matt Whittom */ class PerforceDownloaderTest extends \PHPUnit_Framework_TestCase { - private $io; - private $config; - private $testPath; - public static $repository; + protected $config; + protected $downloader; + protected $io; + protected $package; + protected $processExecutor; + protected $repoConfig; + protected $repository; + protected $testPath; protected function setUp() { - $this->testPath = sys_get_temp_dir() . '/composer-test'; - $this->config = new Config(); - $this->config->merge( - array( - 'config' => array( - 'home' => $this->testPath, - ), - ) - ); - $this->io = $this->getMock('Composer\IO\IOInterface'); + $this->testPath = sys_get_temp_dir() . '/composer-test'; + $this->repoConfig = $this->getRepoConfig(); + $this->config = $this->getConfig(); + $this->io = $this->getMockIoInterface(); + $this->processExecutor = $this->getMockProcessExecutor(); + $this->repository = $this->getMockRepository($this->repoConfig, $this->io, $this->config); + $this->package = $this->getMockPackageInterface($this->repository); + $this->downloader = new PerforceDownloader($this->io, $this->config, $this->processExecutor); } - public function testInitPerforceGetRepoConfig() + protected function tearDown() + { + $this->downloader = null; + $this->package = null; + $this->repository = null; + $this->io = null; + $this->config = null; + $this->repoConfig = null; + $this->testPath = null; + } + + protected function getMockProcessExecutor() + { + return $this->getMock('Composer\Util\ProcessExecutor'); + } + + protected function getConfig() + { + $config = new Config(); + $settings = array('config' => array('home' => $this->testPath)); + $config->merge($settings); + + return $config; + } + + protected function getMockIoInterface() + { + return $this->getMock('Composer\IO\IOInterface'); + } + + protected function getMockPackageInterface(VcsRepository $repository) { - $downloader = new PerforceDownloader($this->io, $this->config); $package = $this->getMock('Composer\Package\PackageInterface'); - $repoConfig = array('url' => 'TEST_URL', 'p4user' => 'TEST_USER'); - $repository = $this->getMock( - 'Composer\Repository\VcsRepository', - array('getRepoConfig'), - array($repoConfig, $this->io, $this->config) - ); - $package->expects($this->at(0)) - ->method('getRepository') - ->will($this->returnValue($repository)); - $repository->expects($this->at(0)) - ->method('getRepoConfig'); - $path = $this->testPath; - $downloader->initPerforce($package, $path, 'SOURCE_REF'); + $package->expects($this->any())->method('getRepository')->will($this->returnValue($repository)); + + return $package; } - public function testDoDownload() + protected function getRepoConfig() + { + return array('url' => 'TEST_URL', 'p4user' => 'TEST_USER'); + } + + protected function getMockRepository(array $repoConfig, IOInterface $io, Config $config) + { + $class = 'Composer\Repository\VcsRepository'; + $methods = array('getRepoConfig'); + $args = array($repoConfig, $io, $config); + $repository = $this->getMock($class, $methods, $args); + $repository->expects($this->any())->method('getRepoConfig')->will($this->returnValue($repoConfig)); + + return $repository; + } + + public function testInitPerforceInstantiatesANewPerforceObject() + { + $this->downloader->initPerforce($this->package, $this->testPath, 'SOURCE_REF'); + } + + public function testInitPerforceDoesNothingIfPerforceAlreadySet() + { + $perforce = $this->getMockBuilder('Composer\Util\Perforce')->disableOriginalConstructor()->getMock(); + $this->downloader->setPerforce($perforce); + $this->repository->expects($this->never())->method('getRepoConfig'); + $this->downloader->initPerforce($this->package, $this->testPath, 'SOURCE_REF'); + } + + /** + * @depends testInitPerforceInstantiatesANewPerforceObject + * @depends testInitPerforceDoesNothingIfPerforceAlreadySet + */ + public function testDoDownloadWithTag() + { + //I really don't like this test but the logic of each Perforce method is tested in the Perforce class. Really I am just enforcing workflow. + $ref = 'SOURCE_REF@123'; + $label = 123; + $this->package->expects($this->once())->method('getSourceReference')->will($this->returnValue($ref)); + $this->io->expects($this->once())->method('write')->with($this->stringContains('Cloning '.$ref)); + $perforceMethods = array('setStream', 'p4Login', 'writeP4ClientSpec', 'connectClient', 'syncCodeBase', 'cleanupClientSpec'); + $perforce = $this->getMockBuilder('Composer\Util\Perforce', $perforceMethods)->disableOriginalConstructor()->getMock(); + $perforce->expects($this->at(0))->method('initializePath')->with($this->equalTo($this->testPath)); + $perforce->expects($this->at(1))->method('setStream')->with($this->equalTo($ref)); + $perforce->expects($this->at(2))->method('p4Login')->with($this->identicalTo($this->io)); + $perforce->expects($this->at(3))->method('writeP4ClientSpec'); + $perforce->expects($this->at(4))->method('connectClient'); + $perforce->expects($this->at(5))->method('syncCodeBase')->with($label); + $perforce->expects($this->at(6))->method('cleanupClientSpec'); + $this->downloader->setPerforce($perforce); + $this->downloader->doDownload($this->package, $this->testPath, 'url'); + } + + /** + * @depends testInitPerforceInstantiatesANewPerforceObject + * @depends testInitPerforceDoesNothingIfPerforceAlreadySet + */ + public function testDoDownloadWithNoTag() { - $downloader = new PerforceDownloader($this->io, $this->config); - $repoConfig = array('depot' => 'TEST_DEPOT', 'branch' => 'TEST_BRANCH', 'p4user' => 'TEST_USER'); - $port = 'TEST_PORT'; - $path = 'TEST_PATH'; - $process = $this->getmock('Composer\Util\ProcessExecutor'); - $perforce = $this->getMock( - 'Composer\Util\Perforce', - array('setStream', 'queryP4User', 'writeP4ClientSpec', 'connectClient', 'syncCodeBase'), - array($repoConfig, $port, $path, $process, true, 'TEST') - ); $ref = 'SOURCE_REF'; - $label = 'LABEL'; - $perforce->expects($this->at(0)) - ->method('setStream') - ->with($this->equalTo($ref)); - $perforce->expects($this->at(1)) - ->method('queryP4User') - ->with($this->io); - $perforce->expects($this->at(2)) - ->method('writeP4ClientSpec'); - $perforce->expects($this->at(3)) - ->method('connectClient'); - $perforce->expects($this->at(4)) - ->method('syncCodeBase') - ->with($this->equalTo($label)); - $downloader->setPerforce($perforce); - $package = $this->getMock('Composer\Package\PackageInterface'); - $package->expects($this->at(0)) - ->method('getSourceReference') - ->will($this->returnValue($ref)); - $package->expects($this->at(1)) - ->method('getPrettyVersion') - ->will($this->returnValue($label)); - $path = $this->testPath; - $downloader->doDownload($package, $path); + $label = null; + $this->package->expects($this->once())->method('getSourceReference')->will($this->returnValue($ref)); + $this->io->expects($this->once())->method('write')->with($this->stringContains('Cloning '.$ref)); + $perforceMethods = array('setStream', 'p4Login', 'writeP4ClientSpec', 'connectClient', 'syncCodeBase', 'cleanupClientSpec'); + $perforce = $this->getMockBuilder('Composer\Util\Perforce', $perforceMethods)->disableOriginalConstructor()->getMock(); + $perforce->expects($this->at(0))->method('initializePath')->with($this->equalTo($this->testPath)); + $perforce->expects($this->at(1))->method('setStream')->with($this->equalTo($ref)); + $perforce->expects($this->at(2))->method('p4Login')->with($this->identicalTo($this->io)); + $perforce->expects($this->at(3))->method('writeP4ClientSpec'); + $perforce->expects($this->at(4))->method('connectClient'); + $perforce->expects($this->at(5))->method('syncCodeBase')->with($label); + $perforce->expects($this->at(6))->method('cleanupClientSpec'); + $this->downloader->setPerforce($perforce); + $this->downloader->doDownload($this->package, $this->testPath, 'url'); } } diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index bbe77d7ee..58e0078b0 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -28,7 +28,15 @@ class ZipDownloaderTest extends \PHPUnit_Framework_TestCase $packageMock = $this->getMock('Composer\Package\PackageInterface'); $packageMock->expects($this->any()) ->method('getDistUrl') - ->will($this->returnValue('file://'.__FILE__)) + ->will($this->returnValue($distUrl = 'file://'.__FILE__)) + ; + $packageMock->expects($this->any()) + ->method('getDistUrls') + ->will($this->returnValue(array($distUrl))) + ; + $packageMock->expects($this->atLeastOnce()) + ->method('getTransportOptions') + ->will($this->returnValue(array())) ; $io = $this->getMock('Composer\IO\IOInterface'); diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index fd26b0a3c..aaf8b6267 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -14,8 +14,10 @@ namespace Composer\Test\EventDispatcher; use Composer\EventDispatcher\Event; use Composer\EventDispatcher\EventDispatcher; +use Composer\Installer\InstallerEvents; use Composer\TestCase; -use Composer\Script; +use Composer\Script\ScriptEvents; +use Composer\Script\CommandEvent; use Composer\Util\ProcessExecutor; class EventDispatcherTest extends TestCase @@ -27,14 +29,34 @@ class EventDispatcherTest extends TestCase { $io = $this->getMock('Composer\IO\IOInterface'); $dispatcher = $this->getDispatcherStubForListenersTest(array( - "Composer\Test\EventDispatcher\EventDispatcherTest::call" + 'Composer\Test\EventDispatcher\EventDispatcherTest::call' ), $io); $io->expects($this->once()) ->method('write') ->with('Script Composer\Test\EventDispatcher\EventDispatcherTest::call handling the post-install-cmd event terminated with an exception'); - $dispatcher->dispatchCommandEvent("post-install-cmd", false); + $dispatcher->dispatchCommandEvent(ScriptEvents::POST_INSTALL_CMD, false); + } + + public function testDispatcherCanConvertScriptEventToCommandEventForListener() + { + $io = $this->getMock('Composer\IO\IOInterface'); + $dispatcher = $this->getDispatcherStubForListenersTest(array( + 'Composer\Test\EventDispatcher\EventDispatcherTest::expectsCommandEvent' + ), $io); + + $this->assertEquals(1, $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false)); + } + + public function testDispatcherDoesNotAttemptConversionForListenerWithoutTypehint() + { + $io = $this->getMock('Composer\IO\IOInterface'); + $dispatcher = $this->getDispatcherStubForListenersTest(array( + 'Composer\Test\EventDispatcher\EventDispatcherTest::expectsVariableEvent' + ), $io); + + $this->assertEquals(1, $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false)); } /** @@ -63,7 +85,7 @@ class EventDispatcherTest extends TestCase ->with($command) ->will($this->returnValue(0)); - $dispatcher->dispatchCommandEvent("post-install-cmd", false); + $dispatcher->dispatchCommandEvent(ScriptEvents::POST_INSTALL_CMD, false); } public function testDispatcherCanExecuteCliAndPhpInSameEventScriptStack() @@ -99,7 +121,7 @@ class EventDispatcherTest extends TestCase ->with('Composer\Test\EventDispatcher\EventDispatcherTest', 'someMethod') ->will($this->returnValue(true)); - $dispatcher->dispatchCommandEvent("post-install-cmd", false); + $dispatcher->dispatchCommandEvent(ScriptEvents::POST_INSTALL_CMD, false); } private function getDispatcherStubForListenersTest($listeners, $io) @@ -145,7 +167,7 @@ class EventDispatcherTest extends TestCase ->will($this->returnValue($listener)); ob_start(); - $dispatcher->dispatchCommandEvent("post-install-cmd", false); + $dispatcher->dispatchCommandEvent(ScriptEvents::POST_INSTALL_CMD, false); $this->assertEquals('foo', trim(ob_get_clean())); } @@ -171,7 +193,32 @@ class EventDispatcherTest extends TestCase ->with($this->equalTo('Script '.$code.' handling the post-install-cmd event returned with an error')); $this->setExpectedException('RuntimeException'); - $dispatcher->dispatchCommandEvent("post-install-cmd", false); + $dispatcher->dispatchCommandEvent(ScriptEvents::POST_INSTALL_CMD, false); + } + + public function testDispatcherInstallerEvents() + { + $process = $this->getMock('Composer\Util\ProcessExecutor'); + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') + ->setConstructorArgs(array( + $this->getMock('Composer\Composer'), + $this->getMock('Composer\IO\IOInterface'), + $process, + )) + ->setMethods(array('getListeners')) + ->getMock(); + + $dispatcher->expects($this->atLeastOnce()) + ->method('getListeners') + ->will($this->returnValue(array())); + + $policy = $this->getMock('Composer\DependencyResolver\PolicyInterface'); + $pool = $this->getMockBuilder('Composer\DependencyResolver\Pool')->disableOriginalConstructor()->getMock(); + $installedRepo = $this->getMockBuilder('Composer\Repository\CompositeRepository')->disableOriginalConstructor()->getMock(); + $request = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); + + $dispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $policy, $pool, $installedRepo, $request); + $dispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $policy, $pool, $installedRepo, $request, array()); } public static function call() @@ -179,6 +226,16 @@ class EventDispatcherTest extends TestCase throw new \RuntimeException(); } + public static function expectsCommandEvent(CommandEvent $event) + { + return false; + } + + public static function expectsVariableEvent($event) + { + return false; + } + public static function someMethod() { return true; diff --git a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test new file mode 100644 index 000000000..18f47732e --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test @@ -0,0 +1,36 @@ +--TEST-- +Abandoned packages are flagged +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0", "abandoned": true } + ] + }, + { + "type": "package", + "package": [ + { "name": "c/c", "version": "1.0.0", "abandoned": "b/b" } + ] + } + ], + "require": { + "a/a": "1.0.0", + "c/c": "1.0.0" + } +} +--RUN-- +install +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Installing dependencies (including require-dev) +Package a/a is abandoned, you should avoid using it. No replacement was suggested. +Package c/c is abandoned, you should avoid using it. Use b/b instead. +Writing lock file +Generating autoload files + +--EXPECT-- +Installing a/a (1.0.0) +Installing c/c (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test new file mode 100644 index 000000000..e2593ba35 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test @@ -0,0 +1,42 @@ +--TEST-- +Broken dependencies should not lead to a replacer being installed which is not mentioned by name +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0", "require": {"c/c": "1.*"} }, + { "name": "c/c", "version": "1.0.0", "replace": {"a/a": "1.0.0" },"require":{"x/x": "1.0"}}, + { "name": "d/d", "version": "1.0.0", "replace": {"a/a": "1.0.0", "c/c":"1.0.0" }} + ] + } + ], + "require": { + "a/a": "1.*", + "b/b": "1.*" + } +} +--RUN-- +install +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Installing dependencies (including require-dev) +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - c/c 1.0.0 requires x/x 1.0 -> no matching package found. + - b/b 1.0.0 requires c/c 1.* -> satisfiable by c/c[1.0.0]. + - Installation request for b/b 1.* -> satisfiable by b/b[1.0.0]. + +Potential causes: + - A typo in the package name + - The package is not available in a stable-enough version according to your minimum-stability setting + see for more details. + +Read for further common problems. + +--EXPECT-EXIT-CODE-- +2 +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/install-branch-alias-composer-repo.test b/tests/Composer/Test/Fixtures/installer/install-branch-alias-composer-repo.test new file mode 100644 index 000000000..bad64fc7d --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/install-branch-alias-composer-repo.test @@ -0,0 +1,19 @@ +--TEST-- +Installing branch aliased package from a Composer repository. +--COMPOSER-- +{ + "repositories": [ + { + "type": "composer", + "url": "file://install-branch-alias-composer-repo" + } + ], + "require": { + "a/a": "3.2.*@dev" + } +} +--RUN-- +install +--EXPECT-- +Installing a/a (dev-foobar abcdef0) +Marking a/a (3.2.x-dev abcdef0) as installed, alias of a/a (dev-foobar abcdef0) diff --git a/tests/Composer/Test/Fixtures/installer/install-branch-alias-composer-repo/packages.json b/tests/Composer/Test/Fixtures/installer/install-branch-alias-composer-repo/packages.json new file mode 100644 index 000000000..d51c0be9f --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/install-branch-alias-composer-repo/packages.json @@ -0,0 +1,23 @@ +{ + "packages": { + "a/a": { + "dev-foobar": { + "name": "a/a", + "version": "dev-foobar", + "version_normalized": "dev-foobar", + "source": { + "type": "git", + "url": "git@example.com:repo.git", + "reference": "abcdef0000000000000000000000000000000000" + }, + "time": "2014-11-13 11:52:12", + "type": "library", + "extra": { + "branch-alias": { + "dev-foobar": "3.2.x-dev" + } + } + } + } + } +} diff --git a/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test b/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test index fe7582b42..ccc9ead1a 100644 --- a/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test +++ b/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test @@ -47,6 +47,8 @@ install --prefer-dist "stability-flags": { "a/a": 20 }, + "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } diff --git a/tests/Composer/Test/Fixtures/installer/install-dev.test b/tests/Composer/Test/Fixtures/installer/install-dev.test index 3b03675bb..b6543fb1b 100644 --- a/tests/Composer/Test/Fixtures/installer/install-dev.test +++ b/tests/Composer/Test/Fixtures/installer/install-dev.test @@ -19,7 +19,7 @@ Installs a package in dev env } } --RUN-- -install --dev +install --EXPECT-- Installing a/a (1.0.0) -Installing a/b (1.0.0) \ No newline at end of file +Installing a/b (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test b/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test index 65b3fe80b..7bb69f131 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test @@ -24,7 +24,9 @@ Requirements from the composer file are not installed if the lock file is presen "packages-dev": null, "aliases": [], "minimum-stability": "stable", - "stability-flags": [] + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false } --RUN-- install diff --git a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test new file mode 100644 index 000000000..6063abfee --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test @@ -0,0 +1,44 @@ +--TEST-- +Install from a lock file that deleted a package +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "whitelisted", "version": "1.1.0" }, + { "name": "whitelisted", "version": "1.0.0", "require": { "fixed-dependency": "1.0.0", "old-dependency": "1.0.0" } }, + { "name": "fixed-dependency", "version": "1.1.0" }, + { "name": "fixed-dependency", "version": "1.0.0" }, + { "name": "old-dependency", "version": "1.0.0" } + ] + } + ], + "require": { + "whitelisted": "1.*", + "fixed-dependency": "1.*" + } +} +--LOCK-- +{ + "packages": [ + { "name": "whitelisted", "version": "1.1.0" }, + { "name": "fixed-dependency", "version": "1.0.0" } + ], + "packages-dev": null, + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false +} +--INSTALLED-- +[ + { "name": "whitelisted", "version": "1.0.0", "require": { "old-dependency": "1.0.0", "fixed-dependency": "1.0.0" } }, + { "name": "fixed-dependency", "version": "1.0.0" }, + { "name": "old-dependency", "version": "1.0.0" } +] +--RUN-- +install +--EXPECT-- +Uninstalling old-dependency (1.0.0) +Updating whitelisted (1.0.0) to whitelisted (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/install-ignore-platform-package-requirements.test b/tests/Composer/Test/Fixtures/installer/install-ignore-platform-package-requirements.test new file mode 100644 index 000000000..7959b6e07 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/install-ignore-platform-package-requirements.test @@ -0,0 +1,22 @@ +--TEST-- +Install in ignore-platform-reqs mode +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0", "require": { "ext-testdummy": "*", "php": "98" } } + ] + } + ], + "require": { + "a/a": "1.0.0", + "php": "99.9", + "ext-dummy2": "3" + } +} +--RUN-- +install --ignore-platform-reqs +--EXPECT-- +Installing a/a (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test b/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test index e5ddacf20..5acb7a069 100644 --- a/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test @@ -32,7 +32,9 @@ Installing an old alias that doesn't exist anymore from a lock is possible "packages-dev": null, "aliases": [], "minimum-stability": "dev", - "stability-flags": [] + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false } --RUN-- install diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test index fb618ebe3..4b2c62a53 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test @@ -35,6 +35,8 @@ Partial update from lock file should apply lock file and downgrade unstable pack "stability-flags": { "b/unstable": 15 }, + "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } @@ -57,6 +59,8 @@ update c/uptodate "aliases": [], "minimum-stability": "stable", "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test index 51368b861..5c3508840 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test @@ -35,6 +35,8 @@ Partial update from lock file should update everything to the state of the lock, "stability-flags": { "b/unstable": 15 }, + "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } @@ -57,6 +59,8 @@ update b/unstable "aliases": [], "minimum-stability": "stable", "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test index 146277d02..e931a1f7d 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test @@ -42,6 +42,8 @@ update b/unstable "aliases": [], "minimum-stability": "stable", "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } diff --git a/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test b/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test index c57a36d35..ad34e9c02 100644 --- a/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test +++ b/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test @@ -1,5 +1,5 @@ --TEST-- -Composer installers are installed first if they have no requirements +Composer installers are installed first if they have no meaningful requirements --COMPOSER-- { "repositories": [ @@ -9,20 +9,23 @@ Composer installers are installed first if they have no requirements { "name": "pkg", "version": "1.0.0" }, { "name": "pkg2", "version": "1.0.0" }, { "name": "inst", "version": "1.0.0", "type": "composer-plugin" }, - { "name": "inst2", "version": "1.0.0", "type": "composer-plugin", "require": { "pkg2": "*" } } + { "name": "inst-with-req", "version": "1.0.0", "type": "composer-plugin", "require": { "php": ">=5", "ext-json": "*", "composer-plugin-api": "*" } }, + { "name": "inst-with-req2", "version": "1.0.0", "type": "composer-plugin", "require": { "pkg2": "*" } } ] } ], "require": { "pkg": "1.0.0", "inst": "1.0.0", - "inst2": "1.0.0" + "inst-with-req2": "1.0.0", + "inst-with-req": "1.0.0" } } --RUN-- install --EXPECT-- Installing inst (1.0.0) +Installing inst-with-req (1.0.0) Installing pkg (1.0.0) Installing pkg2 (1.0.0) -Installing inst2 (1.0.0) +Installing inst-with-req2 (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/provide-priorities.test b/tests/Composer/Test/Fixtures/installer/provide-priorities.test deleted file mode 100644 index f97e16e6c..000000000 --- a/tests/Composer/Test/Fixtures/installer/provide-priorities.test +++ /dev/null @@ -1,34 +0,0 @@ ---TEST-- -Provide only applies when no existing package has the given name ---COMPOSER-- -{ - "repositories": [ - { - "type": "package", - "package": [ - { "name": "higher-prio-hijacker", "version": "1.1.0", "provide": { "package": "1.0.0" } }, - { "name": "provider2", "version": "1.1.0", "provide": { "package2": "1.0.0" } } - ] - }, - { - "type": "package", - "package": [ - { "name": "package", "version": "0.9.0" }, - { "name": "package", "version": "1.0.0" }, - { "name": "hijacker", "version": "1.1.0", "provide": { "package": "1.0.0" } }, - { "name": "provider3", "version": "1.1.0", "provide": { "package3": "1.0.0" } } - ] - } - ], - "require": { - "package": "1.*", - "package2": "1.*", - "provider3": "1.1.0" - } -} ---RUN-- -install ---EXPECT-- -Installing package (1.0.0) -Installing provider2 (1.1.0) -Installing provider3 (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/replace-alias.test b/tests/Composer/Test/Fixtures/installer/replace-alias.test new file mode 100644 index 000000000..327fb5ab5 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/replace-alias.test @@ -0,0 +1,25 @@ +--TEST-- +Ensure a replacer package deals with branch aliases +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "dev-master", "replace": {"c/c": "self.version" }, "extra": { "branch-alias": {"dev-master": "1.0.x-dev"} } }, + { "name": "b/b", "version": "1.0.0", "require": {"c/c": "1.*"} }, + { "name": "c/c", "version": "dev-master", "extra": { "branch-alias": {"dev-master": "1.0.x-dev"} } } + ] + } + ], + "require": { + "a/a": "dev-master", + "b/b": "1.*" + } +} +--RUN-- +install +--EXPECT-- +Installing a/a (dev-master) +Marking a/a (1.0.x-dev) as installed, alias of a/a (dev-master) +Installing b/b (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/replace-priorities.test b/tests/Composer/Test/Fixtures/installer/replace-priorities.test index 2f27ba7b7..d69dd9a22 100644 --- a/tests/Composer/Test/Fixtures/installer/replace-priorities.test +++ b/tests/Composer/Test/Fixtures/installer/replace-priorities.test @@ -1,5 +1,5 @@ --TEST-- -Replace takes precedence only in higher priority repositories +Replace takes precedence only in higher priority repositories and if explicitly required --COMPOSER-- { "repositories": [ @@ -14,13 +14,15 @@ Replace takes precedence only in higher priority repositories "package": [ { "name": "package", "version": "1.0.0" }, { "name": "package2", "version": "1.0.0" }, + { "name": "package3", "version": "1.0.0", "require": { "forked": "*" } }, { "name": "hijacker", "version": "1.1.0", "replace": { "package": "1.1.0" } } ] } ], "require": { "package": "1.*", - "package2": "1.*" + "package2": "1.*", + "package3": "1.*" } } --RUN-- @@ -28,3 +30,4 @@ install --EXPECT-- Installing package (1.0.0) Installing forked (1.1.0) +Installing package3 (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/replace-root-require.test b/tests/Composer/Test/Fixtures/installer/replace-root-require.test new file mode 100644 index 000000000..c00ac4fd5 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/replace-root-require.test @@ -0,0 +1,24 @@ +--TEST-- +Ensure a transiently required replacer can replace root requirements +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0", "require": {"c/c": "1.*"} }, + { "name": "c/c", "version": "1.0.0", "replace": {"a/a": "1.0.0" }} + ] + } + ], + "require": { + "a/a": "1.*", + "b/b": "1.*" + } +} +--RUN-- +install +--EXPECT-- +Installing c/c (1.0.0) +Installing b/b (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/replace-vendor-priorities.test b/tests/Composer/Test/Fixtures/installer/replace-vendor-priorities.test deleted file mode 100644 index 86c491feb..000000000 --- a/tests/Composer/Test/Fixtures/installer/replace-vendor-priorities.test +++ /dev/null @@ -1,21 +0,0 @@ ---TEST-- -Replacer of the same vendor takes precedence if same prio repo ---COMPOSER-- -{ - "repositories": [ - { - "type": "package", - "package": [ - { "name": "b/replacer", "version": "1.1.0", "replace": { "a/package": "1.1.0" } }, - { "name": "a/replacer", "version": "1.1.0", "replace": { "a/package": "1.1.0" } } - ] - } - ], - "require": { - "a/package": "1.*" - } -} ---RUN-- -install ---EXPECT-- -Installing a/replacer (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/suggest-installed.test b/tests/Composer/Test/Fixtures/installer/suggest-installed.test index f46102d0a..94f6c2016 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-installed.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-installed.test @@ -20,10 +20,10 @@ Suggestions are not displayed for installed packages install --EXPECT-OUTPUT-- Loading composer repositories with package information -Installing dependencies +Installing dependencies (including require-dev) Writing lock file Generating autoload files --EXPECT-- Installing a/a (1.0.0) -Installing b/b (1.0.0) \ No newline at end of file +Installing b/b (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod.test b/tests/Composer/Test/Fixtures/installer/suggest-prod.test new file mode 100644 index 000000000..290ccf4bb --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/suggest-prod.test @@ -0,0 +1,26 @@ +--TEST-- +Suggestions are not displayed in non-dev mode +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0", "suggest": { "b/b": "an obscure reason" } } + ] + } + ], + "require": { + "a/a": "1.0.0" + } +} +--RUN-- +install --no-dev +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Installing dependencies +Writing lock file +Generating autoload files + +--EXPECT-- +Installing a/a (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test index d1e8f6102..99d13a720 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test @@ -6,7 +6,7 @@ Suggestions are not displayed for packages if they are replaced { "type": "package", "package": [ - { "name": "a/a", "version": "1.0.0", "suggest": { "b/b": "an obscure reason" } }, + { "name": "a/a", "version": "1.0.0", "suggest": { "b/b": "an obscure reason" }, "require": { "c/c": "*" } }, { "name": "c/c", "version": "1.0.0", "replace": { "b/b": "1.0.0" } } ] } @@ -20,10 +20,10 @@ Suggestions are not displayed for packages if they are replaced install --EXPECT-OUTPUT-- Loading composer repositories with package information -Installing dependencies +Installing dependencies (including require-dev) Writing lock file Generating autoload files --EXPECT-- +Installing c/c (1.0.0) Installing a/a (1.0.0) -Installing c/c (1.0.0) \ No newline at end of file diff --git a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test index d2ea37766..d7e026e98 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test @@ -18,10 +18,10 @@ Suggestions are displayed install --EXPECT-OUTPUT-- Loading composer repositories with package information -Installing dependencies +Installing dependencies (including require-dev) a/a suggests installing b/b (an obscure reason) Writing lock file Generating autoload files --EXPECT-- -Installing a/a (1.0.0) \ No newline at end of file +Installing a/a (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test index ad9451c6f..920321a20 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test @@ -38,7 +38,9 @@ Update aliased package does not mess up the lock file "packages-dev": null, "aliases": [], "minimum-stability": "dev", - "stability-flags": [] + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false } --INSTALLED-- [ @@ -64,8 +66,10 @@ update "aliases": [], "minimum-stability": "dev", "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } --EXPECT-- -Updating a/a (dev-master 1234) to a/a (dev-master master) \ No newline at end of file +Updating a/a (dev-master 1234) to a/a (dev-master master) diff --git a/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test b/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test index 191f97495..cca859e9f 100644 --- a/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test +++ b/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test @@ -34,7 +34,7 @@ Updates updateable packages in dry-run mode { "name": "a/b", "version": "1.0.0" } ] --RUN-- -update --dev --dry-run +update --dry-run --EXPECT-- Updating a/a (1.0.0) to a/a (1.0.1) Updating a/b (1.0.0) to a/b (2.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-all.test b/tests/Composer/Test/Fixtures/installer/update-all.test index ad5e1d3be..a9bb435a1 100644 --- a/tests/Composer/Test/Fixtures/installer/update-all.test +++ b/tests/Composer/Test/Fixtures/installer/update-all.test @@ -34,7 +34,7 @@ Updates updateable packages { "name": "a/b", "version": "1.0.0" } ] --RUN-- -update --dev +update --EXPECT-- Updating a/a (1.0.0) to a/a (1.0.1) -Updating a/b (1.0.0) to a/b (2.0.0) \ No newline at end of file +Updating a/b (1.0.0) to a/b (2.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test b/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test new file mode 100644 index 000000000..02f94cd6e --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirements.test @@ -0,0 +1,26 @@ +--TEST-- +Update in ignore-platform-reqs mode +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.1", "require": { "ext-testdummy": "*" } } + ] + } + ], + "require": { + "a/a": "1.0.*", + "php": "99.9", + "ext-dummy2": "9" + } +} +--INSTALLED-- +[ + { "name": "a/a", "version": "1.0.0" } +] +--RUN-- +update --ignore-platform-reqs +--EXPECT-- +Updating a/a (1.0.0) to a/a (1.0.1) diff --git a/tests/Composer/Test/Fixtures/installer/update-installed-reference-dry-run.test b/tests/Composer/Test/Fixtures/installer/update-installed-reference-dry-run.test new file mode 100644 index 000000000..3c9036be4 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-installed-reference-dry-run.test @@ -0,0 +1,30 @@ +--TEST-- +Updating a dev package forcing it's reference, using dry run, should not do anything if the referenced version is the installed one +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "abc123", "url": "", "type": "git" } + } + ] + } + ], + "require": { + "a/a": "dev-master#def000" + } +} +--INSTALLED-- +[ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "def000", "url": "", "type": "git" }, + "dist": { "reference": "def000", "url": "", "type": "zip", "shasum": "" } + } +] +--RUN-- +update --dry-run +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-installed-reference.test b/tests/Composer/Test/Fixtures/installer/update-installed-reference.test new file mode 100644 index 000000000..e6814ccfe --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-installed-reference.test @@ -0,0 +1,30 @@ +--TEST-- +Updating a dev package forcing it's reference should not do anything if the referenced version is the installed one +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "abc123", "url": "", "type": "git" } + } + ] + } + ], + "require": { + "a/a": "dev-master#def000" + } +} +--INSTALLED-- +[ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "def000", "url": "", "type": "git" }, + "dist": { "reference": "def000", "url": "", "type": "zip", "shasum": "" } + } +] +--RUN-- +update +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test b/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test new file mode 100644 index 000000000..00efd5688 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-prefer-lowest-stable.test @@ -0,0 +1,40 @@ +--TEST-- +Updates packages to their lowest stable version +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0-rc1" }, + { "name": "a/a", "version": "1.0.1" }, + { "name": "a/a", "version": "1.1.0" }, + + { "name": "a/b", "version": "1.0.0" }, + { "name": "a/b", "version": "1.0.1" }, + { "name": "a/b", "version": "2.0.0" }, + + { "name": "a/c", "version": "1.0.0" }, + { "name": "a/c", "version": "2.0.0" } + ] + } + ], + "require": { + "a/a": "~1.0@dev", + "a/c": "2.*" + }, + "require-dev": { + "a/b": "*" + } +} +--INSTALLED-- +[ + { "name": "a/a", "version": "1.0.0-rc1" }, + { "name": "a/c", "version": "2.0.0" }, + { "name": "a/b", "version": "1.0.1" } +] +--RUN-- +update --prefer-lowest --prefer-stable +--EXPECT-- +Updating a/a (1.0.0-rc1) to a/a (1.0.1) +Updating a/b (1.0.1) to a/b (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test index e8aa593c0..de1fb1b73 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test @@ -45,4 +45,4 @@ update vendor/Test* exact/Test-Package notexact/Test all/* no/reg?xp Updating vendor/Test-Package (1.0) to vendor/Test-Package (2.0) Updating exact/Test-Package (1.0) to exact/Test-Package (2.0) Updating all/Package1 (1.0) to all/Package1 (2.0) -Updating all/Package2 (1.0) to all/Package2 (2.0) \ No newline at end of file +Updating all/Package2 (1.0) to all/Package2 (2.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test index d73b93557..96036e479 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test @@ -31,7 +31,9 @@ Limited update takes rules from lock if available, and not from the installed re "packages-dev": null, "aliases": [], "minimum-stability": "stable", - "stability-flags": [] + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false } --INSTALLED-- [ diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test new file mode 100644 index 000000000..e658e8c06 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test @@ -0,0 +1,32 @@ +--TEST-- +Update with a package whitelist removes unused packages +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "whitelisted", "version": "1.1.0" }, + { "name": "whitelisted", "version": "1.0.0", "require": { "fixed-dependency": "1.0.0", "old-dependency": "1.0.0" } }, + { "name": "fixed-dependency", "version": "1.1.0" }, + { "name": "fixed-dependency", "version": "1.0.0" }, + { "name": "old-dependency", "version": "1.0.0" } + ] + } + ], + "require": { + "whitelisted": "1.*", + "fixed-dependency": "1.*" + } +} +--INSTALLED-- +[ + { "name": "whitelisted", "version": "1.0.0", "require": { "old-dependency": "1.0.0", "fixed-dependency": "1.0.0" } }, + { "name": "fixed-dependency", "version": "1.0.0" }, + { "name": "old-dependency", "version": "1.0.0" } +] +--RUN-- +update --with-dependencies whitelisted +--EXPECT-- +Uninstalling old-dependency (1.0.0) +Updating whitelisted (1.0.0) to whitelisted (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test index f5c4ccc24..04624561d 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test @@ -19,7 +19,9 @@ Installing locked dev packages should remove old dependencies "packages-dev": null, "aliases": [], "minimum-stability": "dev", - "stability-flags": [] + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false } --INSTALLED-- [ diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test index 3eb701719..c5c838517 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test @@ -31,7 +31,9 @@ Updating a dev package for new reference updates the url and reference "packages-dev": null, "aliases": [], "minimum-stability": "dev", - "stability-flags": {"a/a":20} + "stability-flags": {"a/a":20}, + "prefer-stable": false, + "prefer-lowest": false } --INSTALLED-- [ @@ -57,6 +59,8 @@ update "aliases": [], "minimum-stability": "dev", "stability-flags": {"a/a":20}, + "prefer-stable": false, + "prefer-lowest": false, "platform": [], "platform-dev": [] } diff --git a/tests/Composer/Test/IO/ConsoleIOTest.php b/tests/Composer/Test/IO/ConsoleIOTest.php index 3a4313f69..c83ec6296 100644 --- a/tests/Composer/Test/IO/ConsoleIOTest.php +++ b/tests/Composer/Test/IO/ConsoleIOTest.php @@ -49,6 +49,30 @@ class ConsoleIOTest extends TestCase $consoleIO->write('some information about something', false); } + public function testWriteWithMultipleLineStringWhenDebugging() + { + $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface'); + $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); + $outputMock->expects($this->once()) + ->method('write') + ->with( + $this->callback(function($messages){ + $result = preg_match("[(.*)/(.*) First line]", $messages[0]) > 0; + $result &= preg_match("[(.*)/(.*) Second line]", $messages[1]) > 0; + return $result; + }), + $this->equalTo(false) + ); + $helperMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet'); + + $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock); + $startTime = microtime(true); + $consoleIO->enableDebugging($startTime); + + $example = explode('\n', 'First line\nSecond lines'); + $consoleIO->write($example, false); + } + public function testOverwrite() { $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface'); @@ -58,21 +82,27 @@ class ConsoleIOTest extends TestCase ->method('write') ->with($this->equalTo('something (strlen = 23)')); $outputMock->expects($this->at(1)) + ->method('isDecorated') + ->willReturn(true); + $outputMock->expects($this->at(2)) ->method('write') ->with($this->equalTo(str_repeat("\x08", 23)), $this->equalTo(false)); - $outputMock->expects($this->at(2)) + $outputMock->expects($this->at(3)) ->method('write') ->with($this->equalTo('shorter (12)'), $this->equalTo(false)); - $outputMock->expects($this->at(3)) + $outputMock->expects($this->at(4)) ->method('write') ->with($this->equalTo(str_repeat(' ', 11)), $this->equalTo(false)); - $outputMock->expects($this->at(4)) + $outputMock->expects($this->at(5)) ->method('write') ->with($this->equalTo(str_repeat("\x08", 11)), $this->equalTo(false)); - $outputMock->expects($this->at(5)) + $outputMock->expects($this->at(6)) + ->method('isDecorated') + ->willReturn(true); + $outputMock->expects($this->at(7)) ->method('write') ->with($this->equalTo(str_repeat("\x08", 12)), $this->equalTo(false)); - $outputMock->expects($this->at(6)) + $outputMock->expects($this->at(8)) ->method('write') ->with($this->equalTo('something longer than initial (34)')); diff --git a/tests/Composer/Test/Installer/InstallerEventTest.php b/tests/Composer/Test/Installer/InstallerEventTest.php new file mode 100644 index 000000000..7cd63f6b5 --- /dev/null +++ b/tests/Composer/Test/Installer/InstallerEventTest.php @@ -0,0 +1,39 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Installer; + +use Composer\Installer\InstallerEvent; + +class InstallerEventTest extends \PHPUnit_Framework_TestCase +{ + public function testGetter() + { + $composer = $this->getMock('Composer\Composer'); + $io = $this->getMock('Composer\IO\IOInterface'); + $policy = $this->getMock('Composer\DependencyResolver\PolicyInterface'); + $pool = $this->getMockBuilder('Composer\DependencyResolver\Pool')->disableOriginalConstructor()->getMock(); + $installedRepo = $this->getMockBuilder('Composer\Repository\CompositeRepository')->disableOriginalConstructor()->getMock(); + $request = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); + $operations = array($this->getMock('Composer\DependencyResolver\Operation\OperationInterface')); + $event = new InstallerEvent('EVENT_NAME', $composer, $io, $policy, $pool, $installedRepo, $request, $operations); + + $this->assertSame('EVENT_NAME', $event->getName()); + $this->assertInstanceOf('Composer\Composer', $event->getComposer()); + $this->assertInstanceOf('Composer\IO\IOInterface', $event->getIO()); + $this->assertInstanceOf('Composer\DependencyResolver\PolicyInterface', $event->getPolicy()); + $this->assertInstanceOf('Composer\DependencyResolver\Pool', $event->getPool()); + $this->assertInstanceOf('Composer\Repository\CompositeRepository', $event->getInstalledRepo()); + $this->assertInstanceOf('Composer\DependencyResolver\Request', $event->getRequest()); + $this->assertCount(1, $event->getOperations()); + } +} diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 0cc17cfb0..2c77f9bc3 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -51,7 +51,7 @@ class InstallerTest extends TestCase { $io = $this->getMock('Composer\IO\IOInterface'); - $downloadManager = $this->getMock('Composer\Downloader\DownloadManager'); + $downloadManager = $this->getMock('Composer\Downloader\DownloadManager', array(), array($io)); $config = $this->getMock('Composer\Config'); $repositoryManager = new RepositoryManager($io, $config); @@ -138,7 +138,7 @@ class InstallerTest extends TestCase /** * @dataProvider getIntegrationTests */ - public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectOutput, $expect) + public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode) { if ($condition) { eval('$res = '.$condition.';'); @@ -152,7 +152,7 @@ class InstallerTest extends TestCase $io->expects($this->any()) ->method('write') ->will($this->returnCallback(function ($text, $newline) use (&$output) { - $output .= $text . ($newline ? "\n":""); + $output .= $text . ($newline ? "\n" : ""); })); $composer = FactoryMock::create($io, $composerConfig); @@ -203,19 +203,23 @@ class InstallerTest extends TestCase $application = new Application; $application->get('install')->setCode(function ($input, $output) use ($installer) { $installer - ->setDevMode($input->getOption('dev')) - ->setDryRun($input->getOption('dry-run')); + ->setDevMode(!$input->getOption('no-dev')) + ->setDryRun($input->getOption('dry-run')) + ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); return $installer->run(); }); $application->get('update')->setCode(function ($input, $output) use ($installer) { $installer - ->setDevMode($input->getOption('dev')) + ->setDevMode(!$input->getOption('no-dev')) ->setUpdate(true) ->setDryRun($input->getOption('dry-run')) ->setUpdateWhitelist($input->getArgument('packages')) - ->setWhitelistDependencies($input->getOption('with-dependencies')); + ->setWhitelistDependencies($input->getOption('with-dependencies')) + ->setPreferStable($input->getOption('prefer-stable')) + ->setPreferLowest($input->getOption('prefer-lowest')) + ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); return $installer->run(); }); @@ -228,7 +232,7 @@ class InstallerTest extends TestCase $appOutput = fopen('php://memory', 'w+'); $result = $application->run(new StringInput($run), new StreamOutput($appOutput)); fseek($appOutput, 0); - $this->assertEquals(0, $result, $output . stream_get_contents($appOutput)); + $this->assertEquals($expectExitCode, $result, $output . stream_get_contents($appOutput)); if ($expectLock) { unset($actualLock['hash']); @@ -237,10 +241,10 @@ class InstallerTest extends TestCase } $installationManager = $composer->getInstallationManager(); - $this->assertSame($expect, implode("\n", $installationManager->getTrace())); + $this->assertSame(rtrim($expect), implode("\n", $installationManager->getTrace())); if ($expectOutput) { - $this->assertEquals($expectOutput, $output); + $this->assertEquals(rtrim($expectOutput), rtrim($output)); } } @@ -254,56 +258,112 @@ class InstallerTest extends TestCase continue; } - $test = file_get_contents($file->getRealpath()); - - $content = '(?:.(?!--[A-Z]))+'; - $pattern = '{^ - --TEST--\s*(?P.*?)\s* - (?:--CONDITION--\s*(?P'.$content.'))?\s* - --COMPOSER--\s*(?P'.$content.')\s* - (?:--LOCK--\s*(?P'.$content.'))?\s* - (?:--INSTALLED--\s*(?P'.$content.'))?\s* - --RUN--\s*(?P.*?)\s* - (?:--EXPECT-LOCK--\s*(?P'.$content.'))?\s* - (?:--EXPECT-OUTPUT--\s*(?P'.$content.'))?\s* - --EXPECT--\s*(?P.*?)\s* - $}xs'; + $testData = $this->readTestFile($file, $fixturesDir); $installed = array(); $installedDev = array(); $lock = array(); $expectLock = array(); + $expectExitCode = 0; + + try { + $message = $testData['TEST']; + $condition = !empty($testData['CONDITION']) ? $testData['CONDITION'] : null; + $composer = JsonFile::parseJson($testData['COMPOSER']); - if (preg_match($pattern, $test, $match)) { - try { - $message = $match['test']; - $condition = !empty($match['condition']) ? $match['condition'] : null; - $composer = JsonFile::parseJson($match['composer']); - if (!empty($match['lock'])) { - $lock = JsonFile::parseJson($match['lock']); - if (!isset($lock['hash'])) { - $lock['hash'] = md5(json_encode($composer)); + if (isset($composer['repositories'])) { + foreach ($composer['repositories'] as &$repo) { + if ($repo['type'] !== 'composer') { + continue; } + + // Change paths like file://foobar to file:///path/to/fixtures + if (preg_match('{^file://[^/]}', $repo['url'])) { + $repo['url'] = 'file://' . strtr($fixturesDir, '\\', '/') . '/' . substr($repo['url'], 7); + } + + unset($repo); } - if (!empty($match['installed'])) { - $installed = JsonFile::parseJson($match['installed']); - } - $run = $match['run']; - if (!empty($match['expectLock'])) { - $expectLock = JsonFile::parseJson($match['expectLock']); + } + + if (!empty($testData['LOCK'])) { + $lock = JsonFile::parseJson($testData['LOCK']); + if (!isset($lock['hash'])) { + $lock['hash'] = md5(json_encode($composer)); } - $expectOutput = $match['expectOutput']; - $expect = $match['expect']; - } catch (\Exception $e) { - die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file))); } - } else { - die(sprintf('Test "%s" is not valid, did not match the expected format.', str_replace($fixturesDir.'/', '', $file))); + if (!empty($testData['INSTALLED'])) { + $installed = JsonFile::parseJson($testData['INSTALLED']); + } + $run = $testData['RUN']; + if (!empty($testData['EXPECT-LOCK'])) { + $expectLock = JsonFile::parseJson($testData['EXPECT-LOCK']); + } + $expectOutput = isset($testData['EXPECT-OUTPUT']) ? $testData['EXPECT-OUTPUT'] : null; + $expect = $testData['EXPECT']; + $expectExitCode = isset($testData['EXPECT-EXIT-CODE']) ? (int) $testData['EXPECT-EXIT-CODE'] : 0; + } catch (\Exception $e) { + die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file))); } - $tests[] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect); + $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode); } return $tests; } + + protected function readTestFile(\SplFileInfo $file, $fixturesDir) + { + $tokens = preg_split('#(?:^|\n*)--([A-Z-]+)--\n#', file_get_contents($file->getRealPath()), null, PREG_SPLIT_DELIM_CAPTURE); + + $sectionInfo = array( + 'TEST' => true, + 'CONDITION' => false, + 'COMPOSER' => true, + 'LOCK' => false, + 'INSTALLED' => false, + 'RUN' => true, + 'EXPECT-LOCK' => false, + 'EXPECT-OUTPUT' => false, + 'EXPECT-EXIT-CODE' => false, + 'EXPECT' => true, + ); + + $section = null; + foreach ($tokens as $i => $token) + { + if (null === $section && empty($token)) { + continue; // skip leading blank + } + + if (null === $section) { + if (!isset($sectionInfo[$token])) { + throw new \RuntimeException(sprintf( + 'The test file "%s" must not contain a section named "%s".', + str_replace($fixturesDir.'/', '', $file), + $token + )); + } + $section = $token; + continue; + } + + $sectionData = $token; + + $data[$section] = $sectionData; + $section = $sectionData = null; + } + + foreach ($sectionInfo as $section => $required) { + if ($required && !isset($data[$section])) { + throw new \RuntimeException(sprintf( + 'The test file "%s" must have a section named "%s".', + str_replace($fixturesDir.'/', '', $file), + $section + )); + } + } + + return $data; + } } diff --git a/tests/Composer/Test/Json/ComposerSchemaTest.php b/tests/Composer/Test/Json/ComposerSchemaTest.php new file mode 100644 index 000000000..1b8805f48 --- /dev/null +++ b/tests/Composer/Test/Json/ComposerSchemaTest.php @@ -0,0 +1,90 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Json; + +use JsonSchema\Validator; + +/** + * @author Rob Bast + */ +class ComposerSchemaTest extends \PHPUnit_Framework_TestCase +{ + public function testRequiredProperties() + { + $json = '{ }'; + $this->assertEquals(array( + array('property' => '', 'message' => 'the property name is required'), + array('property' => '', 'message' => 'the property description is required'), + ), $this->check($json)); + + $json = '{ "name": "vendor/package" }'; + $this->assertEquals(array( + array('property' => '', 'message' => 'the property description is required'), + ), $this->check($json)); + + $json = '{ "description": "generic description" }'; + $this->assertEquals(array( + array('property' => '', 'message' => 'the property name is required'), + ), $this->check($json)); + } + + public function testMinimumStabilityValues() + { + $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "" }'; + $this->assertEquals(array( + array( + 'property' => 'minimum-stability', + 'message' => 'does not match the regex pattern ^dev|alpha|beta|rc|RC|stable$' + ), + ), $this->check($json), 'empty string'); + + $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "dummy" }'; + $this->assertEquals(array( + array( + 'property' => 'minimum-stability', + 'message' => 'does not match the regex pattern ^dev|alpha|beta|rc|RC|stable$' + ), + ), $this->check($json), 'dummy'); + + $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "dev" }'; + $this->assertTrue($this->check($json), 'dev'); + + $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "alpha" }'; + $this->assertTrue($this->check($json), 'alpha'); + + $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "beta" }'; + $this->assertTrue($this->check($json), 'beta'); + + $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "rc" }'; + $this->assertTrue($this->check($json), 'rc lowercase'); + + $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "RC" }'; + $this->assertTrue($this->check($json), 'rc uppercase'); + + $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "stable" }'; + $this->assertTrue($this->check($json), 'stable'); + } + + private function check($json) + { + $schema = json_decode(file_get_contents(__DIR__ . '/../../../../res/composer-schema.json')); + $validator = new Validator(); + $validator->check(json_decode($json), $schema); + + if (!$validator->isValid()) { + return $validator->getErrors(); + } + + return true; + } +} diff --git a/tests/Composer/Test/Json/JsonFileTest.php b/tests/Composer/Test/Json/JsonFileTest.php index 79a1e40f4..9036a23e8 100644 --- a/tests/Composer/Test/Json/JsonFileTest.php +++ b/tests/Composer/Test/Json/JsonFileTest.php @@ -132,12 +132,8 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase { $data = array('test' => array(), 'test2' => new \stdClass); $json = '{ - "test": [ - - ], - "test2": { - - } + "test": [], + "test2": {} }'; $this->assertJsonFormat($json, $data); } @@ -198,6 +194,18 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase $this->assertJsonFormat('"\\u018c"', $data, 0); } + public function testDoubleEscapedUnicode() + { + $jsonFile = new JsonFile('composer.json'); + $data = array("ZdjÄ™cia","hjkjhl\\u0119kkjk"); + $encodedData = $jsonFile->encode($data); + $doubleEncodedData = $jsonFile->encode(array('t' => $encodedData)); + + $decodedData = json_decode($doubleEncodedData, true); + $doubleData = json_decode($decodedData['t'], true); + $this->assertEquals($data, $doubleData); + } + private function expectParseException($text, $json) { try { @@ -218,5 +226,4 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase $this->assertEquals($json, $file->encode($data, $options)); } } - } diff --git a/tests/Composer/Test/Json/JsonFormatterTest.php b/tests/Composer/Test/Json/JsonFormatterTest.php new file mode 100644 index 000000000..557312fc3 --- /dev/null +++ b/tests/Composer/Test/Json/JsonFormatterTest.php @@ -0,0 +1,49 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Json; + +use Composer\Json\JsonFormatter; + +class JsonFormatterTest extends \PHPUnit_Framework_TestCase +{ + /** + * Test if \u0119 (196+153) will get correctly formatted + * See ticket #2613 + */ + public function testUnicodeWithPrependedSlash() + { + if (!extension_loaded('mbstring')) { + $this->markTestSkipped('Test requires the mbstring extension'); + } + + $data = '"' . chr(92) . chr(92) . chr(92) . 'u0119"'; + $encodedData = JsonFormatter::format($data, true, true); + $expected = '34+92+92+196+153+34'; + $this->assertEquals($expected, $this->getCharacterCodes($encodedData)); + } + + /** + * Convert string to character codes split by a plus sign + * @param string $string + * @return string + */ + protected function getCharacterCodes($string) + { + $codes = array(); + for ($i = 0; $i < strlen($string); $i++) { + $codes[] = ord($string[$i]); + } + + return implode('+', $codes); + } +} diff --git a/tests/Composer/Test/Json/JsonManipulatorTest.php b/tests/Composer/Test/Json/JsonManipulatorTest.php index f4097dcca..d8e564346 100644 --- a/tests/Composer/Test/Json/JsonManipulatorTest.php +++ b/tests/Composer/Test/Json/JsonManipulatorTest.php @@ -73,6 +73,7 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase ), array( '{ + "empty": "", "require": { "foo": "bar" } @@ -81,6 +82,7 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase 'vendor/baz', 'qux', '{ + "empty": "", "require": { "foo": "bar", "vendor/baz": "qux" @@ -228,6 +230,106 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase "foo": "qux" } } +' + ), + array( + '{ + "repositories": [{ + "type": "package", + "package": { + "bar": "ba[z", + "dist": { + "url": "http...", + "type": "zip" + }, + "autoload": { + "classmap": [ "foo/bar" ] + } + } + }], + "require": { + "php": "5.*" + }, + "require-dev": { + "foo": "bar" + } +}', + 'require-dev', + 'foo', + 'qux', + '{ + "repositories": [{ + "type": "package", + "package": { + "bar": "ba[z", + "dist": { + "url": "http...", + "type": "zip" + }, + "autoload": { + "classmap": [ "foo/bar" ] + } + } + }], + "require": { + "php": "5.*" + }, + "require-dev": { + "foo": "qux" + } +} +' + ), + ); + } + + /** + * @dataProvider providerAddLinkAndSortPackages + */ + public function testAddLinkAndSortPackages($json, $type, $package, $constraint, $sortPackages, $expected) + { + $manipulator = new JsonManipulator($json); + $this->assertTrue($manipulator->addLink($type, $package, $constraint, $sortPackages)); + $this->assertEquals($expected, $manipulator->getContents()); + } + + public function providerAddLinkAndSortPackages() + { + return array( + array( + '{ + "require": { + "vendor/baz": "qux" + } +}', + 'require', + 'foo', + 'bar', + true, + '{ + "require": { + "foo": "bar", + "vendor/baz": "qux" + } +} +' + ), + array( + '{ + "require": { + "vendor/baz": "qux" + } +}', + 'require', + 'foo', + 'bar', + false, + '{ + "require": { + "vendor/baz": "qux", + "foo": "bar" + } +} ' ), ); @@ -347,6 +449,71 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase } } } +' + ), + 'works on undefined ones' => array( + '{ + "repositories": { + "main": { + "foo": "bar", + "bar": "baz" + } + } +}', + 'removenotthere', + true, + '{ + "repositories": { + "main": { + "foo": "bar", + "bar": "baz" + } + } +} +' + ), + 'works on child having unmatched name' => array( + '{ + "repositories": { + "baz": { + "foo": "bar", + "bar": "baz" + } + } +}', + 'bar', + true, + '{ + "repositories": { + "baz": { + "foo": "bar", + "bar": "baz" + } + } +} +' + ), + 'works on child having duplicate name' => array( + '{ + "repositories": { + "foo": { + "baz": "qux" + }, + "baz": { + "foo": "bar", + "bar": "baz" + } + } +}', + 'baz', + true, + '{ + "repositories": { + "foo": { + "baz": "qux" + } + } +} ' ), 'works on empty repos' => array( @@ -403,6 +570,28 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase "package": { "bar": "ba}z" } } } +}', + 'bar', + false + ), + 'fails on deep arrays with borked texts' => array( + '{ + "repositories": [ + { + "package": { "bar": "ba[z" } + } + ] +}', + 'bar', + false + ), + 'fails on deep arrays with borked texts2' => array( + '{ + "repositories": [ + { + "package": { "bar": "ba]z" } + } + ] }', 'bar', false @@ -410,6 +599,108 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase ); } + public function testRemoveSubNodeFromRequire() + { + $manipulator = new JsonManipulator('{ + "repositories": [ + { + "package": { + "require": { + "this/should-not-end-up-in-root-require": "~2.0" + }, + "require-dev": { + "this/should-not-end-up-in-root-require-dev": "~2.0" + } + } + } + ], + "require": { + "package/a": "*", + "package/b": "*", + "package/c": "*" + }, + "require-dev": { + "package/d": "*" + } +}'); + + $this->assertTrue($manipulator->removeSubNode('require', 'package/c')); + $this->assertTrue($manipulator->removeSubNode('require-dev', 'package/d')); + $this->assertEquals('{ + "repositories": [ + { + "package": { + "require": { + "this/should-not-end-up-in-root-require": "~2.0" + }, + "require-dev": { + "this/should-not-end-up-in-root-require-dev": "~2.0" + } + } + } + ], + "require": { + "package/a": "*", + "package/b": "*" + }, + "require-dev": { + } +} +', $manipulator->getContents()); + } + + public function testAddSubNodeInRequire() + { + $manipulator = new JsonManipulator('{ + "repositories": [ + { + "package": { + "require": { + "this/should-not-end-up-in-root-require": "~2.0" + }, + "require-dev": { + "this/should-not-end-up-in-root-require-dev": "~2.0" + } + } + } + ], + "require": { + "package/a": "*", + "package/b": "*" + }, + "require-dev": { + "package/d": "*" + } +}'); + + $this->assertTrue($manipulator->addSubNode('require', 'package/c', '*')); + $this->assertTrue($manipulator->addSubNode('require-dev', 'package/e', '*')); + $this->assertEquals('{ + "repositories": [ + { + "package": { + "require": { + "this/should-not-end-up-in-root-require": "~2.0" + }, + "require-dev": { + "this/should-not-end-up-in-root-require-dev": "~2.0" + } + } + } + ], + "require": { + "package/a": "*", + "package/b": "*", + "package/c": "*" + }, + "require-dev": { + "package/d": "*", + "package/e": "*" + } +} +', $manipulator->getContents()); + } + public function testAddRepositoryCanInitializeEmptyRepositories() { $manipulator = new JsonManipulator('{ @@ -670,6 +961,24 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase ', $manipulator->getContents()); } + public function testAddRootSettingDoesNotBreakDots() + { + $manipulator = new JsonManipulator('{ + "github-oauth": { + "github.com": "foo" + } +}'); + + $this->assertTrue($manipulator->addSubNode('github-oauth', 'bar', 'baz')); + $this->assertEquals('{ + "github-oauth": { + "github.com": "foo", + "bar": "baz" + } +} +', $manipulator->getContents()); + } + public function testRemoveConfigSettingCanRemoveSubKeyInHash() { $manipulator = new JsonManipulator('{ diff --git a/tests/Composer/Test/Json/JsonValidationExceptionTest.php b/tests/Composer/Test/Json/JsonValidationExceptionTest.php new file mode 100644 index 000000000..76959d688 --- /dev/null +++ b/tests/Composer/Test/Json/JsonValidationExceptionTest.php @@ -0,0 +1,42 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Json; + +use Composer\Json\JsonValidationException; + +class JsonValidationExceptionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider errorProvider + */ + public function testGetErrors($message, $errors) + { + $object = new JsonValidationException($message, $errors); + $this->assertEquals($message, $object->getMessage()); + $this->assertEquals($errors, $object->getErrors()); + } + + public function testGetErrorsWhenNoErrorsProvided() + { + $object = new JsonValidationException('test message'); + $this->assertEquals(array(), $object->getErrors()); + } + + public function errorProvider() + { + return array( + array('test message', array()), + array(null, null) + ); + } +} diff --git a/tests/Composer/Test/Mock/FactoryMock.php b/tests/Composer/Test/Mock/FactoryMock.php index 11b266590..52c3fbf2e 100644 --- a/tests/Composer/Test/Mock/FactoryMock.php +++ b/tests/Composer/Test/Mock/FactoryMock.php @@ -16,14 +16,15 @@ use Composer\Config; use Composer\Factory; use Composer\Repository; use Composer\Repository\RepositoryManager; +use Composer\Repository\WritableRepositoryInterface; use Composer\Installer; use Composer\IO\IOInterface; class FactoryMock extends Factory { - public static function createConfig() + public static function createConfig(IOInterface $io = null, $cwd = null) { - $config = new Config(); + $config = new Config(true, $cwd); $config->merge(array( 'config' => array('home' => sys_get_temp_dir().'/composer-test'), @@ -46,7 +47,7 @@ class FactoryMock extends Factory { } - protected function purgePackages(Repository\RepositoryManager $rm, Installer\InstallationManager $im) + protected function purgePackages(WritableRepositoryInterface $repo, Installer\InstallationManager $im) { } } diff --git a/tests/Composer/Test/Mock/RemoteFilesystemMock.php b/tests/Composer/Test/Mock/RemoteFilesystemMock.php index caf1c5e65..79a25e550 100644 --- a/tests/Composer/Test/Mock/RemoteFilesystemMock.php +++ b/tests/Composer/Test/Mock/RemoteFilesystemMock.php @@ -36,5 +36,4 @@ class RemoteFilesystemMock extends RemoteFilesystem throw new TransportException('The "'.$fileUrl.'" file could not be downloaded (NOT FOUND)', 404); } - } diff --git a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php index 536f2128c..bc74be1e9 100644 --- a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php @@ -46,6 +46,24 @@ class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase 'B/sub/prefixD.foo', 'B/sub/prefixE.foo', 'B/sub/prefixF.foo', + 'C/prefixA.foo', + 'C/prefixB.foo', + 'C/prefixC.foo', + 'C/prefixD.foo', + 'C/prefixE.foo', + 'C/prefixF.foo', + 'D/prefixA', + 'D/prefixB', + 'D/prefixC', + 'D/prefixD', + 'D/prefixE', + 'D/prefixF', + 'E/subtestA.foo', + 'F/subtestA.foo', + 'G/subtestA.foo', + 'H/subtestA.foo', + 'I/J/subtestA.foo', + 'K/dirJ/subtestA.foo', 'toplevelA.foo', 'toplevelB.foo', 'prefixA.foo', @@ -54,6 +72,10 @@ class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase 'prefixD.foo', 'prefixE.foo', 'prefixF.foo', + 'parameters.yml', + 'parameters.yml.dist', + '!important!.txt', + '!important_too!.txt' ); foreach ($fileTree as $relativePath) { @@ -82,6 +104,8 @@ class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase $this->finder = new ArchivableFilesFinder($this->sources, $excludes); $this->assertArchivableFiles(array( + '/!important!.txt', + '/!important_too!.txt', '/A/prefixA.foo', '/A/prefixD.foo', '/A/prefixE.foo', @@ -91,6 +115,24 @@ class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase '/B/sub/prefixD.foo', '/B/sub/prefixE.foo', '/B/sub/prefixF.foo', + '/C/prefixA.foo', + '/C/prefixD.foo', + '/C/prefixE.foo', + '/C/prefixF.foo', + '/D/prefixA', + '/D/prefixB', + '/D/prefixC', + '/D/prefixD', + '/D/prefixE', + '/D/prefixF', + '/E/subtestA.foo', + '/F/subtestA.foo', + '/G/subtestA.foo', + '/H/subtestA.foo', + '/I/J/subtestA.foo', + '/K/dirJ/subtestA.foo', + '/parameters.yml', + '/parameters.yml.dist', '/prefixB.foo', '/prefixD.foo', '/prefixE.foo', @@ -120,6 +162,15 @@ class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase '!/*/*/prefixF.foo', '', 'refixD.foo', + '/C', + 'D/prefixA', + 'E', + 'F/', + 'G/*', + 'H/**', + 'J/', + 'parameters.yml', + '\!important!.txt' ))); // git does not currently support negative git attributes @@ -160,9 +211,15 @@ class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase '# comments', '', '^prefixD.foo', + 'D/prefixA', + 'parameters.yml', + '\!important!.txt', + 'E', + 'F/', 'syntax: glob', 'prefixF.*', 'B/*', + 'H/**', ))); $this->finder = new ArchivableFilesFinder($this->sources, array()); @@ -173,7 +230,9 @@ class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase 'hg archive archive.zip' ); - array_shift($expectedFiles); // remove .hg_archival.txt + // Remove .hg_archival.txt from the expectedFiles + $archiveKey = array_search('/.hg_archival.txt', $expectedFiles); + array_splice($expectedFiles, $archiveKey, 1); $this->assertArchivableFiles($expectedFiles); } diff --git a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php index 5a9f8c2d8..aa98a62a2 100644 --- a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php @@ -13,7 +13,6 @@ namespace Composer\Test\Package\Archiver; use Composer\Factory; -use Composer\Package\Archiver; use Composer\Package\PackageInterface; class ArchiveManagerTest extends ArchiverTest diff --git a/tests/Composer/Test/Package/Archiver/GitExcludeFilterTest.php b/tests/Composer/Test/Package/Archiver/GitExcludeFilterTest.php new file mode 100644 index 000000000..97c02c8e6 --- /dev/null +++ b/tests/Composer/Test/Package/Archiver/GitExcludeFilterTest.php @@ -0,0 +1,36 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Package\Archiver; + +use Composer\Package\Archiver\GitExcludeFilter; + +class GitExcludeFilterTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider patterns + */ + public function testPatternEscape($ignore, $expected) + { + $filter = new GitExcludeFilter('/'); + + $this->assertEquals($expected, $filter->parseGitIgnoreLine($ignore)); + } + + public function patterns() + { + return array( + array('app/config/parameters.yml', array('#(?=[^\.])app/(?=[^\.])config/(?=[^\.])parameters\.yml(?=$|/)#', false, false)), + array('!app/config/parameters.yml', array('#(?=[^\.])app/(?=[^\.])config/(?=[^\.])parameters\.yml(?=$|/)#', true, false)), + ); + } +} diff --git a/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php b/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php index 4b9877523..f1889a1ce 100644 --- a/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php +++ b/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php @@ -62,12 +62,33 @@ class ArrayDumperTest extends \PHPUnit_Framework_TestCase $this->assertSame('dev', $config['minimum-stability']); } + public function testDumpAbandoned() + { + $this->packageExpects('isAbandoned', true); + $this->packageExpects('getReplacementPackage', true); + + $config = $this->dumper->dump($this->package); + + $this->assertSame(true, $config['abandoned']); + } + + public function testDumpAbandonedReplacement() + { + $this->packageExpects('isAbandoned', true); + $this->packageExpects('getReplacementPackage', 'foo/bar'); + + $config = $this->dumper->dump($this->package); + + $this->assertSame('foo/bar', $config['abandoned']); + } + /** * @dataProvider getKeys */ public function testKeys($key, $value, $method = null, $expectedValue = null) { $this->packageExpects('get'.ucfirst($method ?: $key), $value); + $this->packageExpects('isAbandoned', $value); $config = $this->dumper->dump($this->package); @@ -194,6 +215,11 @@ class ArrayDumperTest extends \PHPUnit_Framework_TestCase array(new Link('foo', 'foo/bar', new VersionConstraint('=', '1.0.0.0'), 'requires', '1.0.0'), new Link('bar', 'bar/baz', new VersionConstraint('=', '1.0.0.0'), 'requires', '1.0.0')), 'conflicts', array('bar/baz' => '1.0.0', 'foo/bar' => '1.0.0') + ), + array( + 'transport-options', + array('ssl' => array('local_cert' => '/opt/certs/test.pem')), + 'transportOptions' ) ); } diff --git a/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php b/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php index 248e251ef..1491571a1 100644 --- a/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php @@ -19,7 +19,7 @@ class ArrayLoaderTest extends \PHPUnit_Framework_TestCase { public function setUp() { - $this->loader = new ArrayLoader(); + $this->loader = new ArrayLoader(null, true); } public function testSelfVersion() @@ -117,6 +117,8 @@ class ArrayLoaderTest extends \PHPUnit_Framework_TestCase 'archive' => array( 'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'), ), + 'transport-options' => array('ssl' => array('local_cert' => '/opt/certs/test.pem')), + 'abandoned' => 'foo/bar' ); $package = $this->loader->load($config); @@ -136,5 +138,73 @@ class ArrayLoaderTest extends \PHPUnit_Framework_TestCase $this->assertInstanceOf('Composer\Package\AliasPackage', $package); $this->assertEquals('1.0.x-dev', $package->getPrettyVersion()); + + $config = array( + 'name' => 'A', + 'version' => 'dev-master', + 'extra' => array('branch-alias' => array('dev-master' => '1.0-dev')), + ); + + $package = $this->loader->load($config); + + $this->assertInstanceOf('Composer\Package\AliasPackage', $package); + $this->assertEquals('1.0.x-dev', $package->getPrettyVersion()); + + $config = array( + 'name' => 'B', + 'version' => '4.x-dev', + 'extra' => array('branch-alias' => array('4.x-dev' => '4.0.x-dev')), + ); + + $package = $this->loader->load($config); + + $this->assertInstanceOf('Composer\Package\AliasPackage', $package); + $this->assertEquals('4.0.x-dev', $package->getPrettyVersion()); + + $config = array( + 'name' => 'B', + 'version' => '4.x-dev', + 'extra' => array('branch-alias' => array('4.x-dev' => '4.0-dev')), + ); + + $package = $this->loader->load($config); + + $this->assertInstanceOf('Composer\Package\AliasPackage', $package); + $this->assertEquals('4.0.x-dev', $package->getPrettyVersion()); + + $config = array( + 'name' => 'C', + 'version' => '4.x-dev', + 'extra' => array('branch-alias' => array('4.x-dev' => '3.4.x-dev')), + ); + + $package = $this->loader->load($config); + + $this->assertInstanceOf('Composer\Package\CompletePackage', $package); + $this->assertEquals('4.x-dev', $package->getPrettyVersion()); + } + + public function testAbandoned() + { + $config = array( + 'name' => 'A', + 'version' => '1.2.3.4', + 'abandoned' => 'foo/bar' + ); + + $package = $this->loader->load($config); + $this->assertTrue($package->isAbandoned()); + $this->assertEquals('foo/bar', $package->getReplacementPackage()); + } + + public function testNotAbandoned() + { + $config = array( + 'name' => 'A', + 'version' => '1.2.3.4' + ); + + $package = $this->loader->load($config); + $this->assertFalse($package->isAbandoned()); } } diff --git a/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php b/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php index 1a6a3bf78..d1f3be1f8 100644 --- a/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php @@ -35,7 +35,7 @@ class RootPackageLoaderTest extends \PHPUnit_Framework_TestCase $self = $this; /* Can do away with this mock object when https://github.com/sebastianbergmann/phpunit-mock-objects/issues/81 is fixed */ - $processExecutor = new ProcessExecutorMock(function($command, &$output = null, $cwd = null) use ($self, $commitHash) { + $processExecutor = new ProcessExecutorMock(function ($command, &$output = null, $cwd = null) use ($self, $commitHash) { if (0 === strpos($command, 'git describe')) { // simulate not being on a tag return 1; @@ -69,7 +69,7 @@ class RootPackageLoaderTest extends \PHPUnit_Framework_TestCase $self = $this; /* Can do away with this mock object when https://github.com/sebastianbergmann/phpunit-mock-objects/issues/81 is fixed */ - $processExecutor = new ProcessExecutorMock(function($command, &$output = null, $cwd = null) use ($self) { + $processExecutor = new ProcessExecutorMock(function ($command, &$output = null, $cwd = null) use ($self) { $self->assertEquals('git describe --exact-match --tags', $command); $output = "v2.0.5-alpha2"; @@ -98,7 +98,7 @@ class RootPackageLoaderTest extends \PHPUnit_Framework_TestCase $self = $this; /* Can do away with this mock object when https://github.com/sebastianbergmann/phpunit-mock-objects/issues/81 is fixed */ - $processExecutor = new ProcessExecutorMock(function($command, &$output = null, $cwd = null) use ($self) { + $processExecutor = new ProcessExecutorMock(function ($command, &$output = null, $cwd = null) use ($self) { if ('git describe --exact-match --tags' === $command) { $output = "foo-bar"; @@ -124,7 +124,7 @@ class RootPackageLoaderTest extends \PHPUnit_Framework_TestCase ->disableOriginalConstructor() ->getMock(); - $processExecutor = new ProcessExecutorMock(function($command, &$output = null, $cwd = null) { + $processExecutor = new ProcessExecutorMock(function ($command, &$output = null, $cwd = null) { return 1; }); @@ -143,6 +143,7 @@ class RootPackageLoaderTest extends \PHPUnit_Framework_TestCase 'foo/bar' => '~2.1.0-beta2', 'bar/baz' => '1.0.x-dev as 1.2.0', 'qux/quux' => '1.0.*@rc', + 'zux/complex' => '~1.0,>=1.0.2@dev' ), 'minimum-stability' => 'alpha', )); @@ -151,6 +152,7 @@ class RootPackageLoaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array( 'bar/baz' => BasePackage::STABILITY_DEV, 'qux/quux' => BasePackage::STABILITY_RC, + 'zux/complex' => BasePackage::STABILITY_DEV, ), $package->getStabilityFlags()); } } diff --git a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php index 9b1982b9d..32c55bd40 100644 --- a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php @@ -29,7 +29,7 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase ->method('load') ->with($config); - $loader = new ValidatingArrayLoader($internalLoader); + $loader = new ValidatingArrayLoader($internalLoader, true, null, ValidatingArrayLoader::CHECK_ALL); $loader->load($config); } @@ -73,14 +73,17 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase ), 'require' => array( 'a/b' => '1.*', + 'b/c' => '~2', 'example' => '>2.0-dev,<2.4-dev', ), 'require-dev' => array( 'a/b' => '1.*', + 'b/c' => '*', 'example' => '>2.0-dev,<2.4-dev', ), 'conflict' => array( 'a/b' => '1.*', + 'b/c' => '>2.7', 'example' => '>2.0-dev,<2.4-dev', ), 'replace' => array( @@ -137,12 +140,14 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase 'branch-alias' => array( 'dev-master' => '2.0-dev', 'dev-old' => '1.0.x-dev', + '3.x-dev' => '3.1.x-dev' ), ), 'bin' => array( 'bin/foo', 'bin/bar', ), + 'transport-options' => array('ssl' => array('local_cert' => '/opt/certs/test.pem')) ), ), array( // test as array @@ -160,7 +165,7 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase public function testLoadFailureThrowsException($config, $expectedErrors) { $internalLoader = $this->getMock('Composer\Package\Loader\LoaderInterface'); - $loader = new ValidatingArrayLoader($internalLoader); + $loader = new ValidatingArrayLoader($internalLoader, true, null, ValidatingArrayLoader::CHECK_ALL); try { $loader->load($config); $this->fail('Expected exception to be thrown'); @@ -178,7 +183,7 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase public function testLoadWarnings($config, $expectedWarnings) { $internalLoader = $this->getMock('Composer\Package\Loader\LoaderInterface'); - $loader = new ValidatingArrayLoader($internalLoader); + $loader = new ValidatingArrayLoader($internalLoader, true, null, ValidatingArrayLoader::CHECK_ALL); $loader->load($config); $warnings = $loader->getWarnings(); @@ -190,15 +195,20 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase /** * @dataProvider warningProvider */ - public function testLoadSkipsWarningDataWhenIgnoringErrors($config) + public function testLoadSkipsWarningDataWhenIgnoringErrors($config, $expectedWarnings, $mustCheck = true) { + if (!$mustCheck) { + $this->assertTrue(true); + + return; + } $internalLoader = $this->getMock('Composer\Package\Loader\LoaderInterface'); $internalLoader ->expects($this->once()) ->method('load') ->with(array('name' => 'a/b')); - $loader = new ValidatingArrayLoader($internalLoader); + $loader = new ValidatingArrayLoader($internalLoader, true, null, ValidatingArrayLoader::CHECK_ALL); $config['name'] = 'a/b'; $loader->load($config); } @@ -256,6 +266,15 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase 'autoload : invalid value (psr0), must be one of psr-0, psr-4, classmap, files' ) ), + array( + array( + 'name' => 'foo/bar', + 'transport-options' => 'test', + ), + array( + 'transport-options : should be an array, string given' + ) + ), ); } @@ -288,6 +307,52 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase 'support.wiki : invalid value (foo:bar), must be an http/https URL', ) ), + array( + array( + 'name' => 'foo/bar', + 'require' => array( + 'foo/baz' => '*', + 'bar/baz' => '>=1.0', + 'bar/foo' => 'dev-master', + 'bar/hacked' => '@stable', + ), + ), + array( + 'require.foo/baz : unbound version constraints (*) should be avoided', + 'require.bar/baz : unbound version constraints (>=1.0) should be avoided', + 'require.bar/foo : unbound version constraints (dev-master) should be avoided', + 'require.bar/hacked : unbound version constraints (@stable) should be avoided', + ), + false + ), + array( + array( + 'name' => 'foo/bar', + 'extra' => array( + 'branch-alias' => array( + '5.x-dev' => '3.1.x-dev' + ), + ) + ), + array( + 'extra.branch-alias.5.x-dev : the target branch (3.1.x-dev) is not a valid numeric alias for this version' + ), + false + ), + array( + array( + 'name' => 'foo/bar', + 'extra' => array( + 'branch-alias' => array( + '5.x-dev' => '3.1-dev' + ), + ) + ), + array( + 'extra.branch-alias.5.x-dev : the target branch (3.1-dev) is not a valid numeric alias for this version' + ), + false + ), ); } } diff --git a/tests/Composer/Test/Package/LockerTest.php b/tests/Composer/Test/Package/LockerTest.php index 1b879f421..913868cd1 100644 --- a/tests/Composer/Test/Package/LockerTest.php +++ b/tests/Composer/Test/Package/LockerTest.php @@ -120,7 +120,9 @@ class LockerTest extends \PHPUnit_Framework_TestCase ->expects($this->once()) ->method('write') ->with(array( - '_readme' => array('This file locks the dependencies of your project to a known state', 'Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file'), + '_readme' => array('This file locks the dependencies of your project to a known state', + 'Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file', + 'This file is @gener'.'ated automatically'), 'hash' => 'md5', 'packages' => array( array('name' => 'pkg1', 'version' => '1.0.0-beta'), @@ -132,9 +134,11 @@ class LockerTest extends \PHPUnit_Framework_TestCase 'stability-flags' => array(), 'platform' => array(), 'platform-dev' => array(), + 'prefer-stable' => false, + 'prefer-lowest' => false, )); - $locker->setLockData(array($package1, $package2), array(), array(), array(), array(), 'dev', array()); + $locker->setLockData(array($package1, $package2), array(), array(), array(), array(), 'dev', array(), false, false); } public function testLockBadPackages() @@ -153,7 +157,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase $this->setExpectedException('LogicException'); - $locker->setLockData(array($package1), array(), array(), array(), array(), 'dev', array()); + $locker->setLockData(array($package1), array(), array(), array(), array(), 'dev', array(), false, false); } public function testIsFresh() diff --git a/tests/Composer/Test/Package/Version/VersionParserTest.php b/tests/Composer/Test/Package/Version/VersionParserTest.php index 18b5c493f..13920e2f0 100644 --- a/tests/Composer/Test/Package/Version/VersionParserTest.php +++ b/tests/Composer/Test/Package/Version/VersionParserTest.php @@ -54,7 +54,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase ); $self = $this; - $createPackage = function($arr) use ($self) { + $createPackage = function ($arr) use ($self) { $package = $self->getMock('\Composer\Package\PackageInterface'); $package->expects($self->once())->method('isDev')->will($self->returnValue(true)); $package->expects($self->once())->method('getSourceType')->will($self->returnValue('git')); @@ -67,6 +67,29 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase return array_map($createPackage, $data); } + /** + * @dataProvider numericAliasVersions + */ + public function testParseNumericAliasPrefix($input, $expected) + { + $parser = new VersionParser; + $this->assertSame($expected, $parser->parseNumericAliasPrefix($input)); + } + + public function numericAliasVersions() + { + return array( + array('0.x-dev', '0.'), + array('1.0.x-dev', '1.0.'), + array('1.x-dev', '1.'), + array('1.2.x-dev', '1.2.'), + array('1.2-dev', '1.2.'), + array('1-dev', '1.'), + array('dev-develop', false), + array('dev-master', false), + ); + } + /** * @dataProvider successfulNormalizedVersions */ @@ -79,34 +102,41 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase public function successfulNormalizedVersions() { return array( - 'none' => array('1.0.0', '1.0.0.0'), - 'none/2' => array('1.2.3.4', '1.2.3.4'), - 'parses state' => array('1.0.0RC1dev', '1.0.0.0-RC1-dev'), - 'CI parsing' => array('1.0.0-rC15-dev', '1.0.0.0-RC15-dev'), - 'delimiters' => array('1.0.0.RC.15-dev', '1.0.0.0-RC15-dev'), - 'RC uppercase' => array('1.0.0-rc1', '1.0.0.0-RC1'), - 'patch replace' => array('1.0.0.pl3-dev', '1.0.0.0-patch3-dev'), - 'forces w.x.y.z' => array('1.0-dev', '1.0.0.0-dev'), - 'forces w.x.y.z/2' => array('0', '0.0.0.0'), - 'parses long' => array('10.4.13-beta', '10.4.13.0-beta'), - 'expand shorthand' => array('10.4.13-b', '10.4.13.0-beta'), - 'expand shorthand2' => array('10.4.13-b5', '10.4.13.0-beta5'), - 'strips leading v' => array('v1.0.0', '1.0.0.0'), - 'strips v/datetime' => array('v20100102', '20100102'), - 'parses dates y-m' => array('2010.01', '2010-01'), - 'parses dates w/ .' => array('2010.01.02', '2010-01-02'), - 'parses dates w/ -' => array('2010-01-02', '2010-01-02'), - 'parses numbers' => array('2010-01-02.5', '2010-01-02-5'), - 'parses datetime' => array('20100102-203040', '20100102-203040'), - 'parses dt+number' => array('20100102203040-10', '20100102203040-10'), - 'parses dt+patch' => array('20100102-203040-p1', '20100102-203040-patch1'), - 'parses master' => array('dev-master', '9999999-dev'), - 'parses trunk' => array('dev-trunk', '9999999-dev'), - 'parses branches' => array('1.x-dev', '1.9999999.9999999.9999999-dev'), - 'parses arbitrary' => array('dev-feature-foo', 'dev-feature-foo'), - 'parses arbitrary2' => array('DEV-FOOBAR', 'dev-FOOBAR'), - 'parses arbitrary3' => array('dev-feature/foo', 'dev-feature/foo'), - 'ignores aliases' => array('dev-master as 1.0.0', '9999999-dev'), + 'none' => array('1.0.0', '1.0.0.0'), + 'none/2' => array('1.2.3.4', '1.2.3.4'), + 'parses state' => array('1.0.0RC1dev', '1.0.0.0-RC1-dev'), + 'CI parsing' => array('1.0.0-rC15-dev', '1.0.0.0-RC15-dev'), + 'delimiters' => array('1.0.0.RC.15-dev', '1.0.0.0-RC15-dev'), + 'RC uppercase' => array('1.0.0-rc1', '1.0.0.0-RC1'), + 'patch replace' => array('1.0.0.pl3-dev', '1.0.0.0-patch3-dev'), + 'forces w.x.y.z' => array('1.0-dev', '1.0.0.0-dev'), + 'forces w.x.y.z/2' => array('0', '0.0.0.0'), + 'parses long' => array('10.4.13-beta', '10.4.13.0-beta'), + 'parses long/2' => array('10.4.13beta2', '10.4.13.0-beta2'), + 'parses long/semver' => array('10.4.13beta.2', '10.4.13.0-beta2'), + 'expand shorthand' => array('10.4.13-b', '10.4.13.0-beta'), + 'expand shorthand2' => array('10.4.13-b5', '10.4.13.0-beta5'), + 'strips leading v' => array('v1.0.0', '1.0.0.0'), + 'strips v/datetime' => array('v20100102', '20100102'), + 'parses dates y-m' => array('2010.01', '2010-01'), + 'parses dates w/ .' => array('2010.01.02', '2010-01-02'), + 'parses dates w/ -' => array('2010-01-02', '2010-01-02'), + 'parses numbers' => array('2010-01-02.5', '2010-01-02-5'), + 'parses dates y.m.Y' => array('2010.1.555', '2010.1.555.0'), + 'parses datetime' => array('20100102-203040', '20100102-203040'), + 'parses dt+number' => array('20100102203040-10', '20100102203040-10'), + 'parses dt+patch' => array('20100102-203040-p1', '20100102-203040-patch1'), + 'parses master' => array('dev-master', '9999999-dev'), + 'parses trunk' => array('dev-trunk', '9999999-dev'), + 'parses branches' => array('1.x-dev', '1.9999999.9999999.9999999-dev'), + 'parses arbitrary' => array('dev-feature-foo', 'dev-feature-foo'), + 'parses arbitrary2' => array('DEV-FOOBAR', 'dev-FOOBAR'), + 'parses arbitrary3' => array('dev-feature/foo', 'dev-feature/foo'), + 'ignores aliases' => array('dev-master as 1.0.0', '9999999-dev'), + 'semver metadata' => array('dev-master+foo.bar', '9999999-dev'), + 'semver metadata/2' => array('1.0.0-beta.5+foo', '1.0.0.0-beta5'), + 'semver metadata/3' => array('1.0.0+foo', '1.0.0.0'), + 'metadata w/ alias' => array('1.0.0+foo as 2.0', '1.0.0.0'), ); } @@ -128,6 +158,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase 'invalid type' => array('1.0.0-meh'), 'too many bits' => array('1.0.0.0.0'), 'non-dev arbitrary' => array('feature-foo'), + 'metadata w/ space' => array('1.0.0+foo bar'), ); } @@ -206,7 +237,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase 'match any' => array('*', new EmptyConstraint()), 'match any/2' => array('*.*', new EmptyConstraint()), 'match any/3' => array('*.x.*', new EmptyConstraint()), - 'match any/4' => array('x.x.x.*', new EmptyConstraint()), + 'match any/4' => array('x.X.x.*', new EmptyConstraint()), 'not equal' => array('<>1.0.0', new VersionConstraint('<>', '1.0.0.0')), 'not equal/2' => array('!=1.0.0', new VersionConstraint('!=', '1.0.0.0')), 'greater than' => array('>1.0.0', new VersionConstraint('>', '1.0.0.0')), @@ -219,6 +250,8 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase 'completes version' => array('=1.0', new VersionConstraint('=', '1.0.0.0')), 'shorthand beta' => array('1.2.3b5', new VersionConstraint('=', '1.2.3.0-beta5')), 'accepts spaces' => array('>= 1.2.3', new VersionConstraint('>=', '1.2.3.0')), + 'accepts spaces/2' => array('< 1.2.3', new VersionConstraint('<', '1.2.3.0-dev')), + 'accepts spaces/3' => array('> 1.2.3', new VersionConstraint('>', '1.2.3.0')), 'accepts master' => array('>=dev-master', new VersionConstraint('>=', '9999999-dev')), 'accepts master/2' => array('dev-master', new VersionConstraint('=', '9999999-dev')), 'accepts arbitrary' => array('dev-feature-a', new VersionConstraint('=', 'dev-feature-a')), @@ -251,7 +284,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase array('20.*', new VersionConstraint('>=', '20.0.0.0-dev'), new VersionConstraint('<', '21.0.0.0-dev')), array('2.0.*', new VersionConstraint('>=', '2.0.0.0-dev'), new VersionConstraint('<', '2.1.0.0-dev')), array('2.2.x', new VersionConstraint('>=', '2.2.0.0-dev'), new VersionConstraint('<', '2.3.0.0-dev')), - array('2.10.x', new VersionConstraint('>=', '2.10.0.0-dev'), new VersionConstraint('<', '2.11.0.0-dev')), + array('2.10.X', new VersionConstraint('>=', '2.10.0.0-dev'), new VersionConstraint('<', '2.11.0.0-dev')), array('2.1.3.*', new VersionConstraint('>=', '2.1.3.0-dev'), new VersionConstraint('<', '2.1.4.0-dev')), array('0.*', null, new VersionConstraint('<', '1.0.0.0-dev')), ); @@ -289,16 +322,112 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase ); } - public function testParseConstraintsMulti() + /** + * @dataProvider caretConstraints + */ + public function testParseCaretWildcard($input, $min, $max) + { + $parser = new VersionParser; + if ($min) { + $expected = new MultiConstraint(array($min, $max)); + } else { + $expected = $max; + } + + $this->assertSame((string) $expected, (string) $parser->parseConstraints($input)); + } + + public function caretConstraints() + { + return array( + array('^1', new VersionConstraint('>=', '1.0.0.0-dev'), new VersionConstraint('<', '2.0.0.0-dev')), + array('^0', new VersionConstraint('>=', '0.0.0.0-dev'), new VersionConstraint('<', '1.0.0.0-dev')), + array('^0.0', new VersionConstraint('>=', '0.0.0.0-dev'), new VersionConstraint('<', '0.1.0.0-dev')), + array('^1.2', new VersionConstraint('>=', '1.2.0.0-dev'), new VersionConstraint('<', '2.0.0.0-dev')), + array('^1.2.3-beta.2', new VersionConstraint('>=', '1.2.3.0-beta2'), new VersionConstraint('<', '2.0.0.0-dev')), + array('^1.2.3.4', new VersionConstraint('>=', '1.2.3.4-dev'), new VersionConstraint('<', '2.0.0.0-dev')), + array('^1.2.3', new VersionConstraint('>=', '1.2.3.0-dev'), new VersionConstraint('<', '2.0.0.0-dev')), + array('^0.2.3', new VersionConstraint('>=', '0.2.3.0-dev'), new VersionConstraint('<', '0.3.0.0-dev')), + array('^0.2', new VersionConstraint('>=', '0.2.0.0-dev'), new VersionConstraint('<', '0.3.0.0-dev')), + array('^0.0.3', new VersionConstraint('>=', '0.0.3.0-dev'), new VersionConstraint('<', '0.0.4.0-dev')), + array('^0.0.3-alpha', new VersionConstraint('>=', '0.0.3.0-alpha'), new VersionConstraint('<', '0.0.4.0-dev')), + array('^0.0.3-dev', new VersionConstraint('>=', '0.0.3.0-dev'), new VersionConstraint('<', '0.0.4.0-dev')), + ); + } + + /** + * @dataProvider hyphenConstraints + */ + public function testParseHyphen($input, $min, $max) + { + $parser = new VersionParser; + if ($min) { + $expected = new MultiConstraint(array($min, $max)); + } else { + $expected = $max; + } + + $this->assertSame((string) $expected, (string) $parser->parseConstraints($input)); + } + + public function hyphenConstraints() + { + return array( + array('1 - 2', new VersionConstraint('>=', '1.0.0.0-dev'), new VersionConstraint('<', '3.0.0.0-dev')), + array('1.2.3 - 2.3.4.5', new VersionConstraint('>=', '1.2.3.0-dev'), new VersionConstraint('<=', '2.3.4.5')), + array('1.2-beta - 2.3', new VersionConstraint('>=', '1.2.0.0-beta'), new VersionConstraint('<', '2.4.0.0-dev')), + array('1.2-beta - 2.3-dev', new VersionConstraint('>=', '1.2.0.0-beta'), new VersionConstraint('<=', '2.3.0.0-dev')), + array('1.2-RC - 2.3.1', new VersionConstraint('>=', '1.2.0.0-RC'), new VersionConstraint('<=', '2.3.1.0')), + array('1.2.3-alpha - 2.3-RC', new VersionConstraint('>=', '1.2.3.0-alpha'), new VersionConstraint('<=', '2.3.0.0-RC')), + ); + } + + /** + * @dataProvider multiConstraintProvider + */ + public function testParseConstraintsMulti($constraint) { $parser = new VersionParser; $first = new VersionConstraint('>', '2.0.0.0'); $second = new VersionConstraint('<=', '3.0.0.0'); $multi = new MultiConstraint(array($first, $second)); - $this->assertSame((string) $multi, (string) $parser->parseConstraints('>2.0,<=3.0')); + $this->assertSame((string) $multi, (string) $parser->parseConstraints($constraint)); + } + + public function multiConstraintProvider() + { + return array( + array('>2.0,<=3.0'), + array('>2.0 <=3.0'), + array('>2.0 <=3.0'), + array('>2.0, <=3.0'), + array('>2.0 ,<=3.0'), + array('>2.0 , <=3.0'), + array('>2.0 , <=3.0'), + array('> 2.0 <= 3.0'), + array('> 2.0 , <= 3.0'), + array(' > 2.0 , <= 3.0 '), + ); + } + + public function testParseConstraintsMultiWithStabilitySuffix() + { + $parser = new VersionParser; + $first = new VersionConstraint('>=', '1.1.0.0-alpha4'); + $second = new VersionConstraint('<', '1.2.9999999.9999999-dev'); + $multi = new MultiConstraint(array($first, $second)); + $this->assertSame((string) $multi, (string) $parser->parseConstraints('>=1.1.0-alpha4,<1.2.x-dev')); + + $first = new VersionConstraint('>=', '1.1.0.0-alpha4'); + $second = new VersionConstraint('<', '1.2.0.0-beta2'); + $multi = new MultiConstraint(array($first, $second)); + $this->assertSame((string) $multi, (string) $parser->parseConstraints('>=1.1.0-alpha4,<1.2-beta2')); } - public function testParseConstraintsMultiDisjunctiveHasPrioOverConjuctive() + /** + * @dataProvider multiConstraintProvider2 + */ + public function testParseConstraintsMultiDisjunctiveHasPrioOverConjuctive($constraint) { $parser = new VersionParser; $first = new VersionConstraint('>', '2.0.0.0'); @@ -306,7 +435,16 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase $third = new VersionConstraint('>', '2.0.6.0'); $multi1 = new MultiConstraint(array($first, $second)); $multi2 = new MultiConstraint(array($multi1, $third), false); - $this->assertSame((string) $multi2, (string) $parser->parseConstraints('>2.0,<2.0.5 | >2.0.6')); + $this->assertSame((string) $multi2, (string) $parser->parseConstraints($constraint)); + } + + public function multiConstraintProvider2() + { + return array( + array('>2.0,<2.0.5 | >2.0.6'), + array('>2.0,<2.0.5 || >2.0.6'), + array('> 2.0 , <2.0.5 | > 2.0.6'), + ); } public function testParseConstraintsMultiWithStabilities() @@ -333,6 +471,9 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase return array( 'empty ' => array(''), 'invalid version' => array('1.0.0-meh'), + 'operator abuse' => array('>2.0,,<=3.0'), + 'operator abuse/2' => array('>2.0 ,, <=3.0'), + 'operator abuse/3' => array('>2.0 ||| <=3.0'), ); } diff --git a/tests/Composer/Test/Package/Version/VersionSelectorTest.php b/tests/Composer/Test/Package/Version/VersionSelectorTest.php new file mode 100644 index 000000000..6feb207a7 --- /dev/null +++ b/tests/Composer/Test/Package/Version/VersionSelectorTest.php @@ -0,0 +1,139 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Package\Version; + +use Composer\Package\Version\VersionSelector; +use Composer\Package\Version\VersionParser; + +class VersionSelectorTest extends \PHPUnit_Framework_TestCase +{ + // A) multiple versions, get the latest one + // B) targetPackageVersion will pass to pool + // C) No results, throw exception + + public function testLatestVersionIsReturned() + { + $packageName = 'foobar'; + + $package1 = $this->createMockPackage('1.2.1'); + $package2 = $this->createMockPackage('1.2.2'); + $package3 = $this->createMockPackage('1.2.0'); + $packages = array($package1, $package2, $package3); + + $pool = $this->createMockPool(); + $pool->expects($this->once()) + ->method('whatProvides') + ->with($packageName, null, true) + ->will($this->returnValue($packages)); + + $versionSelector = new VersionSelector($pool); + $best = $versionSelector->findBestCandidate($packageName); + + // 1.2.2 should be returned because it's the latest of the returned versions + $this->assertEquals($package2, $best, 'Latest version should be 1.2.2'); + } + + public function testFalseReturnedOnNoPackages() + { + $pool = $this->createMockPool(); + $pool->expects($this->once()) + ->method('whatProvides') + ->will($this->returnValue(array())); + + $versionSelector = new VersionSelector($pool); + $best = $versionSelector->findBestCandidate('foobaz'); + $this->assertFalse($best, 'No versions are available returns false'); + } + + /** + * @dataProvider getRecommendedRequireVersionPackages + */ + public function testFindRecommendedRequireVersion($prettyVersion, $isDev, $stability, $expectedVersion, $branchAlias = null) + { + $pool = $this->createMockPool(); + $versionSelector = new VersionSelector($pool); + $versionParser = new VersionParser(); + + $package = $this->getMock('\Composer\Package\PackageInterface'); + $package->expects($this->any()) + ->method('getPrettyVersion') + ->will($this->returnValue($prettyVersion)); + $package->expects($this->any()) + ->method('getVersion') + ->will($this->returnValue($versionParser->normalize($prettyVersion))); + $package->expects($this->any()) + ->method('isDev') + ->will($this->returnValue($isDev)); + $package->expects($this->any()) + ->method('getStability') + ->will($this->returnValue($stability)); + + $branchAlias = $branchAlias === null ? array() : array('branch-alias' => array($prettyVersion => $branchAlias)); + $package->expects($this->any()) + ->method('getExtra') + ->will($this->returnValue($branchAlias)); + + $recommended = $versionSelector->findRecommendedRequireVersion($package); + + // assert that the recommended version is what we expect + $this->assertEquals($expectedVersion, $recommended); + } + + public function getRecommendedRequireVersionPackages() + { + return array( + // real version, is dev package, stability, expected recommendation, [branch-alias] + array('1.2.1', false, 'stable', '~1.2'), + array('1.2', false, 'stable', '~1.2'), + array('v1.2.1', false, 'stable', '~1.2'), + array('3.1.2-pl2', false, 'stable', '~3.1'), + array('3.1.2-patch', false, 'stable', '~3.1'), + array('0.1.0', false, 'stable', '0.1.*'), + array('0.1.3', false, 'stable', '0.1.*'), + array('0.0.3', false, 'stable', '0.0.3.*'), + array('0.0.3-alpha', false, 'alpha', '0.0.3.*@alpha'), + array('2.0-beta.1', false, 'beta', '~2.0@beta'), + array('3.1.2-alpha5', false, 'alpha', '~3.1@alpha'), + array('3.0-RC2', false, 'RC', '~3.0@RC'), + // date-based versions are not touched at all + array('v20121020', false, 'stable', 'v20121020'), + array('v20121020.2', false, 'stable', 'v20121020.2'), + // dev packages without alias are not touched at all + array('dev-master', true, 'dev', 'dev-master'), + array('3.1.2-dev', true, 'dev', '3.1.2-dev'), + // dev packages with alias inherit the alias + array('dev-master', true, 'dev', '~2.1@dev', '2.1.x-dev'), + array('dev-master', true, 'dev', '~2.1@dev', '2.1-dev'), + array('dev-master', true, 'dev', '~2.1@dev', '2.1.3.x-dev'), + array('dev-master', true, 'dev', '~2.0@dev', '2.x-dev'), + // numeric alias + array('3.x-dev', true, 'dev', '~3.0@dev', '3.0.x-dev'), + array('3.x-dev', true, 'dev', '~3.0@dev', '3.0-dev'), + ); + } + + private function createMockPackage($version) + { + $package = $this->getMock('\Composer\Package\PackageInterface'); + $package->expects($this->any()) + ->method('getVersion') + ->will($this->returnValue($version)); + + return $package; + } + + private function createMockPool() + { + return $this->getMock('Composer\DependencyResolver\Pool', array(), array(), '', true); + } +} diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index 08d0d1aab..a2090082f 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -39,7 +39,7 @@ class PluginInstallerTest extends \PHPUnit_Framework_TestCase $this->directory = sys_get_temp_dir() . '/' . uniqid(); for ($i = 1; $i <= 4; $i++) { $filename = '/Fixtures/plugin-v'.$i.'/composer.json'; - mkdir(dirname($this->directory . $filename), 0777, TRUE); + mkdir(dirname($this->directory . $filename), 0777, true); $this->packages[] = $loader->load(__DIR__ . $filename); } @@ -76,7 +76,7 @@ class PluginInstallerTest extends \PHPUnit_Framework_TestCase $this->composer->setInstallationManager($im); $this->composer->setAutoloadGenerator($this->autoloadGenerator); - $this->pm = new PluginManager($this->composer, $this->io); + $this->pm = new PluginManager($this->io, $this->composer); $this->composer->setPluginManager($this->pm); $config->merge(array( @@ -164,4 +164,21 @@ class PluginInstallerTest extends \PHPUnit_Framework_TestCase $plugins = $this->pm->getPlugins(); $this->assertEquals('installer-v3', $plugins[1]->version); } + + public function testRegisterPluginOnlyOneTime() + { + $this->repository + ->expects($this->exactly(2)) + ->method('getPackages') + ->will($this->returnValue(array())); + $installer = new PluginInstaller($this->io, $this->composer); + $this->pm->loadInstalledPlugins(); + + $installer->install($this->repository, $this->packages[0]); + $installer->install($this->repository, clone $this->packages[0]); + + $plugins = $this->pm->getPlugins(); + $this->assertCount(1, $plugins); + $this->assertEquals('installer-v1', $plugins[0]->version); + } } diff --git a/tests/Composer/Test/Repository/ArtifactRepositoryTest.php b/tests/Composer/Test/Repository/ArtifactRepositoryTest.php index 109b53bfb..66c02acdc 100644 --- a/tests/Composer/Test/Repository/ArtifactRepositoryTest.php +++ b/tests/Composer/Test/Repository/ArtifactRepositoryTest.php @@ -19,6 +19,14 @@ use Composer\Package\BasePackage; class ArtifactRepositoryTest extends TestCase { + public function setUp() + { + parent::setUp(); + if (!extension_loaded('zip')) { + $this->markTestSkipped('You need the zip extension to run this test.'); + } + } + public function testExtractsConfigsFromZipArchives() { $expectedPackages = array( @@ -26,12 +34,16 @@ class ArtifactRepositoryTest extends TestCase 'composer/composer-1.0.0-alpha6', 'vendor1/package2-4.3.2', 'vendor3/package1-5.4.3', + 'test/jsonInRoot-1.0.0', + 'test/jsonInFirstLevel-1.0.0', + //The files not-an-artifact.zip and jsonSecondLevel are not valid + //artifacts and do not get detected. ); $coordinates = array('type' => 'artifact', 'url' => __DIR__ . '/Fixtures/artifacts'); $repo = new ArtifactRepository($coordinates, new NullIO(), new Config()); - $foundPackages = array_map(function(BasePackage $package) { + $foundPackages = array_map(function (BasePackage $package) { return "{$package->getPrettyName()}-{$package->getPrettyVersion()}"; }, $repo->getPackages()); @@ -40,4 +52,64 @@ class ArtifactRepositoryTest extends TestCase $this->assertSame($expectedPackages, $foundPackages); } + + public function testAbsoluteRepoUrlCreatesAbsoluteUrlPackages() + { + $absolutePath = __DIR__ . '/Fixtures/artifacts'; + $coordinates = array('type' => 'artifact', 'url' => $absolutePath); + $repo = new ArtifactRepository($coordinates, new NullIO(), new Config()); + + foreach ($repo->getPackages() as $package) { + $this->assertTrue(strpos($package->getDistUrl(), $absolutePath) === 0); + } + } + + public function testRelativeRepoUrlCreatesRelativeUrlPackages() + { + $relativePath = 'tests/Composer/Test/Repository/Fixtures/artifacts'; + $coordinates = array('type' => 'artifact', 'url' => $relativePath); + $repo = new ArtifactRepository($coordinates, new NullIO(), new Config()); + + foreach ($repo->getPackages() as $package) { + $this->assertTrue(strpos($package->getDistUrl(), $relativePath) === 0); + } + } } + +//Files jsonInFirstLevel.zip, jsonInRoot.zip and jsonInSecondLevel.zip were generated with: +// +//$archivesToCreate = array( +// 'jsonInRoot' => array( +// "extra.txt" => "Testing testing testing", +// "composer.json" => '{ "name": "test/jsonInRoot", "version": "1.0.0" }', +// "subdir/extra.txt" => "Testing testing testing", +// "subdir/extra2.txt" => "Testing testing testing", +// ), +// +// 'jsonInFirstLevel' => array( +// "extra.txt" => "Testing testing testing", +// "subdir/composer.json" => '{ "name": "test/jsonInFirstLevel", "version": "1.0.0" }', +// "subdir/extra.txt" => "Testing testing testing", +// "subdir/extra2.txt" => "Testing testing testing", +// ), +// +// 'jsonInSecondLevel' => array( +// "extra.txt" => "Testing testing testing", +// "subdir/extra1.txt" => "Testing testing testing", +// "subdir/foo/composer.json" => '{ "name": "test/jsonInSecondLevel", "version": "1.0.0" }', +// "subdir/foo/extra1.txt" => "Testing testing testing", +// "subdir/extra2.txt" => "Testing testing testing", +// "subdir/extra3.txt" => "Testing testing testing", +// ), +//); +// +//foreach ($archivesToCreate as $archiveName => $fileDetails) { +// $zipFile = new ZipArchive(); +// $zipFile->open("$archiveName.zip", ZIPARCHIVE::CREATE); +// +// foreach ($fileDetails as $filename => $fileContents) { +// $zipFile->addFromString($filename, $fileContents); +// } +// +// $zipFile->close(); +//} diff --git a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php index fa6214dee..fa1ec6d5b 100644 --- a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php +++ b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php @@ -12,7 +12,6 @@ namespace Composer\Repository; -use Composer\Repository\FilesystemRepository; use Composer\TestCase; class FilesystemRepositoryTest extends TestCase diff --git a/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInFirstLevel.zip b/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInFirstLevel.zip new file mode 100644 index 000000000..498037464 Binary files /dev/null and b/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInFirstLevel.zip differ diff --git a/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInRoot.zip b/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInRoot.zip new file mode 100644 index 000000000..7b2a87eb9 Binary files /dev/null and b/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInRoot.zip differ diff --git a/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInSecondLevel.zip b/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInSecondLevel.zip new file mode 100644 index 000000000..0e5abc61b Binary files /dev/null and b/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInSecondLevel.zip differ diff --git a/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php b/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php index 214d7b702..c22250a04 100644 --- a/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php +++ b/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php @@ -121,6 +121,7 @@ class ChannelReaderTest extends TestCase $expectedPackage->setType('pear-library'); $expectedPackage->setDistType('file'); $expectedPackage->setDescription('description'); + $expectedPackage->setLicense(array('license')); $expectedPackage->setDistUrl("http://test.loc/get/sample-1.0.0.1.tgz"); $expectedPackage->setAutoload(array('classmap' => array(''))); $expectedPackage->setIncludePaths(array('/')); diff --git a/tests/Composer/Test/Repository/PearRepositoryTest.php b/tests/Composer/Test/Repository/PearRepositoryTest.php index a42c8e0b3..7eaad13e7 100644 --- a/tests/Composer/Test/Repository/PearRepositoryTest.php +++ b/tests/Composer/Test/Repository/PearRepositoryTest.php @@ -84,13 +84,6 @@ class PearRepositoryTest extends TestCase public function repositoryDataProvider() { return array( - array( - 'pear.phpunit.de', - array( - array('name' => 'pear-pear.phpunit.de/PHPUnit_MockObject', 'version' => '1.1.1'), - array('name' => 'pear-pear.phpunit.de/PHPUnit', 'version' => '3.6.10'), - ) - ), array( 'pear.php.net', array( diff --git a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php index a3cb9dd23..e180c5eec 100644 --- a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php @@ -86,10 +86,12 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase $remoteFilesystem->expects($this->at(2)) ->method('getContents') ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) - ->will($this->returnValue('{"master_branch": "test_master", "private": true}')); + ->will($this->returnValue('{"master_branch": "test_master", "private": true, "owner": {"login": "composer"}, "name": "packagist"}')); $configSource = $this->getMock('Composer\Config\ConfigSourceInterface'); + $authConfigSource = $this->getMock('Composer\Config\ConfigSourceInterface'); $this->config->setConfigSource($configSource); + $this->config->setAuthConfigSource($authConfigSource); $repoConfig = array( 'url' => $repoUrl, @@ -131,7 +133,7 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase $remoteFilesystem->expects($this->at(0)) ->method('getContents') ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) - ->will($this->returnValue('{"master_branch": "test_master"}')); + ->will($this->returnValue('{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}')); $repoConfig = array( 'url' => $repoUrl, @@ -174,7 +176,7 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase $remoteFilesystem->expects($this->at(0)) ->method('getContents') ->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false)) - ->will($this->returnValue('{"master_branch": "test_master"}')); + ->will($this->returnValue('{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}')); $remoteFilesystem->expects($this->at(1)) ->method('getContents') @@ -283,19 +285,16 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase $this->assertEquals('test_master', $gitHubDriver->getRootIdentifier()); - // Dist is not available for GitDriver - $dist = $gitHubDriver->getDist($identifier); - $this->assertNull($dist); + $dist = $gitHubDriver->getDist($sha); + $this->assertEquals('zip', $dist['type']); + $this->assertEquals('https://api.github.com/repos/composer/packagist/zipball/SOMESHA', $dist['url']); + $this->assertEquals($sha, $dist['reference']); $source = $gitHubDriver->getSource($identifier); $this->assertEquals('git', $source['type']); $this->assertEquals($repoSshUrl, $source['url']); $this->assertEquals($identifier, $source['reference']); - // Dist is not available for GitDriver - $dist = $gitHubDriver->getDist($sha); - $this->assertNull($dist); - $source = $gitHubDriver->getSource($sha); $this->assertEquals('git', $source['type']); $this->assertEquals($repoSshUrl, $source['url']); diff --git a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php index 36cd69ebc..59030f506 100644 --- a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php @@ -15,119 +15,143 @@ namespace Composer\Test\Repository\Vcs; use Composer\Repository\Vcs\PerforceDriver; use Composer\Util\Filesystem; use Composer\Config; +use Composer\Util\Perforce; /** * @author Matt Whittom */ class PerforceDriverTest extends \PHPUnit_Framework_TestCase { - private $config; - private $io; - private $process; - private $remoteFileSystem; - private $testPath; + protected $config; + protected $io; + protected $process; + protected $remoteFileSystem; + protected $testPath; + protected $driver; + protected $repoConfig; - public function setUp() - { - $this->testPath = sys_get_temp_dir() . '/composer-test'; - $this->config = new Config(); - $this->config->merge( - array( - 'config' => array( - 'home' => $this->testPath, - ), - ) - ); + const TEST_URL = 'TEST_PERFORCE_URL'; + const TEST_DEPOT = 'TEST_DEPOT_CONFIG'; + const TEST_BRANCH = 'TEST_BRANCH_CONFIG'; - $this->io = $this->getMock('Composer\IO\IOInterface'); - $this->process = $this->getMock('Composer\Util\ProcessExecutor'); - $this->remoteFileSystem = $this->getMockBuilder('Composer\Util\RemoteFilesystem')->disableOriginalConstructor() - ->getMock(); + protected function setUp() + { + $this->testPath = sys_get_temp_dir() . '/composer-test'; + $this->config = $this->getTestConfig($this->testPath); + $this->repoConfig = $this->getTestRepoConfig(); + $this->io = $this->getMockIOInterface(); + $this->process = $this->getMockProcessExecutor(); + $this->remoteFileSystem = $this->getMockRemoteFilesystem(); + $this->perforce = $this->getMockPerforce(); + $this->driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->process, $this->remoteFileSystem); + $this->overrideDriverInternalPerforce($this->perforce); } - public function tearDown() + protected function tearDown() { + //cleanup directory under test path $fs = new Filesystem; $fs->removeDirectory($this->testPath); + $this->driver = null; + $this->perforce = null; + $this->remoteFileSystem = null; + $this->process = null; + $this->io = null; + $this->repoConfig = null; + $this->config = null; + $this->testPath = null; } - public function testInitializeCapturesVariablesFromRepoConfig() + protected function overrideDriverInternalPerforce(Perforce $perforce) { - $this->setUp(); - $repoConfig = array( - 'url' => 'TEST_PERFORCE_URL', - 'depot' => 'TEST_DEPOT_CONFIG', - 'branch' => 'TEST_BRANCH_CONFIG' - ); - $driver = new PerforceDriver($repoConfig, $this->io, $this->config, $this->process, $this->remoteFileSystem); - $process = $this->getMock('Composer\Util\ProcessExecutor'); - $arguments = array( - array('depot' => 'TEST_DEPOT', 'branch' => 'TEST_BRANCH'), - 'port' => 'TEST_PORT', - 'path' => $this->testPath, - $process, - true, - 'TEST' + $reflectionClass = new \ReflectionClass($this->driver); + $property = $reflectionClass->getProperty('perforce'); + $property->setAccessible(true); + $property->setValue($this->driver, $perforce); + } + + protected function getTestConfig($testPath) + { + $config = new Config(); + $config->merge(array('config' => array('home' => $testPath))); + + return $config; + } + + protected function getTestRepoConfig() + { + return array( + 'url' => self::TEST_URL, + 'depot' => self::TEST_DEPOT, + 'branch' => self::TEST_BRANCH, ); - $perforce = $this->getMock('Composer\Util\Perforce', null, $arguments); - $driver->setPerforce($perforce); + } + + protected function getMockIOInterface() + { + return $this->getMock('Composer\IO\IOInterface'); + } + + protected function getMockProcessExecutor() + { + return $this->getMock('Composer\Util\ProcessExecutor'); + } + + protected function getMockRemoteFilesystem() + { + return $this->getMockBuilder('Composer\Util\RemoteFilesystem')->disableOriginalConstructor()->getMock(); + } + + protected function getMockPerforce() + { + $methods = array('p4login', 'checkStream', 'writeP4ClientSpec', 'connectClient', 'getComposerInformation', 'cleanupClientSpec'); + + return $this->getMockBuilder('Composer\Util\Perforce', $methods)->disableOriginalConstructor()->getMock(); + } + + public function testInitializeCapturesVariablesFromRepoConfig() + { + $driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->process, $this->remoteFileSystem); $driver->initialize(); - $this->assertEquals('TEST_PERFORCE_URL', $driver->getUrl()); - $this->assertEquals('TEST_DEPOT_CONFIG', $driver->getDepot()); - $this->assertEquals('TEST_BRANCH_CONFIG', $driver->getBranch()); + $this->assertEquals(self::TEST_URL, $driver->getUrl()); + $this->assertEquals(self::TEST_DEPOT, $driver->getDepot()); + $this->assertEquals(self::TEST_BRANCH, $driver->getBranch()); } public function testInitializeLogsInAndConnectsClient() { - $this->setUp(); - $repoConfig = array( - 'url' => 'TEST_PERFORCE_URL', - 'depot' => 'TEST_DEPOT_CONFIG', - 'branch' => 'TEST_BRANCH_CONFIG' - ); - $driver = new PerforceDriver($repoConfig, $this->io, $this->config, $this->process, $this->remoteFileSystem); - $perforce = $this->getMockBuilder('Composer\Util\Perforce')->disableOriginalConstructor()->getMock(); - $perforce->expects($this->at(0)) - ->method('p4Login') - ->with($this->io); - $perforce->expects($this->at(1)) - ->method('checkStream') - ->with($this->equalTo('TEST_DEPOT_CONFIG')); - $perforce->expects($this->at(2)) - ->method('writeP4ClientSpec'); - $perforce->expects($this->at(3)) - ->method('connectClient'); - - $driver->setPerforce($perforce); - $driver->initialize(); + $this->perforce->expects($this->at(0))->method('p4Login')->with($this->identicalTo($this->io)); + $this->perforce->expects($this->at(1))->method('checkStream')->with($this->equalTo(self::TEST_DEPOT)); + $this->perforce->expects($this->at(2))->method('writeP4ClientSpec'); + $this->perforce->expects($this->at(3))->method('connectClient'); + $this->driver->initialize(); } - public function testHasComposerFile() + /** + * @depends testInitializeCapturesVariablesFromRepoConfig + * @depends testInitializeLogsInAndConnectsClient + */ + public function testHasComposerFileReturnsFalseOnNoComposerFile() { - $repoConfig = array( - 'url' => 'TEST_PERFORCE_URL', - 'depot' => 'TEST_DEPOT_CONFIG', - 'branch' => 'TEST_BRANCH_CONFIG' - ); - $driver = new PerforceDriver($repoConfig, $this->io, $this->config, $this->process, $this->remoteFileSystem); - $process = $this->getMock('Composer\Util\ProcessExecutor'); - $arguments = array( - array('depot' => 'TEST_DEPOT', 'branch' => 'TEST_BRANCH'), - 'port' => 'TEST_PORT', - 'path' => $this->testPath, - $process, - true, - 'TEST' - ); - $perforce = $this->getMock('Composer\Util\Perforce', array('getComposerInformation'), $arguments); - $perforce->expects($this->at(0)) - ->method('getComposerInformation') - ->with($this->equalTo('//TEST_DEPOT_CONFIG/TEST_IDENTIFIER')) - ->will($this->returnValue('Some json stuff')); - $driver->setPerforce($perforce); - $driver->initialize(); $identifier = 'TEST_IDENTIFIER'; - $result = $driver->hasComposerFile($identifier); + $formatted_depot_path = '//' . self::TEST_DEPOT . '/' . $identifier; + $this->perforce->expects($this->any())->method('getComposerInformation')->with($this->equalTo($formatted_depot_path))->will($this->returnValue(array())); + $this->driver->initialize(); + $result = $this->driver->hasComposerFile($identifier); + $this->assertFalse($result); + } + + /** + * @depends testInitializeCapturesVariablesFromRepoConfig + * @depends testInitializeLogsInAndConnectsClient + */ + public function testHasComposerFileReturnsTrueWithOneOrMoreComposerFiles() + { + $identifier = 'TEST_IDENTIFIER'; + $formatted_depot_path = '//' . self::TEST_DEPOT . '/' . $identifier; + $this->perforce->expects($this->any())->method('getComposerInformation')->with($this->equalTo($formatted_depot_path))->will($this->returnValue(array(''))); + $this->driver->initialize(); + $result = $this->driver->hasComposerFile($identifier); $this->assertTrue($result); } @@ -143,4 +167,10 @@ class PerforceDriverTest extends \PHPUnit_Framework_TestCase $this->expectOutputString(''); $this->assertFalse(PerforceDriver::supports($this->io, $this->config, 'existing.url')); } + + public function testCleanup() + { + $this->perforce->expects($this->once())->method('cleanupClientSpec'); + $this->driver->cleanup(); + } } diff --git a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php index 2f698565f..2ef1baa18 100644 --- a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php @@ -23,9 +23,6 @@ class SvnDriverTest extends \PHPUnit_Framework_TestCase public function testWrongCredentialsInUrl() { $console = $this->getMock('Composer\IO\IOInterface'); - $console->expects($this->exactly(6)) - ->method('isInteractive') - ->will($this->returnValue(true)); $output = "svn: OPTIONS of 'http://corp.svn.local/repo':"; $output .= " authorization failed: Could not authenticate to server:"; diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index c74e84a5f..a4d1255f9 100644 --- a/tests/Composer/Test/Util/FilesystemTest.php +++ b/tests/Composer/Test/Util/FilesystemTest.php @@ -176,4 +176,65 @@ class FilesystemTest extends TestCase array('phar://c:../Foo', 'phar://c:../Foo'), ); } + + /** + * @link https://github.com/composer/composer/issues/3157 + */ + public function testUnlinkSymlinkedDirectory() + { + $tmp = sys_get_temp_dir(); + $basepath = $tmp . "/composer_testdir"; + $symlinked = $basepath . "/linked"; + @mkdir($basepath . "/real", 0777, true); + touch($basepath . "/real/FILE"); + + $result = @symlink($basepath . "/real", $symlinked); + + if (!$result) { + $this->markTestSkipped('Symbolic links for directories not supported on this platform'); + } + + if (!is_dir($symlinked)) { + $this->fail('Precondition assertion failed (is_dir is false on symbolic link to directory).'); + } + + $fs = new Filesystem(); + $result = $fs->unlink($symlinked); + $this->assertTrue($result); + $this->assertFalse(file_exists($symlinked)); + } + + /** + * @link https://github.com/composer/composer/issues/3144 + */ + public function testRemoveSymlinkedDirectoryWithTrailingSlash() + { + $tmp = sys_get_temp_dir(); + $basepath = $tmp . "/composer_testdir"; + @mkdir($basepath . "/real", 0777, true); + touch($basepath . "/real/FILE"); + $symlinked = $basepath . "/linked"; + $symlinkedTrailingSlash = $symlinked . "/"; + + $result = @symlink($basepath . "/real", $symlinked); + + if (!$result) { + $this->markTestSkipped('Symbolic links for directories not supported on this platform'); + } + + if (!is_dir($symlinked)) { + $this->fail('Precondition assertion failed (is_dir is false on symbolic link to directory).'); + } + + if (!is_dir($symlinkedTrailingSlash)) { + $this->fail('Precondition assertion failed (is_dir false w trailing slash).'); + } + + $fs = new Filesystem(); + + $result = $fs->removeDirectory($symlinkedTrailingSlash); + $this->assertTrue($result); + $this->assertFalse(file_exists($symlinkedTrailingSlash)); + $this->assertFalse(file_exists($symlinked)); + } } diff --git a/tests/Composer/Test/Util/GitHubTest.php b/tests/Composer/Test/Util/GitHubTest.php new file mode 100644 index 000000000..4a1f3a35a --- /dev/null +++ b/tests/Composer/Test/Util/GitHubTest.php @@ -0,0 +1,264 @@ + +* Jordi Boggiano +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +namespace Composer\Test\Util; + +use Composer\Downloader\TransportException; +use Composer\Util\GitHub; +use RecursiveArrayIterator; +use RecursiveIteratorIterator; + +/** +* @author Rob Bast +*/ +class GitHubTest extends \PHPUnit_Framework_TestCase +{ + private $username = 'username'; + private $password = 'password'; + private $authcode = 'authcode'; + private $message = 'mymessage'; + private $origin = 'github.com'; + private $token = 'githubtoken'; + + public function testUsernamePasswordAuthenticationFlow() + { + $io = $this->getIOMock(); + $io + ->expects($this->at(0)) + ->method('write') + ->with($this->message) + ; + $io + ->expects($this->once()) + ->method('ask') + ->with('Username: ') + ->willReturn($this->username) + ; + $io + ->expects($this->once()) + ->method('askAndHideAnswer') + ->with('Password: ') + ->willReturn($this->password) + ; + + $rfs = $this->getRemoteFilesystemMock(); + $rfs + ->expects($this->once()) + ->method('getContents') + ->with( + $this->equalTo($this->origin), + $this->equalTo(sprintf('https://api.%s/authorizations', $this->origin)), + $this->isFalse(), + $this->anything() + ) + ->willReturn(sprintf('{"token": "%s"}', $this->token)) + ; + + $config = $this->getConfigMock(); + $config + ->expects($this->exactly(2)) + ->method('getAuthConfigSource') + ->willReturn($this->getAuthJsonMock()) + ; + $config + ->expects($this->once()) + ->method('getConfigSource') + ->willReturn($this->getConfJsonMock()) + ; + + $github = new GitHub($io, $config, null, $rfs); + + $this->assertTrue($github->authorizeOAuthInteractively($this->origin, $this->message)); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Invalid GitHub credentials 5 times in a row, aborting. + */ + public function testUsernamePasswordFailure() + { + $io = $this->getIOMock(); + $io + ->expects($this->exactly(5)) + ->method('ask') + ->with('Username: ') + ->willReturn($this->username) + ; + $io + ->expects($this->exactly(5)) + ->method('askAndHideAnswer') + ->with('Password: ') + ->willReturn($this->password) + ; + + $rfs = $this->getRemoteFilesystemMock(); + $rfs + ->expects($this->exactly(5)) + ->method('getContents') + ->will($this->throwException(new TransportException('', 401))) + ; + + $config = $this->getConfigMock(); + $config + ->expects($this->exactly(1)) + ->method('getAuthConfigSource') + ->willReturn($this->getAuthJsonMock()) + ; + + $github = new GitHub($io, $config, null, $rfs); + + $github->authorizeOAuthInteractively($this->origin); + } + + public function testTwoFactorAuthentication() + { + $io = $this->getIOMock(); + $io + ->expects($this->exactly(2)) + ->method('hasAuthentication') + ->will($this->onConsecutiveCalls(true, true)) + ; + $io + ->expects($this->exactly(2)) + ->method('ask') + ->withConsecutive( + array('Username: '), + array('Authentication Code: ') + ) + ->will($this->onConsecutiveCalls($this->username, $this->authcode)) + ; + $io + ->expects($this->once()) + ->method('askAndHideAnswer') + ->with('Password: ') + ->willReturn($this->password) + ; + + $exception = new TransportException('', 401); + $exception->setHeaders(array('X-GitHub-OTP: required; app')); + + $rfs = $this->getRemoteFilesystemMock(); + $rfs + ->expects($this->at(0)) + ->method('getContents') + ->will($this->throwException($exception)) + ; + $rfs + ->expects($this->at(1)) + ->method('getContents') + ->with( + $this->equalTo($this->origin), + $this->equalTo(sprintf('https://api.%s/authorizations', $this->origin)), + $this->isFalse(), + $this->callback(function ($array) { + $headers = GitHubTest::recursiveFind($array, 'header'); + foreach ($headers as $string) { + if ('X-GitHub-OTP: authcode' === $string) { + return true; + } + } + return false; + }) + ) + ->willReturn(sprintf('{"token": "%s"}', $this->token)) + ; + + $config = $this->getConfigMock(); + $config + ->expects($this->atLeastOnce()) + ->method('getAuthConfigSource') + ->willReturn($this->getAuthJsonMock()) + ; + $config + ->expects($this->atLeastOnce()) + ->method('getConfigSource') + ->willReturn($this->getConfJsonMock()) + ; + + $github = new GitHub($io, $config, null, $rfs); + + $this->assertTrue($github->authorizeOAuthInteractively($this->origin)); + } + + private function getIOMock() + { + $io = $this + ->getMockBuilder('Composer\IO\ConsoleIO') + ->disableOriginalConstructor() + ->getMock() + ; + + return $io; + } + + private function getConfigMock() + { + $config = $this->getMock('Composer\Config'); + + return $config; + } + + private function getRemoteFilesystemMock() + { + $rfs = $this + ->getMockBuilder('Composer\Util\RemoteFilesystem') + ->disableOriginalConstructor() + ->getMock() + ; + + return $rfs; + } + + private function getAuthJsonMock() + { + $authjson = $this + ->getMockBuilder('Composer\Config\JsonConfigSource') + ->disableOriginalConstructor() + ->getMock() + ; + $authjson + ->expects($this->atLeastOnce()) + ->method('getName') + ->willReturn('auth.json') + ; + + return $authjson; + } + + private function getConfJsonMock() + { + $confjson = $this + ->getMockBuilder('Composer\Config\JsonConfigSource') + ->disableOriginalConstructor() + ->getMock() + ; + $confjson + ->expects($this->atLeastOnce()) + ->method('removeConfigSetting') + ->with('github-oauth.'.$this->origin) + ; + + return $confjson; + } + + public static function recursiveFind($array, $needle) + { + $iterator = new RecursiveArrayIterator($array); + $recursive = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::SELF_FIRST); + + foreach ($recursive as $key => $value) { + if ($key === $needle) { + return $value; + } + } + } +} diff --git a/tests/Composer/Test/Util/PerforceTest.php b/tests/Composer/Test/Util/PerforceTest.php index f2eeb1964..dc727096d 100644 --- a/tests/Composer/Test/Util/PerforceTest.php +++ b/tests/Composer/Test/Util/PerforceTest.php @@ -22,17 +22,49 @@ class PerforceTest extends \PHPUnit_Framework_TestCase { protected $perforce; protected $processExecutor; + protected $io; - public function setUp() + const TEST_DEPOT = 'depot'; + const TEST_BRANCH = 'branch'; + const TEST_P4USER = 'user'; + const TEST_CLIENT_NAME = 'TEST'; + const TEST_PORT = 'port'; + const TEST_PATH = 'path'; + + protected function setUp() { $this->processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); - $repoConfig = array( - 'depot' => 'depot', - 'branch' => 'branch', - 'p4user' => 'user', - 'unique_perforce_client_name' => 'TEST' + $this->repoConfig = $this->getTestRepoConfig(); + $this->io = $this->getMockIOInterface(); + $this->createNewPerforceWithWindowsFlag(true); + } + + protected function tearDown() + { + $this->perforce = null; + $this->io = null; + $this->repoConfig = null; + $this->processExecutor = null; + } + + public function getTestRepoConfig() + { + return array( + 'depot' => self::TEST_DEPOT, + 'branch' => self::TEST_BRANCH, + 'p4user' => self::TEST_P4USER, + 'unique_perforce_client_name' => self::TEST_CLIENT_NAME ); - $this->perforce = new Perforce($repoConfig, 'port', 'path', $this->processExecutor, true); + } + + public function getMockIOInterface() + { + return $this->getMock('Composer\IO\IOInterface'); + } + + protected function createNewPerforceWithWindowsFlag($flag) + { + $this->perforce = new Perforce($this->repoConfig, self::TEST_PORT, self::TEST_PATH, $this->processExecutor, $flag, $this->io); } public function testGetClientWithoutStream() @@ -98,116 +130,90 @@ class PerforceTest extends \PHPUnit_Framework_TestCase public function testQueryP4UserWithUserAlreadySet() { - $io = $this->getMock('Composer\IO\IOInterface'); - - $repoConfig = array('depot' => 'depot', 'branch' => 'branch', 'p4user' => 'TEST_USER'); - $this->perforce = new Perforce($repoConfig, 'port', 'path', $this->processExecutor, true, 'TEST'); - - $this->perforce->queryP4user($io); - $this->assertEquals('TEST_USER', $this->perforce->getUser()); + $this->perforce->queryP4user(); + $this->assertEquals(self::TEST_P4USER, $this->perforce->getUser()); } public function testQueryP4UserWithUserSetInP4VariablesWithWindowsOS() { - $repoConfig = array('depot' => 'depot', 'branch' => 'branch'); - $this->perforce = new Perforce($repoConfig, 'port', 'path', $this->processExecutor, true, 'TEST'); - - $io = $this->getMock('Composer\IO\IOInterface'); + $this->createNewPerforceWithWindowsFlag(true); + $this->perforce->setUser(null); $expectedCommand = 'p4 set'; - $this->processExecutor->expects($this->at(0)) - ->method('execute') - ->with($this->equalTo($expectedCommand)) - ->will( - $this->returnCallback( - function ($command, &$output) { - $output = 'P4USER=TEST_P4VARIABLE_USER' . PHP_EOL ; - - return true; - } - ) - ); + $callback = function ($command, &$output) { + $output = 'P4USER=TEST_P4VARIABLE_USER' . PHP_EOL; - $this->perforce->queryP4user($io); + return true; + }; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will($this->returnCallback($callback)); + $this->perforce->queryP4user(); $this->assertEquals('TEST_P4VARIABLE_USER', $this->perforce->getUser()); } public function testQueryP4UserWithUserSetInP4VariablesNotWindowsOS() { - $repoConfig = array('depot' => 'depot', 'branch' => 'branch'); - $this->perforce = new Perforce($repoConfig, 'port', 'path', $this->processExecutor, false, 'TEST'); - - $io = $this->getMock('Composer\IO\IOInterface'); + $this->createNewPerforceWithWindowsFlag(false); + $this->perforce->setUser(null); $expectedCommand = 'echo $P4USER'; - $this->processExecutor->expects($this->at(0)) - ->method('execute') - ->with($this->equalTo($expectedCommand)) - ->will( - $this->returnCallback( - function ($command, &$output) { - $output = 'TEST_P4VARIABLE_USER' . PHP_EOL; + $callback = function ($command, &$output) { + $output = 'TEST_P4VARIABLE_USER' . PHP_EOL; - return true; - } - ) - ); - - $this->perforce->queryP4user($io); + return true; + }; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will($this->returnCallback($callback)); + $this->perforce->queryP4user(); $this->assertEquals('TEST_P4VARIABLE_USER', $this->perforce->getUser()); } public function testQueryP4UserQueriesForUser() { - $repoConfig = array('depot' => 'depot', 'branch' => 'branch'); - $this->perforce = new Perforce($repoConfig, 'port', 'path', $this->processExecutor, false, 'TEST'); - $io = $this->getMock('Composer\IO\IOInterface'); + $this->perforce->setUser(null); $expectedQuestion = 'Enter P4 User:'; - $io->expects($this->at(0)) - ->method('ask') - ->with($this->equalTo($expectedQuestion)) - ->will($this->returnValue('TEST_QUERY_USER')); - - $this->perforce->queryP4user($io); + $this->io->expects($this->at(0)) + ->method('ask') + ->with($this->equalTo($expectedQuestion)) + ->will($this->returnValue('TEST_QUERY_USER')); + $this->perforce->queryP4user(); $this->assertEquals('TEST_QUERY_USER', $this->perforce->getUser()); } public function testQueryP4UserStoresResponseToQueryForUserWithWindows() { - $repoConfig = array('depot' => 'depot', 'branch' => 'branch'); - $this->perforce = new Perforce($repoConfig, 'port', 'path', $this->processExecutor, true, 'TEST'); - - $io = $this->getMock('Composer\IO\IOInterface'); + $this->createNewPerforceWithWindowsFlag(true); + $this->perforce->setUser(null); $expectedQuestion = 'Enter P4 User:'; - $io->expects($this->at(0)) - ->method('ask') - ->with($this->equalTo($expectedQuestion)) - ->will($this->returnValue('TEST_QUERY_USER')); - $expectedCommand = 'p4 set P4USER=TEST_QUERY_USER'; + $expectedCommand = 'p4 set P4USER=TEST_QUERY_USER'; + $this->io->expects($this->at(0)) + ->method('ask') + ->with($this->equalTo($expectedQuestion)) + ->will($this->returnValue('TEST_QUERY_USER')); $this->processExecutor->expects($this->at(1)) - ->method('execute') - ->with($this->equalTo($expectedCommand)) - ->will($this->returnValue(0)); - - $this->perforce->queryP4user($io); + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will($this->returnValue(0)); + $this->perforce->queryP4user(); } public function testQueryP4UserStoresResponseToQueryForUserWithoutWindows() { - $repoConfig = array('depot' => 'depot', 'branch' => 'branch'); - $this->perforce = new Perforce($repoConfig, 'port', 'path', $this->processExecutor, false, 'TEST'); - - $io = $this->getMock('Composer\IO\IOInterface'); + $this->createNewPerforceWithWindowsFlag(false); + $this->perforce->setUser(null); $expectedQuestion = 'Enter P4 User:'; - $io->expects($this->at(0)) - ->method('ask') - ->with($this->equalTo($expectedQuestion)) - ->will($this->returnValue('TEST_QUERY_USER')); - $expectedCommand = 'export P4USER=TEST_QUERY_USER'; + $expectedCommand = 'export P4USER=TEST_QUERY_USER'; + $this->io->expects($this->at(0)) + ->method('ask') + ->with($this->equalTo($expectedQuestion)) + ->will($this->returnValue('TEST_QUERY_USER')); $this->processExecutor->expects($this->at(1)) - ->method('execute') - ->with($this->equalTo($expectedCommand)) - ->will($this->returnValue(0)); - - $this->perforce->queryP4user($io); + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will($this->returnValue(0)); + $this->perforce->queryP4user(); } public function testQueryP4PasswordWithPasswordAlreadySet() @@ -218,69 +224,55 @@ class PerforceTest extends \PHPUnit_Framework_TestCase 'p4user' => 'user', 'p4password' => 'TEST_PASSWORD' ); - $this->perforce = new Perforce($repoConfig, 'port', 'path', $this->processExecutor, false, 'TEST'); - $io = $this->getMock('Composer\IO\IOInterface'); - - $password = $this->perforce->queryP4Password($io); + $this->perforce = new Perforce($repoConfig, 'port', 'path', $this->processExecutor, false, $this->getMockIOInterface(), 'TEST'); + $password = $this->perforce->queryP4Password(); $this->assertEquals('TEST_PASSWORD', $password); } public function testQueryP4PasswordWithPasswordSetInP4VariablesWithWindowsOS() { - $io = $this->getMock('Composer\IO\IOInterface'); - + $this->createNewPerforceWithWindowsFlag(true); $expectedCommand = 'p4 set'; - $this->processExecutor->expects($this->at(0)) - ->method('execute') - ->with($this->equalTo($expectedCommand)) - ->will( - $this->returnCallback( - function ($command, &$output) { - $output = 'P4PASSWD=TEST_P4VARIABLE_PASSWORD' . PHP_EOL; - - return true; - } - ) - ); + $callback = function ($command, &$output) { + $output = 'P4PASSWD=TEST_P4VARIABLE_PASSWORD' . PHP_EOL; - $password = $this->perforce->queryP4Password($io); + return true; + }; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will($this->returnCallback($callback)); + $password = $this->perforce->queryP4Password(); $this->assertEquals('TEST_P4VARIABLE_PASSWORD', $password); } public function testQueryP4PasswordWithPasswordSetInP4VariablesNotWindowsOS() { - $repoConfig = array('depot' => 'depot', 'branch' => 'branch', 'p4user' => 'user'); - $this->perforce = new Perforce($repoConfig, 'port', 'path', $this->processExecutor, false, 'TEST'); - - $io = $this->getMock('Composer\IO\IOInterface'); + $this->createNewPerforceWithWindowsFlag(false); $expectedCommand = 'echo $P4PASSWD'; - $this->processExecutor->expects($this->at(0)) - ->method('execute') - ->with($this->equalTo($expectedCommand)) - ->will( - $this->returnCallback( - function ($command, &$output) { - $output = 'TEST_P4VARIABLE_PASSWORD' . PHP_EOL; + $callback = function ($command, &$output) { + $output = 'TEST_P4VARIABLE_PASSWORD' . PHP_EOL; - return true; - } - ) - ); + return true; + }; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will($this->returnCallback($callback)); - $password = $this->perforce->queryP4Password($io); + $password = $this->perforce->queryP4Password(); $this->assertEquals('TEST_P4VARIABLE_PASSWORD', $password); } public function testQueryP4PasswordQueriesForPassword() { - $io = $this->getMock('Composer\IO\IOInterface'); $expectedQuestion = 'Enter password for Perforce user user: '; - $io->expects($this->at(0)) + $this->io->expects($this->at(0)) ->method('askAndHideAnswer') ->with($this->equalTo($expectedQuestion)) ->will($this->returnValue('TEST_QUERY_PASSWORD')); - $password = $this->perforce->queryP4Password($io); + $password = $this->perforce->queryP4Password(); $this->assertEquals('TEST_QUERY_PASSWORD', $password); } @@ -364,15 +356,35 @@ class PerforceTest extends \PHPUnit_Framework_TestCase } ) ); + $expectedCommand2 = 'p4 -u user -p port changes //depot/branch/...'; + $expectedCallback = function ($command, &$output) { + $output = 'Change 1234 on 2014/03/19 by Clark.Stuth@Clark.Stuth_test_client \'test changelist\''; + + return true; + }; + $this->processExecutor->expects($this->at(1)) + ->method('execute') + ->with($this->equalTo($expectedCommand2)) + ->will($this->returnCallback($expectedCallback)); $branches = $this->perforce->getBranches(); - $this->assertEquals('//depot/branch', $branches['master']); + $this->assertEquals('//depot/branch@1234', $branches['master']); } public function testGetBranchesWithoutStream() { + $expectedCommand = 'p4 -u user -p port changes //depot/...'; + $expectedCallback = function ($command, &$output) { + $output = 'Change 5678 on 2014/03/19 by Clark.Stuth@Clark.Stuth_test_client \'test changelist\''; + + return true; + }; + $this->processExecutor->expects($this->once()) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will($this->returnCallback($expectedCallback)); $branches = $this->perforce->getBranches(); - $this->assertEquals('//depot', $branches['master']); + $this->assertEquals('//depot@5678', $branches['master']); } public function testGetTagsWithoutStream() @@ -617,12 +629,12 @@ class PerforceTest extends \PHPUnit_Framework_TestCase $result = $this->perforce->checkServerExists('perforce.does.exist:port', $processExecutor); $this->assertTrue($result); } - + /** * Test if "p4" command is missing. - * + * * @covers \Composer\Util\Perforce::checkServerExists - * + * * @return void */ public function testCheckServerClientError() @@ -634,7 +646,7 @@ class PerforceTest extends \PHPUnit_Framework_TestCase ->method('execute') ->with($this->equalTo($expectedCommand), $this->equalTo(null)) ->will($this->returnValue(127)); - + $result = $this->perforce->checkServerExists('perforce.does.exist:port', $processExecutor); $this->assertFalse($result); } @@ -692,4 +704,18 @@ class PerforceTest extends \PHPUnit_Framework_TestCase { $this->perforce->setStream('//depot/branch'); } + + public function testCleanupClientSpecShouldDeleteClient() + { + $fs = $this->getMock('Composer\Util\Filesystem'); + $this->perforce->setFilesystem($fs); + + $testClient = $this->perforce->getClient(); + $expectedCommand = 'p4 -u ' . self::TEST_P4USER . ' -p ' . self::TEST_PORT . ' client -d ' . $testClient; + $this->processExecutor->expects($this->once())->method('execute')->with($this->equalTo($expectedCommand)); + + $fs->expects($this->once())->method('remove')->with($this->perforce->getP4ClientSpec()); + + $this->perforce->cleanupClientSpec(); + } } diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php index eabfe9ed5..bbeba908e 100644 --- a/tests/Composer/Test/Util/RemoteFilesystemTest.php +++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php @@ -130,18 +130,11 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase $this->assertAttributeEquals(50, 'lastProgress', $fs); } - public function testCallbackGetNotifyFailure404() + public function testCallbackGetPassesThrough404() { $fs = new RemoteFilesystem($this->getMock('Composer\IO\IOInterface')); - try { - $this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, 'HTTP/1.1 404 Not Found', 404, 0, 0); - $this->fail(); - } catch (\Exception $e) { - $this->assertInstanceOf('Composer\Downloader\TransportException', $e); - $this->assertEquals(404, $e->getCode()); - $this->assertContains('HTTP/1.1 404 Not Found', $e->getMessage()); - } + $this->assertNull($this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, 'HTTP/1.1 404 Not Found', 404, 0, 0)); } public function testCaptureAuthenticationParamsFromUrl() @@ -180,7 +173,7 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase protected function callGetOptionsForUrl($io, array $args = array(), array $options = array()) { - $fs = new RemoteFilesystem($io, $options); + $fs = new RemoteFilesystem($io, null, $options); $ref = new \ReflectionMethod($fs, 'getOptionsForUrl'); $ref->setAccessible(true); diff --git a/tests/Composer/Test/Util/StreamContextFactoryTest.php b/tests/Composer/Test/Util/StreamContextFactoryTest.php index b0923e6df..afb6bfc7e 100644 --- a/tests/Composer/Test/Util/StreamContextFactoryTest.php +++ b/tests/Composer/Test/Util/StreamContextFactoryTest.php @@ -20,6 +20,8 @@ class StreamContextFactoryTest extends \PHPUnit_Framework_TestCase { unset($_SERVER['HTTP_PROXY']); unset($_SERVER['http_proxy']); + unset($_SERVER['HTTPS_PROXY']); + unset($_SERVER['https_proxy']); unset($_SERVER['no_proxy']); } @@ -27,6 +29,8 @@ class StreamContextFactoryTest extends \PHPUnit_Framework_TestCase { unset($_SERVER['HTTP_PROXY']); unset($_SERVER['http_proxy']); + unset($_SERVER['HTTPS_PROXY']); + unset($_SERVER['https_proxy']); unset($_SERVER['no_proxy']); } @@ -52,7 +56,7 @@ class StreamContextFactoryTest extends \PHPUnit_Framework_TestCase ), array( $a = array('http' => array('method' => 'GET', 'max_redirects' => 20, 'follow_location' => 1)), array('http' => array('method' => 'GET')), - array('options' => $a, 'notification' => $f = function() {}), array('notification' => $f) + array('options' => $a, 'notification' => $f = function () {}), array('notification' => $f) ), ); } @@ -126,17 +130,56 @@ class StreamContextFactoryTest extends \PHPUnit_Framework_TestCase { $_SERVER['http_proxy'] = 'http://username:password@proxyserver.net'; - $context = StreamContextFactory::getContext('http://example.org', array('http' => array('method' => 'GET'))); + $context = StreamContextFactory::getContext('https://example.org', array('http' => array('method' => 'GET'))); $options = stream_context_get_options($context); - $this->assertEquals(array('http' => array( - 'proxy' => 'tcp://proxyserver.net:80', - 'request_fulluri' => true, - 'method' => 'GET', - 'header' => array("Proxy-Authorization: Basic " . base64_encode('username:password')), - 'max_redirects' => 20, - 'follow_location' => 1, - )), $options); + $expected = array( + 'http' => array( + 'proxy' => 'tcp://proxyserver.net:80', + 'request_fulluri' => true, + 'method' => 'GET', + 'header' => array("Proxy-Authorization: Basic " . base64_encode('username:password')), + 'max_redirects' => 20, + 'follow_location' => 1, + ), 'ssl' => array( + 'SNI_enabled' => true, + 'SNI_server_name' => 'example.org' + ) + ); + if (version_compare(PHP_VERSION, '5.6.0', '>=')) { + unset($expected['ssl']['SNI_server_name']); + } + $this->assertEquals($expected, $options); + } + + public function testHttpsProxyOverride() + { + if (!extension_loaded('openssl')) { + $this->markTestSkipped('Requires openssl'); + } + + $_SERVER['http_proxy'] = 'http://username:password@proxyserver.net'; + $_SERVER['https_proxy'] = 'https://woopproxy.net'; + + $context = StreamContextFactory::getContext('https://example.org', array('http' => array('method' => 'GET'))); + $options = stream_context_get_options($context); + + $expected = array( + 'http' => array( + 'proxy' => 'ssl://woopproxy.net:443', + 'request_fulluri' => true, + 'method' => 'GET', + 'max_redirects' => 20, + 'follow_location' => 1, + ), 'ssl' => array( + 'SNI_enabled' => true, + 'SNI_server_name' => 'example.org' + ) + ); + if (version_compare(PHP_VERSION, '5.6.0', '>=')) { + unset($expected['ssl']['SNI_server_name']); + } + $this->assertEquals($expected, $options); } /** diff --git a/tests/Composer/Test/Util/SvnTest.php b/tests/Composer/Test/Util/SvnTest.php index fb938d72e..b1f19ca1a 100644 --- a/tests/Composer/Test/Util/SvnTest.php +++ b/tests/Composer/Test/Util/SvnTest.php @@ -1,6 +1,7 @@ setAccessible(true); @@ -41,7 +42,7 @@ class SvnTest extends \PHPUnit_Framework_TestCase { $url = 'http://svn.example.org'; - $svn = new Svn($url, new NullIO()); + $svn = new Svn($url, new NullIO(), new Config()); $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCommand'); $reflMethod->setAccessible(true); @@ -51,6 +52,72 @@ class SvnTest extends \PHPUnit_Framework_TestCase ); } + public function testCredentialsFromConfig() + { + $url = 'http://svn.apache.org'; + + $config = new Config(); + $config->merge(array( + 'config' => array( + 'http-basic' => array( + 'svn.apache.org' => array('username' => 'foo', 'password' => 'bar') + ) + ) + )); + + $svn = new Svn($url, new NullIO, $config); + $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialString'); + $reflMethod->setAccessible(true); + + $this->assertEquals($this->getCmd(" --username 'foo' --password 'bar' "), $reflMethod->invoke($svn)); + } + + public function testCredentialsFromConfigWithCacheCredentialsTrue() + { + $url = 'http://svn.apache.org'; + + $config = new Config(); + $config->merge( + array( + 'config' => array( + 'http-basic' => array( + 'svn.apache.org' => array('username' => 'foo', 'password' => 'bar') + ) + ) + ) + ); + + $svn = new Svn($url, new NullIO, $config); + $svn->setCacheCredentials(true); + $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialString'); + $reflMethod->setAccessible(true); + + $this->assertEquals($this->getCmd(" --username 'foo' --password 'bar' "), $reflMethod->invoke($svn)); + } + + public function testCredentialsFromConfigWithCacheCredentialsFalse() + { + $url = 'http://svn.apache.org'; + + $config = new Config(); + $config->merge( + array( + 'config' => array( + 'http-basic' => array( + 'svn.apache.org' => array('username' => 'foo', 'password' => 'bar') + ) + ) + ) + ); + + $svn = new Svn($url, new NullIO, $config); + $svn->setCacheCredentials(false); + $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialString'); + $reflMethod->setAccessible(true); + + $this->assertEquals($this->getCmd(" --no-auth-cache --username 'foo' --password 'bar' "), $reflMethod->invoke($svn)); + } + private function getCmd($cmd) { if (defined('PHP_WINDOWS_VERSION_BUILD')) { diff --git a/tests/bootstrap.php b/tests/bootstrap.php index c5e16d625..908861cf5 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -12,7 +12,9 @@ error_reporting(E_ALL); -$loader = require __DIR__.'/../src/bootstrap.php'; -$loader->add('Composer\Test', __DIR__); +if (function_exists('date_default_timezone_set') && function_exists('date_default_timezone_get')) { + date_default_timezone_set(@date_default_timezone_get()); +} +require __DIR__.'/../src/bootstrap.php'; require __DIR__.'/Composer/TestCase.php';