diff --git a/.travis.yml b/.travis.yml index 81af8c474..b019c1926 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,27 @@ language: php -php: +php: - 5.3.3 - 5.3 - 5.4 - 5.5 + - 5.6 + - hhvm -before_script: composer install +matrix: + allow_failures: + - php: hhvm -script: phpunit -c tests/complete.phpunit.xml +before_script: + - sudo apt-get install parallel + - rm -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini + - 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 + +git: + depth: 5 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 47f485af5..e152f1ddc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,86 @@ -* 1.0.0-alpha6 (2012-10-23) +### 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 + * Added `composer-plugin` package type to allow extensibility, and deprecated `composer-installer` + * Added `psr-4` autoloading support and deprecated `target-dir` since it is a better alternative + * Added --no-plugins flag to replace --no-custom-installers where available + * Added `global` command to operate Composer in a user-global directory + * Added `licenses` command to list the license of all your dependencies + * Added `pre-status-cmd` and `post-status-cmd` script events to the `status` command + * Added `post-root-package-install` and `post-create-project-cmd` script events to the `create-project` command + * Added `pre-autoload-dump` script event + * Added --rollback flag to self-update + * Added --no-install flag to create-project to skip installing the dependencies + * Added a `hhvm` platform package to require Facebook's HHVM implementation of PHP + * Added `github-domains` config option to allow using GitHub Enterprise with Composer's GitHub support + * Added `prepend-autoloader` config option to allow appending Composer's autoloader instead of the default prepend behavior + * Added Perforce support to the VCS repository + * Added a vendor/composer/autoload_files.php file that lists all files being included by the files autoloader + * Added support for the `no_proxy` env var and other proxy support improvements + * Added many robustness tweaks to make sure zip downloads work more consistently and corrupted caches are invalidated + * Added the release date to `composer -V` output + * Added `autoloader-suffix` config option to allow overriding the randomly generated autoloader class suffix + * Fixed BitBucket API usage + * Fixed parsing of inferred stability flags that are more stable than the minimum stability + * Fixed installation order of plugins/custom installers + * Fixed tilde and wildcard version constraints to be more intuitive regarding stabilities + * Fixed handling of target-dir changes when updating packages + * Improved performance of the class loader + * Improved memory usage and performance of solving dependencies + * Tons of minor bug fixes and improvements + +### 1.0.0-alpha7 (2013-05-04) + + * Break: For forward compatibility, you should change your deployment scripts to run `composer install --no-dev`. The install command will install dev dependencies by default starting in the next release + * Break: The `update` command now has --dev enabled by default. --no-dev can be used to update without dev requirements, but it will create an incomplete lock file and is discouraged + * Break: Removed support for lock files created before 2012-09-15 due to their outdated unusable format + * Added `prefer-stable` flag to pick stable packages over unstable ones when possible + * Added `preferred-install` config option to always enable --prefer-source or --prefer-dist + * Added `diagnose` command to to system/network checks and find common problems + * Added wildcard support in the update whitelist, e.g. to update all packages of a vendor do `composer update vendor/*` + * Added `archive` command to archive the current directory or a given package + * Added `run-script` command to manually trigger scripts + * Added `proprietary` as valid license identifier for non-free code + * Added a `php-64bit` platform package that you can require to force a 64bit php + * Added a `lib-ICU` platform package + * Added a new official package type `project` for project-bootstrapping packages + * Added zip/dist local cache to speed up repetitive installations + * Added `post-autoload-dump` script event + * Added `Event::getDevMode` to let script handlers know if dev requirements are being installed + * Added `discard-changes` config option to control the default behavior when updating "dirty" dependencies + * Added `use-include-path` config option to make the autoloader look for files in the include path too + * Added `cache-ttl`, `cache-files-ttl` and `cache-files-maxsize` config option + * Added `cache-dir`, `cache-files-dir`, `cache-repo-dir` and `cache-vcs-dir` config option + * Added support for using http(s) authentication to non-github repos + * Added support for using multiple autoloaders at once (e.g. PHPUnit + application both using Composer autoloader) + * Added support for .inc files for classmap autoloading (legacy support, do not do this on new projects!) + * Added support for version constraints in show command, e.g. `composer show monolog/monolog 1.4.*` + * Added support for svn repositories containing packages in a deeper path (see package-path option) + * Added an `artifact` repository to scan a directory containing zipped packages + * Added --no-dev flag to `install` and `update` commands + * Added --stability (-s) flag to create-project to lower the required stability + * Added --no-progress to `install` and `update` to hide the progress indicators + * Added --available (-a) flag to the `show` command to display only available packages + * Added --name-only (-N) flag to the `show` command to show only package names (one per line, no formatting) + * Added --optimize-autoloader (-o) flag to optimize the autoloader from the `install` and `update` commands + * Added -vv and -vvv flags to get more verbose output, can be useful to debug some issues + * Added COMPOSER_NO_INTERACTION env var to do the equivalent of --no-interaction (should be set on build boxes, CI, PaaS) + * Added PHP 5.2 compatibility to the autoloader configuration files so they can be used to configure another autoloader + * Fixed handling of platform requirements of the root package when installing from lock + * Fixed handling of require-dev dependencies + * Fixed handling of unstable packages that should be downgraded to stable packages when updating to new version constraints + * Fixed parsing of the `~` operator combined with unstable versions + * Fixed the `require` command corrupting the json if the new requirement was invalid + * Fixed support of aliases used together with `#` constraints + * Improved output of dependency solver problems by grouping versions of a package together + * Improved performance of classmap generation + * Improved mercurial support in various places + * Improved lock file format to minimize unnecessary diffs + * Improved the `config` command to support all options + * Improved the coverage of the `validate` command + * Tons of minor bug fixes and improvements + +### 1.0.0-alpha6 (2012-10-23) * Schema: Added ability to pass additional options to repositories (i.e. ssh keys/client certificates to secure private repos) * Schema: Added a new `~` operator that should be prefered over `>=`, see http://getcomposer.org/doc/01-basic-usage.md#package-versions @@ -23,7 +105,7 @@ * Improved performance of a few essential code paths * Many bug small fixes and docs improvements -* 1.0.0-alpha5 (2012-08-18) +### 1.0.0-alpha5 (2012-08-18) * Added `dump-autoload` command to only regenerate the autoloader * Added --optimize to `dump-autoload` to generate a more performant classmap-based autoloader for production @@ -41,7 +123,7 @@ * Improved error reporting on network failures and some other edge cases * Various minor bug fixes and docs improvements -* 1.0.0-alpha4 (2012-07-04) +### 1.0.0-alpha4 (2012-07-04) * Break: The default `minimum-stability` is now `stable`, [read more](https://groups.google.com/d/topic/composer-dev/_g3ASeIFlrc/discussion) * Break: Custom installers now receive the IO instance and a Composer instance in their constructor @@ -69,7 +151,7 @@ * Cleaned up / refactored the dependency solver code as well as the output for unsolvable requirements * Various bug fixes and docs improvements -* 1.0.0-alpha3 (2012-05-13) +### 1.0.0-alpha3 (2012-05-13) * Schema: Added `require-dev` for development-time requirements (tests, etc), install with --dev * Schema: Added author.role to list the author's role in the project @@ -91,7 +173,7 @@ * Fixed various bugs relating to package aliasing, proxy configuration, binaries * Various bug fixes and docs improvements -* 1.0.0-alpha2 (2012-04-03) +### 1.0.0-alpha2 (2012-04-03) * Added `create-project` command to install a project from scratch with composer * Added automated `classmap` autoloading support for non-PSR-0 compliant projects @@ -106,6 +188,6 @@ * Removed dependency on filter_var * Various robustness & error handling improvements, docs fixes and more bug fixes -* 1.0.0-alpha1 (2012-03-01) +### 1.0.0-alpha1 (2012-03-01) * Initial release diff --git a/README.md b/README.md index 115014180..cf07c54b5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ 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. @@ -13,13 +13,13 @@ Installation / Usage 1. Download the [`composer.phar`](https://getcomposer.org/composer.phar) executable or use the installer. ``` sh - $ curl -s https://getcomposer.org/installer | php + $ curl -sS https://getcomposer.org/installer | php ``` - 2. Create a composer.json defining your dependencies. Note that this example is a short version for applications that are not meant to be published as packages -themselves. To create libraries/packages please read the [guidelines](https://packagist.org/about). +themselves. To create libraries/packages please read the +[documentation](http://getcomposer.org/doc/02-libraries.md). ``` json { @@ -47,24 +47,7 @@ You can now run Composer by executing the `bin/composer` script: `php /path/to/c Global installation of Composer (manual) ---------------------------------------- -Since Composer works with the current working directory it is possible to install it -in a system wide way. - -1. Change into a directory in your path like `cd /usr/local/bin` -2. Get Composer `curl -s https://getcomposer.org/installer | php` -3. Make the phar executable `chmod a+x composer.phar` -4. Change into a project directory `cd /path/to/my/project` -5. Use Composer as you normally would `composer.phar install` -6. Optionally you can rename the composer.phar to composer to make it easier - -Global installation of Composer (via homebrew) ----------------------------------------------- - -Composer is part of the homebrew-php project. - -1. Tap the homebrew-php repository into your brew installation if you haven't done yet: `brew tap josegonzalez/homebrew-php` -2. Run `brew install josegonzalez/php/composer`. -3. Use Composer with the `composer` command. +Follow instructions [in the documentation](http://getcomposer.org/doc/00-intro.md#globally) Updating Composer ----------------- @@ -82,7 +65,7 @@ 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) +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). @@ -123,4 +106,4 @@ Acknowledgments - This project's Solver started out as a PHP port of openSUSE's [Libzypp satsolver](http://en.opensuse.org/openSUSE:Libzypp_satsolver). - This project uses hiddeninput.exe to prompt for passwords on windows, sources - and details can be found on the [github page of the project](https://github.com/Seldaek/hidden-input). \ No newline at end of file + and details can be found on the [github page of the project](https://github.com/Seldaek/hidden-input). diff --git a/bin/compile b/bin/compile index 8b1d66c43..c4a6b1105 100755 --- a/bin/compile +++ b/bin/compile @@ -8,5 +8,10 @@ use Composer\Compiler; error_reporting(-1); ini_set('display_errors', 1); -$compiler = new Compiler(); -$compiler->compile(); +try { + $compiler = new Compiler(); + $compiler->compile(); +} catch (\Exception $e) { + echo 'Failed to compile phar: ['.get_class($e).'] '.$e->getMessage().' at '.$e->getFile().':'.$e->getLine(); + exit(1); +} diff --git a/bin/composer b/bin/composer index 5899be19c..3a1b5df02 100755 --- a/bin/composer +++ b/bin/composer @@ -1,6 +1,10 @@ #!/usr/bin/env php $code) { - $code = sprintf('"%s"%s', $code, $item === $last ? '' : ', '); + $code = sprintf('"%s"%s', trim($code), $item === $last ? '' : ', '); $length = strlen($line) + strlen($code) - 1; if ($length > 76) { $line = rtrim($line); diff --git a/composer.json b/composer.json index 33b8359ee..718b06780 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,11 +23,14 @@ }, "require": { "php": ">=5.3.2", - "justinrainbow/json-schema": "1.1.*", + "justinrainbow/json-schema": "~1.1", "seld/jsonlint": "1.*", - "symfony/console": "~2.1@dev", - "symfony/finder": "~2.1", - "symfony/process": "~2.1@dev" + "symfony/console": "~2.3", + "symfony/finder": "~2.2", + "symfony/process": "~2.1" + }, + "require-dev": { + "phpunit/phpunit": "~3.7" }, "suggest": { "ext-zip": "Enabling the zip extension allows you to unzip archives, and allows gzip compression of all internet traffic", @@ -36,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 62b2c222e..cfeea277e 100644 --- a/composer.lock +++ b/composer.lock @@ -1,60 +1,104 @@ { - "hash": "f13f9a6a377c842c36fda6109bbbc465", + "_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", + "This file is @generated automatically" + ], + "hash": "eeb4afc3be46412ec15842ce4af01f0b", "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" }, - "time": "2012-01-02 21:33:17", + "require-dev": { + "json-schema/json-schema-test-suite": "1.1.0", + "phpdocumentor/phpdocumentor": "~2", + "phpunit/phpunit": "~3.7" + }, + "bin": [ + "bin/validate-json" + ], "type": "library", - "installation-source": "dist", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, "autoload": { "psr-0": { "JsonSchema": "src/" } - } + }, + "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.0.1", + "version": "1.3.0", "source": { "type": "git", - "url": "http://github.com/Seldaek/jsonlint", - "reference": "1.0.1" + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "a7bc2ec9520ad15382292591b617c43bdb1fec35" }, "dist": { "type": "zip", - "url": "https://github.com/Seldaek/jsonlint/zipball/1.0.1", - "reference": "1.0.1", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/a7bc2ec9520ad15382292591b617c43bdb1fec35", + "reference": "a7bc2ec9520ad15382292591b617c43bdb1fec35", "shasum": "" }, "require": { "php": ">=5.3.0" }, - "time": "2012-08-13 07:00:11", "bin": [ "bin/jsonlint" ], "type": "library", - "installation-source": "dist", "autoload": { - "psr-0": { - "Seld\\JsonLint": "src/" + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -62,167 +106,589 @@ { "name": "Jordi Boggiano", "email": "j.boggiano@seld.be", - "homepage": "http://seld.be", - "role": "Developer" + "homepage": "http://seld.be" } ], "description": "JSON Linter", "keywords": [ "json", - "parser", "linter", + "parser", "validator" - ] + ], + "time": "2014-09-05 15:36:20" }, { "name": "symfony/console", - "version": "dev-master", + "version": "v2.5.4", "target-dir": "Symfony/Component/Console", "source": { "type": "git", - "url": "https://github.com/symfony/Console", - "reference": "003a487a674175a1d2e62f205ecd91ffe85554b1" + "url": "https://github.com/symfony/Console.git", + "reference": "748beed2a1e73179c3f5154d33fe6ae100c1aeb1" }, "dist": { "type": "zip", - "url": "https://github.com/symfony/Console/archive/003a487a674175a1d2e62f205ecd91ffe85554b1.zip", - "reference": "003a487a674175a1d2e62f205ecd91ffe85554b1", + "url": "https://api.github.com/repos/symfony/Console/zipball/748beed2a1e73179c3f5154d33fe6ae100c1aeb1", + "reference": "748beed2a1e73179c3f5154d33fe6ae100c1aeb1", "shasum": "" }, "require": { "php": ">=5.3.3" }, - "time": "2012-11-19 12:58:52", + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.1" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.5-dev" } }, - "installation-source": "source", "autoload": { "psr-0": { "Symfony\\Component\\Console\\": "" } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "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" + "homepage": "http://symfony.com", + "time": "2014-08-14 16:10:54" }, { "name": "symfony/finder", - "version": "v2.1.3", + "version": "v2.5.4", "target-dir": "Symfony/Component/Finder", "source": { "type": "git", - "url": "https://github.com/symfony/Finder", - "reference": "v2.1.3" + "url": "https://github.com/symfony/Finder.git", + "reference": "f40854d1a19c339c7f969f8f6d6d6e9153311c4c" }, "dist": { "type": "zip", - "url": "https://github.com/symfony/Finder/zipball/v2.1.3", - "reference": "v2.1.3", + "url": "https://api.github.com/repos/symfony/Finder/zipball/f40854d1a19c339c7f969f8f6d6d6e9153311c4c", + "reference": "f40854d1a19c339c7f969f8f6d6d6e9153311c4c", "shasum": "" }, "require": { "php": ">=5.3.3" }, - "time": "2012-10-20 00:10:30", "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.5-dev" } }, - "installation-source": "source", "autoload": { "psr-0": { - "Symfony\\Component\\Finder": "" + "Symfony\\Component\\Finder\\": "" } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "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" + "homepage": "http://symfony.com", + "time": "2014-09-03 09:00:14" }, { "name": "symfony/process", - "version": "dev-master", + "version": "v2.5.5", "target-dir": "Symfony/Component/Process", "source": { "type": "git", - "url": "https://github.com/symfony/Process", - "reference": "a11b312f99a5a8bf88c0ea5e7660c13af79f964f" + "url": "https://github.com/symfony/Process.git", + "reference": "8a1ec96c4e519cee0fb971ea48a1eb7369dda54b" }, "dist": { "type": "zip", - "url": "https://github.com/symfony/Process/archive/a11b312f99a5a8bf88c0ea5e7660c13af79f964f.zip", - "reference": "a11b312f99a5a8bf88c0ea5e7660c13af79f964f", + "url": "https://api.github.com/repos/symfony/Process/zipball/8a1ec96c4e519cee0fb971ea48a1eb7369dda54b", + "reference": "8a1ec96c4e519cee0fb971ea48a1eb7369dda54b", "shasum": "" }, "require": { "php": ">=5.3.3" }, - "time": "2012-11-13 14:08:04", "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.5-dev" } }, - "installation-source": "source", "autoload": { "psr-0": { "Symfony\\Component\\Process\\": "" } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "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-09-23 05:25:11" + } + ], + "packages-dev": [ + { + "name": "phpunit/php-code-coverage", + "version": "1.2.18", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b", + "reference": "fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": ">=1.3.0@stable", + "phpunit/php-text-template": ">=1.2.0@stable", + "phpunit/php-token-stream": ">=1.1.3,<1.3.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*@dev" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.0.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2014-09-02 10:13:14" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.3.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/acd690379117b042d1c8af1fafd61bde001bf6bb", + "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "File/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2013-10-10 15:34:57" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", + "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "Text/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2014-01-30 17:20:04" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/19689d4354b295ee3d8c54b4f42c3efb69cbc17c", + "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2013-08-02 07:42:54" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "ad4e1e23ae01b483c16f600ff1bebec184588e32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/ad4e1e23ae01b483c16f600ff1bebec184588e32", + "reference": "ad4e1e23ae01b483c16f600ff1bebec184588e32", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2014-03-03 05:10:30" + }, + { + "name": "phpunit/phpunit", + "version": "3.7.37", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "ae6cefd7cc84586a5ef27e04bae11ee940ec63dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ae6cefd7cc84586a5ef27e04bae11ee940ec63dc", + "reference": "ae6cefd7cc84586a5ef27e04bae11ee940ec63dc", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.3.3", + "phpunit/php-code-coverage": "~1.2", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.1", + "phpunit/php-timer": "~1.0", + "phpunit/phpunit-mock-objects": "~1.2", + "symfony/yaml": "~2.0" + }, + "require-dev": { + "pear-pear.php.net/pear": "1.9.4" + }, + "suggest": { + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "composer/bin/phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPUnit/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "", + "../../symfony/yaml/" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "http://www.phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2014-04-30 12:24:19" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "1.2.3", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "1.2.3" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects/archive/1.2.3.zip", + "reference": "1.2.3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-text-template": ">=1.1.1@stable" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "autoload": { + "classmap": [ + "PHPUnit/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2013-01-13 10:24:48" + }, + { + "name": "symfony/yaml", + "version": "v2.5.4", + "target-dir": "Symfony/Component/Yaml", + "source": { + "type": "git", + "url": "https://github.com/symfony/Yaml.git", + "reference": "01a7695bcfb013d0a15c6757e15aae120342986f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/01a7695bcfb013d0a15c6757e15aae120342986f", + "reference": "01a7695bcfb013d0a15c6757e15aae120342986f", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\Yaml\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { "name": "Symfony Community", "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" } ], - "description": "Symfony Process Component", - "homepage": "http://symfony.com" + "description": "Symfony Yaml Component", + "homepage": "http://symfony.com", + "time": "2014-08-31 03:22:04" } ], - "packages-dev": null, - "aliases": [ - - ], + "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "symfony/console": 20, - "symfony/process": 20 - } + "stability-flags": [], + "prefer-stable": false, + "platform": { + "php": ">=5.3.2" + }, + "platform-dev": [] } diff --git a/doc/00-intro.md b/doc/00-intro.md index 99e15ba8a..81b8c9295 100644 --- a/doc/00-intro.md +++ b/doc/00-intro.md @@ -19,9 +19,9 @@ The problem that Composer solves is this: a) You have a project that depends on a number of libraries. -b) Some of those libraries depend on other libraries . +b) Some of those libraries depend on other libraries. -c) You declare the things you depend on +c) You declare the things you depend on. d) Composer finds out which versions of which packages need to be installed, and installs them (meaning it downloads them into your project). @@ -33,15 +33,29 @@ 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`. +## 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 +incompatibilities. + +To install packages from sources instead of simple zip archives, you will need +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 ### Downloading the Composer Executable @@ -51,7 +65,16 @@ any version beginning with `1.2`. To actually get Composer, we need to do two things. The first one is installing Composer (again, this means downloading it into your project): - $ curl -s https://getcomposer.org/installer | php +```sh +curl -sS https://getcomposer.org/installer | php +``` + +> **Note:** If the above fails for some reason, you can download the installer +> with `php` instead: + +```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 @@ -61,7 +84,9 @@ 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 -s https://getcomposer.org/installer | php -- --install-dir=bin +```sh +curl -sS https://getcomposer.org/installer | php -- --install-dir=bin +``` #### Globally @@ -71,11 +96,27 @@ executable and invoke it without `php`. You can run these commands to easily access `composer` from anywhere on your system: - $ curl -s https://getcomposer.org/installer | php - $ sudo 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) + +Composer is part of the homebrew-php project. + +```sh +brew update +brew tap homebrew/dupes +brew tap homebrew/php +brew install composer +``` + ## Installation - Windows ### Using the Installer @@ -91,38 +132,47 @@ just call `composer` from any directory in your command line. 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 +``` -Create a new `.bat` file alongside composer: +> **Note:** If the above fails due to readfile, use the `http` url or enable php_openssl.dll in php.ini - C:\bin>notepad composer.bat +Create a new `composer.bat` file alongside `composer.phar`: -Paste the following in, it simply proxies all arguments to composer: +```sh +C:\bin>echo @php "%~dp0composer.phar" %*>composer.bat +``` - @ECHO OFF - SET composerScript=composer.phar - php "%~dp0%composerScript%" %* +Close your current terminal. Test usage with a new terminal: -Save the file. 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 -Next, run the `install` command to resolve and download dependencies: +We will now use Composer to install the dependencies of the project. If you +don't have a `composer.json` file in the current directory please skip to the +[Basic Usage](01-basic-usage.md) chapter. + +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 +``` -This will download monolog into the `vendor/monolog/monolog` directory. +Following the [example above](#declaring-dependencies), this will download +monolog into the `vendor/monolog/monolog` directory. ## Autoloading @@ -131,9 +181,11 @@ 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'; +``` -Woh! Now start using monolog! To keep learning more about Composer, keep +Woah! Now start using monolog! To keep learning more about Composer, keep reading the "Basic Usage" chapter. [Basic Usage](01-basic-usage.md) → diff --git a/doc/01-basic-usage.md b/doc/01-basic-usage.md index 971aa5bfd..06d2a8c5d 100644 --- a/doc/01-basic-usage.md +++ b/doc/01-basic-usage.md @@ -4,20 +4,26 @@ To install Composer, you just need to download the `composer.phar` executable. - $ curl -s https://getcomposer.org/installer | php +```sh +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 +```sh +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 -s https://getcomposer.org/installer | php -- --help +> ```sh +> curl -sS https://getcomposer.org/installer | php -- --help +> ``` ## `composer.json`: Project Setup @@ -34,11 +40,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.*`). @@ -58,28 +66,40 @@ smaller decoupled parts. ### Package Versions -We are requiring version `1.0.*` of monolog. This means any version in the `1.0` -development branch. It would match `1.0.0`, `1.0.2` or `1.0.20`. +In the previous example we were requiring version `1.0.*` of monolog. This +means any version in the `1.0` development branch. It would match `1.0.0`, +`1.0.2` or `1.0.20`. Version constraints can be specified in a few different ways. -* **Exact version:** You can specify the exact version of a package, for - example `1.0.2`. +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 comma (`,`) will be treated as a **logical AND**. A pipe (|) 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. -* **Range:** By using comparison operators you can specify ranges of valid - versions. Valid operators are `>`, `>=`, `<`, `<=`, `!=`. An example range - would be `>=1.0`. You can define multiple ranges, separated by a comma: - `>=1.0,<2.0`. +### Next Significant Release (Tilde Operator) -* **Wildcard:** You can specify a pattern with a `*` wildcard. `1.0.*` is the - equivalent of `>=1.0,<1.1`. +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 +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 +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. -* **Next Significant Release (Tilde Operator):** 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 it is mostly useful for projects - respecting semantic versioning. A common usage would be to mark the minimum - minor version you depend on, like `~1.2`, since in theory there should be no - backwards compatibility breaks until 2.0, that works well. +> **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 to also get RC, beta, alpha or dev versions of your dependencies you can do @@ -92,7 +112,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. @@ -116,18 +138,36 @@ to those specific versions. This is important because the `install` command checks if a lock file is present, and if it is, it downloads the versions specified there (regardless of what `composer.json` -says). This means that anyone who sets up the project will download the exact -same version of the dependencies. +says). + +This means that anyone who sets up the project will download the exact +same version of the dependencies. Your CI server, production machines, other +developers in your team, everything and everyone runs on the same dependencies, which +mitigates the potential for bugs affecting only some parts of the deployments. Even if you +develop alone, in six months when reinstalling the project you can feel confident the +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: + +```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). @@ -153,33 +193,38 @@ 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-0": {"Acme": "src/"} - } +```json +{ + "autoload": { + "psr-4": {"Acme\\": "src/"} } +} +``` -Composer will register a -[PSR-0](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md) -autoloader for the `Acme` namespace. +Composer will register a [PSR-4](http://www.php-fig.org/psr/psr-4/) autoloader +for the `Acme` namespace. 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/Acme/Foo.php` containing an `Acme\Foo` class. +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 the `vendor/autoload.php` file. @@ -188,15 +233,17 @@ 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-0 autoloading, classmap is also supported. This allows -classes to be autoloaded even if they do not conform to PSR-0. See the +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 [autoload reference](04-schema.md#autoload) for more details. > **Note:** Composer provides its own autoloader. If you don't want to use -that one, you can just include `vendor/composer/autoload_namespaces.php`, -which returns an associative array mapping namespaces to directories. +that one, you can just include `vendor/composer/autoload_*.php` files, +which return associative arrays allowing you to configure your own autoloader. ← [Intro](00-intro.md) | [Libraries](02-libraries.md) → diff --git a/doc/02-libraries.md b/doc/02-libraries.md index 68f8d96fc..561f3aa79 100644 --- a/doc/02-libraries.md +++ b/doc/02-libraries.md @@ -1,6 +1,6 @@ # Libraries -This chapter will tell you how to make your library installable through composer. +This chapter will tell you how to make your library installable through Composer. ## Every project is a package @@ -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. @@ -29,11 +31,15 @@ convention is all lowercase and dashes for word separation. ## Platform packages Composer has platform packages, which are virtual packages for things that are -installed on the system but are not actually installable by composer. This +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`. + 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 @@ -41,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. @@ -58,26 +64,34 @@ 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. ### Tags 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 for RC, -beta, alpha or patch. +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. 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:** If you specify an explicit version in `composer.json`, the tag name must match the specified version. +> **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 +> (e.g. tag `v1.0.0` will result in version `1.0.0`). ### Branches @@ -91,15 +105,17 @@ 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 dev version, it will install it from source. +> **Note:** When you install a development version, it will be automatically +> pulled from its `source`. See the [`install`](03-cli.md#install) command +> for more details. ### Aliases -It is possible alias branch names to versions. For example, you could alias +It is possible to alias branch names to versions. For example, you could alias `dev-master` to `1.0.x-dev`, which would allow you to require `1.0.x-dev` in all the packages. @@ -115,52 +131,27 @@ on it. It only has an effect on the main project. If you do not want to commit the lock file and you are using git, add it to the `.gitignore`. -## Light-weight distribution packages - -Including the tests and other useless information like `.travis.yml` in -distributed packages is not a good idea. - -The `.gitattributes` file is a git specific file like `.gitignore` also living -at the root directory of your library. It overrides local and global -configuration (`.git/config` and `~/.gitconfig` respectively) when present and -tracked by git. - -Use `.gitattributes` to prevent unwanted files from bloating the zip -distribution packages. - - // .gitattributes - /Tests export-ignore - phpunit.xml.dist export-ignore - Resources/doc/ export-ignore - .travis.yml export-ignore - -Test it by inspecting the zip file generated manually: - - git archive branchName --format zip -o file.zip - -> **Note:** Files would be still tracked by git just not included in the -> distribution. This will only work for GitHub packages installed from -> dist (i.e. tagged releases) for now. - ## Publishing to a VCS Once you have a vcs repository (version control system, e.g. git) containing a `composer.json` file, your library is already composer-installable. In this example we will publish the `acme/hello-world` library on GitHub under -`github.com/composer/hello-world`. +`github.com/username/hello-world`. -Now, To test installing the `acme/hello-world` package, we create a new +Now, to test installing the `acme/hello-world` package, we create a new project locally. We will call it `acme/blog`. This blog will depend on `acme/hello-world`, which in turn depends on `monolog/monolog`. We can 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 @@ -170,23 +161,25 @@ 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/composer/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). -That's all. You can now install the dependencies by running composer's +That's all. You can now install the dependencies by running Composer's `install` command! **Recap:** Any git/svn/hg repository containing a `composer.json` can be added @@ -202,8 +195,8 @@ The other thing that you may have noticed is that we did not specify a package repository for `monolog/monolog`. How did that work? The answer is packagist. [Packagist](https://packagist.org/) is the main package repository for -composer, and it is enabled by default. Anything that is published on -packagist is available automatically through composer. Since monolog +Composer, and it is enabled by default. Anything that is published on +packagist is available automatically through Composer. Since monolog [is on packagist](https://packagist.org/packages/monolog/monolog), we can depend on it without having to specify any additional repositories. diff --git a/doc/03-cli.md b/doc/03-cli.md index 7bec79bba..d0fae39cb 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. @@ -21,6 +21,12 @@ The following options are available with every command: * **--no-ansi:** Disable ANSI output. * **--version (-V):** Display this application version. +## Process Exit Codes + +* **0:** OK +* **1:** Generic/unknown error code +* **2:** Dependency solving error code + ## init In the [Libraries](02-libraries.md) chapter we looked at how to create a @@ -30,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 @@ -48,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 @@ -73,14 +83,13 @@ resolution. * **--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:** By default composer will only install required packages. By - passing this option you can also make it install packages referenced by - `require-dev`. +* **--dev:** Install packages listed in `require-dev` (this is the default behavior). +* **--no-dev:** Skip installing packages listed in `require-dev`. * **--no-scripts:** Skips execution of scripts defined in `composer.json`. -* **--no-custom-installers:** Disables custom installers. +* **--no-plugins:** Disables plugins. * **--no-progress:** Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters. -* **--optimize-autoloader (-o):** Convert PSR-0 autoloading to classmap to get a faster +* **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default. @@ -89,39 +98,52 @@ 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. * **--dry-run:** Simulate the command without actually doing anything. -* **--dev:** Install packages listed in `require-dev`. +* **--dev:** Install packages listed in `require-dev` (this is the default behavior). +* **--no-dev:** Skip installing packages listed in `require-dev`. * **--no-scripts:** Skips execution of scripts defined in `composer.json`. -* **--no-custom-installers:** Disables custom installers. +* **--no-plugins:** Disables plugins. * **--no-progress:** Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters. -* **--optimize-autoloader (-o):** Convert PSR-0 autoloading to classmap to get a faster +* **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default. +* **--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. ## 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. @@ -129,7 +151,9 @@ 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 @@ -139,6 +163,51 @@ to the command. * **--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 +* **--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 + +The global command allows you to run other commands like `install`, `require` +or `update` as if you were running them from the [COMPOSER_HOME](#composer-home) +directory. + +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: + +```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: + +```sh +php composer.phar global update +``` ## search @@ -146,7 +215,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. @@ -158,39 +229,53 @@ 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 * **--installed (-i):** List the packages that are installed. * **--platform (-p):** List only platform packages (php & extensions). * **--self (-s):** List the root package info. -* **--dev:** Include dev-required packages when combined with **--installed** or **--platform**. + +## 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 @@ -198,13 +283,15 @@ 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 @@ -217,7 +304,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:** Wether or not composer do a complete validation. ## status @@ -225,34 +318,56 @@ 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: + +```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 have to run the command with `root` privileges +you may have to run the command with `root` privileges - $ sudo composer self-update +```sh +sudo composer self-update +``` + +### Options + +* **--rollback (-r):** Rollback to the last version you had installed. +* **--clean-backups:** Delete old backups during an update. This makes the current version of composer the only backup available after the update. ## config 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 @@ -262,7 +377,7 @@ the local composer.json file or the global config.json file. configuration value. For settings that can take an array of values (like `github-protocols`), more than one setting-value arguments are allowed. -See the [config schema section](04-schema.md#config-root-only) for valid configuration +See the [config schema section](04-schema.md#config) for valid configuration options. ### Options @@ -284,7 +399,9 @@ the global config file. 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 @@ -303,9 +420,14 @@ To create a new project using composer you can use the "create-project" command. Pass it a package name, and the directory to create the project in. You can also provide a version as third argument, otherwise the latest version is used. -The directory is not allowed to exist, it will be created during installation. +If the directory does not currently exist, it will be created during installation. + +```sh +php composer.phar create-project doctrine/orm path 2.2.* +``` - php composer.phar create-project doctrine/orm path 2.2.0 +It is also possible to run the command without params in a directory with an +existing `composer.json` file to bootstrap a project. By default the command checks for the packages on packagist.org. @@ -318,7 +440,8 @@ By default the command checks for the packages on packagist.org. * **--prefer-source:** Install packages from `source` when available. * **--prefer-dist:** Install packages from `dist` when available. * **--dev:** Install packages listed in `require-dev`. -* **--no-custom-installers:** Disables custom installers. +* **--no-install:** Disables installation of the vendors. +* **--no-plugins:** Disables plugins. * **--no-scripts:** Disables the execution of the scripts defined in the root package. * **--no-progress:** Removes the progress display that can mess with some @@ -333,31 +456,70 @@ If you need to update the autoloader because of new classes in a classmap package for example, you can use "dump-autoload" to do that without having to go through an install or update. -Additionally, it can dump an optimized autoloader that converts PSR-0 packages +Additionally, it can dump an optimized autoloader that converts PSR-0/4 packages into classmap ones for performance reasons. In large applications with many classes, the autoloader can take up a substantial portion of every request's time. Using classmaps for everything is less convenient in development, but -using this option you can still use PSR-0 for convenience and classmaps for +using this option you can still use PSR-0/4 for convenience and classmaps for performance. ### Options -* **--optimize (-o):** Convert PSR-0 autoloading to classmap to get a faster +* **--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. + +## licenses + +Lists the name, version and license of every package installed. Use +`--format=json` to get machine readable output. + +## run-script + +To run [scripts](articles/scripts.md) manually you can use this command, +just give it the script name and optionally --no-dev to disable the dev mode. + +## diagnose + +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. + +```sh +php composer.phar diagnose +``` + +## archive + +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. + +```sh +php composer.phar archive vendor/package 2.0.21 --format=zip +``` + +### Options + +* **--format (-f):** Format of the resulting archive: tar or zip (default: + "tar") +* **--dir:** Write the archive to this directory (default: ".") ## help To get more information about a certain command, just use `help`. - $ php composer.phar help install +```sh +php composer.phar help install +``` ## Environment variables You can set a number of environment variables that override certain settings. Whenever possible it is recommended to specify these settings in the `config` -section of `composer.json` instead. It is worth noting that that the env vars -will always take precedence over the values specified in `composer.json`. +section of `composer.json` instead. It is worth noting that the env vars will +always take precedence over the values specified in `composer.json`. ### COMPOSER @@ -366,7 +528,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 @@ -380,7 +544,7 @@ directory other than `vendor`. ### COMPOSER_BIN_DIR -By setting this option you can change the `bin` ([Vendor Bins](articles/vendor-bins.md)) +By setting this option you can change the `bin` ([Vendor Binaries](articles/vendor-binaries.md)) directory to something other than `vendor/bin`. ### http_proxy or HTTP_PROXY @@ -394,6 +558,28 @@ some tools like git or curl will only use the lower-cased `http_proxy` version. Alternatively you can also define the git proxy using `git config --global http.proxy `. +### no_proxy + +If you are behind a proxy and would like to disable it for certain domains, you +can use the `no_proxy` env var. Simply set it to a comma separated list of +domains the proxy should *not* be used for. + +The env var accepts domains, IP addresses, and IP address blocks in CIDR +notation. You can restrict the filter to a particular port (e.g. `:80`). You +can also set it to `*` to ignore the proxy for all HTTP requests. + +### HTTP_PROXY_REQUEST_FULLURI + +If you use a proxy but it does not support the request_fulluri flag, then you +should set this env var to `false` or `0` to prevent composer from setting the +request_fulluri option. + +### HTTPS_PROXY_REQUEST_FULLURI + +If you use a proxy but it does not support the request_fulluri flag for HTTPS +requests, then you should set this env var to `false` or `0` to prevent composer +from setting the request_fulluri option. + ### COMPOSER_HOME The `COMPOSER_HOME` var allows you to change the composer home directory. This @@ -418,9 +604,26 @@ This file allows you to set [configuration](04-schema.md#config) and In case global configuration matches _local_ configuration, the _local_ configuration in the project's `composer.json` always wins. +### COMPOSER_CACHE_DIR + +The `COMPOSER_CACHE_DIR` var allows you to change the composer cache directory, +which is also configurable via the [`cache-dir`](04-schema.md#config) option. + +By default it points to $COMPOSER_HOME/cache on \*nix and OSX, and +`C:\Users\\AppData\Local\Composer` (or `%LOCALAPPDATA%/Composer`) on Windows. + ### COMPOSER_PROCESS_TIMEOUT This env var controls the time composer waits for commands (such as git commands) to finish executing. The default value is 300 seconds (5 minutes). +### COMPOSER_DISCARD_CHANGES + +This env var controls the discard-changes [config option](04-schema.md#config). + +### COMPOSER_NO_INTERACTION + +If set to 1, this env var will make composer behave as if you passed the +`--no-interaction` flag to every command. This can be set on build boxes/CI. + ← [Libraries](02-libraries.md) | [Schema](04-schema.md) → diff --git a/doc/04-schema.md b/doc/04-schema.md index 0f7096f12..36aed5344 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`. @@ -50,21 +50,23 @@ Required for published packages (libraries). ### version -The version of the package. +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` with an optional suffix of `-dev`, -`-alphaN`, `-betaN` or `-RCN`. +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. 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 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 @@ -84,14 +86,20 @@ that needs some special logic, you can define a custom type. This could be a all be specific to certain projects, and they will need to provide an installer capable of installing packages of that type. -Out of the box, composer supports three types: +Out of the box, composer supports four types: - **library:** This is the default. It will simply copy the files to `vendor`. +- **project:** This denotes a project rather than a library. For example + application shells like the [Symfony standard edition](https://github.com/symfony/symfony-standard), + CMSs like the [SilverStripe installer](https://github.com/silverstripe/silverstripe-installer) + or full fledged applications distributed as packages. This can for example + be used by IDEs to provide listings of projects to initialize when creating + a new workspace. - **metapackage:** An empty package that contains requirements and will trigger their installation, but contains no files and will not write anything to the filesystem. As such, it does not require a dist or source key to be installable. -- **composer-installer:** A package of type `composer-installer` provides an +- **composer-plugin:** A package of type `composer-plugin` may provide an installer for other packages that have a custom type. Read more in the [dedicated article](articles/custom-installers.md). @@ -105,11 +113,11 @@ searching and filtering. Examples: - logging - events - database - redis - templating +- logging +- events +- database +- redis +- templating Optional. @@ -133,47 +141,54 @@ 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/). -An Example: +For closed-source software, you may use `"proprietary"` as the license identifier. - { - "license": "MIT" - } +An Example: +```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. @@ -191,22 +206,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. @@ -225,12 +242,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. @@ -241,11 +260,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. @@ -253,36 +274,58 @@ All links are optional fields. These allow you to further restrict or expand the stability of a package beyond the scope of the [minimum-stability](#minimum-stability) setting. You can apply them to a constraint, or just apply them to an empty constraint if you want to -allow unstable packages of a dependency's dependency for example. +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: + +```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 blocked to a given state, even +commit) for dev versions to make sure they are locked to a given state, even when you run update. These only work if you explicitly require a dev version -and append the reference with `#`. Note that while this is convenient at -times, it should not really be how you use packages in the long term. You -should always try to switch to tagged releases as soon as you can, especially -if the project you work on will not be touched for a while. +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" } - -It is possible to inline-alias a package constraint so that it matches a -constraint that it otherwise would not. For more information [see the +} +``` + +> **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 +> composer.json metadata will still be read from the branch name you specify +> before the hash. Because of that in some cases it will not be a practical +> workaround, and you should always try to switch to tagged releases as soon +> as you can. + +It is also possible to inline-alias a package constraint so that it matches +a constraint that it otherwise would not. For more information [see the aliases article](articles/aliases.md). #### require @@ -293,24 +336,20 @@ unless those requirements can be met. #### require-dev (root-only) Lists packages required for developing this package, or running -tests, etc. The dev requirements of the root package only will be installed -if `install` or `update` is ran with `--dev`. - -Packages listed here and their dependencies can not overrule the resolution -found with the packages listed in require. This is even true if a different -version of a package would be installable and solve the conflict. The reason -is that `install --dev` produces the exact same state as just `install`, apart -from the additional dev packages. - -If you run into such a conflict, you can specify the conflicting package in -the require section and require the right version number to resolve the -conflict. +tests, etc. The dev requirements of the root package are installed by default. +Both `install` or `update` support the `--no-dev` option that prevents dev +dependencies from being installed. #### conflict 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, +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. + #### replace Lists packages that are replaced by this package. This allows you to fork a @@ -348,69 +387,134 @@ 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 Autoload mapping for a PHP autoloader. -Currently [`PSR-0`](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md) -autoloading, `classmap` generation and `files` are supported. PSR-0 is the recommended way though -since it offers greater flexibility (no need to regenerate the autoloader when you add -classes). +Currently [`PSR-0`](http://www.php-fig.org/psr/psr-0/) autoloading, +[`PSR-4`](http://www.php-fig.org/psr/psr-4/) autoloading, `classmap` generation and +`files` includes are supported. PSR-4 is the recommended way though since it offers +greater ease of use (no need to regenerate the autoloader when you add classes). + +#### PSR-4 + +Under the `psr-4` key you define a mapping from namespaces to paths, relative to the +package root. When autoloading a class like `Foo\\Bar\\Baz` a namespace prefix +`Foo\\` pointing to a directory `src/` means that the autoloader will look for a +file named `src/Bar/Baz.php` and include it if present. Note that as opposed to +the older PSR-0 style, the prefix (`Foo\\`) is **not** present in the file path. + +Namespace prefixes must end in `\\` to avoid conflicts between similar prefixes. +For example `Foo` would match classes in the `FooBar` namespace so the trailing +backslashes solve the problem: `Foo\\` and `FooBar\\` are distinct. + +The PSR-4 references are all combined, during install/update, into a single +key => value array which may be found in the generated file +`vendor/composer/autoload_psr4.php`. + +Example: + +```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: + +```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: + +```json +{ + "autoload": { + "psr-4": { "": "src/" } + } +} +``` #### PSR-0 Under the `psr-0` key you define a mapping from namespaces to paths, relative to the package root. Note that this also supports the PEAR-style non-namespaced convention. +Please note namespace declarations should end in `\\` to make sure the autoloader +responds exactly. For example `Foo` would match in `FooBar` so the trailing +backslashes solve the problem: `Foo\\` and `FooBar\\` are distinct. + The PSR-0 references are all combined, during install/update, into a single key => value array which may be found in the generated file `vendor/composer/autoload_namespaces.php`. 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 @@ -420,16 +524,18 @@ key => value array which may be found in the generated file classes in all `.php` and `.inc` files in the given directories/files. You can use the classmap generation support to define autoloading for all libraries -that do not follow PSR-0. To configure this you specify all directories or files +that do not follow PSR-0/4. To configure this you specify all directories or files to search for classes. Example: - { - "autoload": { - "classmap": ["src/", "lib/", "Something.php"] - } +```json +{ + "autoload": { + "classmap": ["src/", "lib/", "Something.php"] } +} +``` #### Files @@ -439,11 +545,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 @@ -455,14 +587,20 @@ A list of paths which should get appended to PHP's `include_path`. Example: - { - "include-path": ["lib/"] - } +```json +{ + "include-path": ["lib/"] +} +``` Optional. ### target-dir +> **DEPRECATED**: This is only present to support legacy PSR-0 style autoloading, +> and all new code should preferably use PSR-4 without target-dir and projects +> using PSR-0 with PHP namespaces are encouraged to migrate to PSR-4 instead. + Defines the installation target. In case the package root is below the namespace declaration you cannot @@ -477,12 +615,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. @@ -501,6 +641,15 @@ a given package can be done in `require` or `require-dev` (see Available options (in order of stability) are `dev`, `alpha`, `beta`, `RC`, and `stable`. +### prefer-stable (root-only) + +When this is enabled, Composer will prefer more stable packages over unstable +ones when finding compatible stable packages is possible. If you require a +dev version or only alphas are available for a package, those will still be +selected granted that the minimum-stability allows for it. + +Use `"prefer-stable": true` to enable. + ### repositories (root-only) Custom package repositories to use. @@ -531,47 +680,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. @@ -584,20 +735,35 @@ A set of configuration options. It is only used for projects. The following options are supported: -* **vendor-dir:** Defaults to `vendor`. You can install dependencies into a - different directory if you want to. -* **bin-dir:** Defaults to `vendor/bin`. If a project includes binaries, they - will be symlinked into this directory. * **process-timeout:** Defaults to `300`. The duration processes like git clones can run before Composer assumes they died out. You may need to make this higher if you have a slow connection or huge vendors. -* **github-protocols:** Defaults to `["git", "https", "http"]`. A list of - protocols to use for github.com clones, in priority order. Use this if you are - behind a proxy or have somehow bad performances with the git protocol. +* **use-include-path:** Defaults to `false`. If true, the Composer autoloader + will also look for classes in the PHP include path. +* **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. +* **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 + 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-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. `$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 `C:\Users\\AppData\Local\Composer` on Windows, `$XDG_CACHE_HOME/composer` on unix systems that follow the XDG Base Directory Specifications, and `$home/cache` on other unix systems. Stores all the caches @@ -605,24 +771,49 @@ The following options are supported: * **cache-files-dir:** Defaults to `$cache-dir/files`. Stores the zip archives of packages. * **cache-repo-dir:** Defaults to `$cache-dir/repo`. Stores repository metadata - for the `composer` type and the VCS repos of type `svn`, `github` and `*bitbucket`. + for the `composer` type and the VCS repos of type `svn`, `github` and `bitbucket`. * **cache-vcs-dir:** Defaults to `$cache-dir/vcs`. Stores VCS clones for loading VCS repository metadata for the `git`/`hg` types and to speed up installs. * **cache-files-ttl:** Defaults to `15552000` (6 months). Composer caches all dist (zip, tar, ..) packages that it downloads. Those are purged after six months of being unused by default. This option allows you to tweak this duration (in seconds) or disable it completely by setting it to 0. +* **cache-files-maxsize:** Defaults to `300MiB`. Composer caches all + dist (zip, tar, ..) packages that it downloads. When the garbage collection + is periodically ran, this is the maximum size the cache will be able to use. + Older (less used) files will be removed first until the cache fits. +* **prepend-autoloader:** Defaults to `true`. If false, the composer autoloader + will not be prepended to existing autoloaders. This is sometimes required to fix + interoperability issues with other autoloaders. +* **autoloader-suffix:** Defaults to `null`. String to be used as a suffix for + the generated Composer autoloader. When null a random one will be generated. +* **optimize-autoloader** Defaults to `false`. Always optimize when dumping + the autoloader. +* **github-domains:** Defaults to `["github.com"]`. A list of domains to use in + github mode. This is used for GitHub Enterprise setups. * **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. +* **discard-changes:** Defaults to `false` and can be any of `true`, `false` or + `"stash"`. This option allows you to set the default style of handling dirty + updates when in non-interactive mode. `true` will always discard changes in + vendors, while `"stash"` will try to stash and reapply. Use this for CI + servers or deploy scripts if you tend to have modified vendors. 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) @@ -638,7 +829,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. @@ -647,7 +840,34 @@ Optional. A set of files that should be treated as binaries and symlinked into the `bin-dir` (from config). -See [Vendor Bins](articles/vendor-bins.md) for more details. +See [Vendor Binaries](articles/vendor-binaries.md) for more details. + +Optional. + +### archive + +A set of options for creating package archives. + +The following options are supported: + +* **exclude:** Allows configuring a list of patterns for excluded paths. The + pattern syntax matches .gitignore files. A leading exclamation mark (!) will + result in any matching files to be included even if a previous pattern + excluded them. A leading slash will only match at the beginning of the project + relative path. An asterisk will not expand to a directory separator. + +Example: + +```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`. Optional. diff --git a/doc/05-repositories.md b/doc/05-repositories.md index 17eaf039d..975b473e5 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,38 +88,44 @@ 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). -#### notify_batch +#### notify-batch -The `notify_batch` field allows you to specify an URL that will be called +The `notify-batch` field allows you to specify an URL that will be called every time a user installs a package. The URL can be either an absolute path (that will use the same domain as the repository) or a fully qualified URL. 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. @@ -126,25 +134,27 @@ This field is optional. #### includes -For large repositories it is possible to split the `packages.json` into +For larger repositories it is possible to split the `packages.json` into multiple files. The `includes` field allows you to reference these additional 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. @@ -152,6 +162,56 @@ hash changed. This field is optional. You probably don't need it for your own custom repository. +#### provider-includes and providers-url + +For very large repositories like packagist.org using the so-called provider +files is the preferred method. The `provider-includes` field allows you to +list a set of files that list package names provided by this repository. The +hash should be a sha256 of the files in this case. + +The `providers-url` describes how provider files are found on the server. It +is an absolute path from the repository root. + +An example: + +```json +{ + "provider-includes": { + "providers-a.json": { + "sha256": "f5b4bc0b354108ef08614e569c1ed01a2782e67641744864a74e788982886f4c" + }, + "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: + +```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 +`%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 +repository. + #### stream options The `packages.json` file is loaded using a PHP stream. You can set extra options @@ -179,41 +239,52 @@ 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": "http://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. -It is possible to inline-alias a package constraint so that it matches a -constraint that it otherwise would not. For more information [see the -aliases article](articles/aliases.md). +Note that you should not rename the package unless you really intend to fork +it in the long term, and completely move away from the original package. +Composer will correctly pick your package over the original one since the +custom repository has priority over packagist. If you want to rename the +package, you should do so in the default (often master) branch and not in a +feature branch, since the package name is taken from the default branch. + +If other dependencies rely on the package you forked, it is possible to +inline-alias it so that it matches a constraint that it otherwise would not. +For more information [see the aliases article](articles/aliases.md). #### Using private repositories 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. @@ -239,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 @@ -247,21 +323,59 @@ 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`. +If the package is in a sub-directory, e.g. `/trunk/foo/bar/composer.json` and +`/tags/1.0/foo/bar/composer.json`, then you can make composer access it by +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` @@ -270,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`. @@ -324,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 @@ -355,35 +473,45 @@ 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. +> **Note**: This repository type has a few limitations and should be avoided +> whenever possible: +> +> - Composer will not update the package unless you change the `version` field. +> - Composer will not update the commit references, so if you use `master` as +> reference you will have to delete the package to force an update, and will +> have to deal with an unstable lock file. + ## Hosting your own While you will probably want to put your packages on packagist most of the time, @@ -397,8 +525,8 @@ 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. -When hosting your own package repository it is recommended to use a `composer` -one. This is type that is native to composer and yields the best performance. +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. @@ -432,18 +560,58 @@ Check [the satis GitHub repository](https://github.com/composer/satis) and the [Satis article](articles/handling-private-packages-with-satis.md) for more information. +### Artifact + +There are some cases, when there is no ability to have one of the previously +mentioned repository types online, even the VCS one. Typical example could be +cross-organisation library exchange through built artifacts. Of course, most +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: + +```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: + +```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 +and you run `update`, that version will be imported as well and Composer will +update to the latest version. + ## Disabling Packagist 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 efd7cc6cb..2b436322f 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,13 +28,15 @@ 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 @@ -59,26 +61,29 @@ is a dependency of your local project. For this reason, you can alias packages in your `require` and `require-dev` fields. Let's say you found a bug in the `monolog/monolog` package. You cloned -Monolog on GitHub and fixed the issue in a branch named `bugfix`. Now you want -to install that version of monolog in your local project. +[Monolog](https://github.com/Seldaek/monolog) on GitHub and fixed the issue in +a branch named `bugfix`. Now you want to install that version of monolog in your +local project. You are using `symfony/monolog-bundle` which requires `monolog/monolog` version `1.*`. So you need your `dev-bugfix` to match that constraint. 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 3ac4eeb6a..98a9a2212 100644 --- a/doc/articles/custom-installers.md +++ b/doc/articles/custom-installers.md @@ -29,71 +29,102 @@ An example use-case would be: > phpDocumentor features Templates that need to be installed outside of the > default /vendor folder structure. As such they have chosen to adopt the -> `phpdocumentor-template` [type][1] and create a Custom Installer to send -> these templates to the correct folder. +> `phpdocumentor-template` [type][1] and create a plugin providing the Custom +> Installer to send these templates to the correct folder. An example composer.json of such a template package would be: - { - "name": "phpdocumentor/template-responsive", - "type": "phpdocumentor-template", - "require": { - "phpdocumentor/template-installer": "*" - } +```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 -> the installer package. +> the plugin package. ## Creating an Installer A Custom Installer is defined as a class that implements the -[`Composer\Installer\InstallerInterface`][3] and is contained in a Composer -package that has the [type][1] `composer-installer`. +[`Composer\Installer\InstallerInterface`][3] and is usually distributed in a +Composer Plugin. -A basic Installer would thus compose of two files: +A basic Installer Plugin would thus compose of three files: 1. the package file: composer.json -2. The Installer class, i.e.: `Composer\Installer\MyInstaller.php` - -> **NOTE**: _The namespace does not need to be `Composer\Installer`, it must -> only implement the right interface._ +2. The Plugin class, e.g.: `My\Project\Composer\Plugin.php`, containing a class that implements `Composer\Plugin\PluginInterface`. +3. The Installer class, e.g.: `My\Project\Composer\Installer.php`, containing a class that implements `Composer\Installer\InstallerInterface`. ### composer.json The package file is the same as any other package file but with the following requirements: -1. the [type][1] attribute must be `composer-installer`. +1. the [type][1] attribute must be `composer-plugin`. 2. the [extra][2] attribute must contain an element `class` defining the - class name of the installer (including namespace). If a package contains - multiple installers this can be array of class names. + class name of the plugin (including namespace). If a package contains + multiple plugins this can be array of class names. Example: - { - "name": "phpdocumentor/template-installer", - "type": "composer-installer", - "license": "MIT", - "autoload": { - "psr-0": {"phpDocumentor\\Composer": "src/"} - }, - "extra": { - "class": "phpDocumentor\\Composer\\TemplateInstaller" - } +```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 Custom Installer class +### The Plugin class -The class that executes the custom installation should implement the -[`Composer\Installer\InstallerInterface`][3] (or extend another installer that -implements that interface). +The class defining the Composer plugin must implement the +[`Composer\Plugin\PluginInterface`][3]. It can then register the Custom +Installer in its `activate()` method. The class may be placed in any location and have any name, as long as it is autoloadable and matches the `extra.class` element in the package definition. -It will also define the [type][1] string as it will be recognized by packages -that will use this installer in the `supports()` method. + +Example: + +```php +getInstallationManager()->addInstaller($installer); + } +} +``` + +### The Custom Installer class + +The class that executes the custom installation should implement the +[`Composer\Installer\InstallerInterface`][4] (or extend another installer that +implements that interface). It defines the [type][1] string as it will be +recognized by packages that will use this installer in the `supports()` method. > **NOTE**: _choose your [type][1] name carefully, it is recommended to follow > the format: `vendor-type`_. For example: `phpdocumentor-template`. @@ -115,41 +146,45 @@ 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`][4] class to strip a prefix +[`Composer\Installer\LibraryInstaller`][5] class to strip a prefix (`phpdocumentor/template-`) and use the remaining part to assemble a completely different installation path. @@ -158,5 +193,6 @@ different installation path. [1]: ../04-schema.md#type [2]: ../04-schema.md#extra -[3]: https://github.com/composer/composer/blob/master/src/Composer/Installer/InstallerInterface.php -[4]: https://github.com/composer/composer/blob/master/src/Composer/Installer/LibraryInstaller.php \ No newline at end of file +[3]: https://github.com/composer/composer/blob/master/src/Composer/Plugin/PluginInterface.php +[4]: https://github.com/composer/composer/blob/master/src/Composer/Installer/InstallerInterface.php +[5]: https://github.com/composer/composer/blob/master/src/Composer/Installer/LibraryInstaller.php diff --git a/doc/articles/handling-private-packages-with-satis.md b/doc/articles/handling-private-packages-with-satis.md index e7fa23509..0ee0adbca 100644 --- a/doc/articles/handling-private-packages-with-satis.md +++ b/doc/articles/handling-private-packages-with-satis.md @@ -2,55 +2,69 @@ tagline: Host your own composer repository --> -# Handling private packages with Satis +# Handling private packages with Satis or Toran Proxy -Satis 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`. +# 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 For example let's assume you have a few packages you want to reuse across your company but don't really want to open-source. You would first define a Satis -configuration file, which is basically a stripped-down version of a -`composer.json` file. It contains a few repositories, and then you use the require -key to say which packages it should dump in the static repository it creates, or -use require-all to select all of them. +configuration: a json file with an arbitrary name that lists your curated +[repositories](../05-repositories.md). Here is an example configuration, you see that it holds a few VCS repositories, but those could be any types of [repositories](../05-repositories.md). Then it uses `"require-all": true` which selects all versions of all packages in the repositories you defined. - { - "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 - } +The default file Satis looks for is `satis.json` in the root of the repository. + +```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 `. For example `php bin/satis build config.json web/` would read the `config.json` @@ -67,7 +81,8 @@ to ssh key authentication instead of prompting for a password. This is also a good trick for continuous integration servers. Set up a virtual-host that points to that `web/` directory, let's say it is -`packages.example.org`. +`packages.example.org`. Alternatively, with PHP >= 5.4.0, you can use the built-in +CLI server `php -S localhost:port -t satis-output-dir/` for a temporary solution. ## Usage @@ -77,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 @@ -94,34 +111,101 @@ 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": { - "cert_file": "/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. + +### Downloads + +When GitHub or BitBucket repositories are mirrored on your local satis, the build process will include +the location of the downloads these platforms make available. This means that the repository and your setup depend +on the availability of these services. + +At the same time, this implies that all code which is hosted somewhere else (on another service or for example in +Subversion) will not have downloads available and thus installations usually take a lot longer. + +To enable your satis installation to create downloads for all (Git, Mercurial and Subversion) your packages, add the +following to your `satis.json`: + +```json +{ + "archive": { + "directory": "dist", + "format": "tar", + "prefix-url": "https://amazing.cdn.example.org", + "skip-dev": true } +} +``` + +#### Options explained + + * `directory`: the location of the dist files (inside the `output-dir`) + * `format`: optional, `zip` (default) or `tar` + * `prefix-url`: optional, location of the downloads, homepage (from `satis.json`) followed by `directory` by default + * `skip-dev`: optional, `false` by default, when enabled (`true`) satis will not create downloads for branches + +Once enabled, all downloads (include those from GitHub and BitBucket) will be replaced with a _local_ version. + +#### prefix-url + +Prefixing the URL with another host is especially helpful if the downloads end up in a private Amazon S3 +bucket or on a CDN host. A CDN would drastically improve download times and therefore package installation. + +Example: A `prefix-url` of `http://my-bucket.s3.amazonaws.com` (and `directory` set to `dist`) creates download URLs +which look like the following: `http://my-bucket.s3.amazonaws.com/dist/vendor-package-version-ref.zip`. + + +### Resolving dependencies + +It is possible to make satis automatically resolve and add all dependencies for your projects. This can be used +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-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/plugins.md b/doc/articles/plugins.md new file mode 100644 index 000000000..65884fd18 --- /dev/null +++ b/doc/articles/plugins.md @@ -0,0 +1,160 @@ + + +# Setting up and using plugins + +## Synopsis + +You may wish to alter or expand Composer's functionality with your own. For +example if your environment poses special requirements on the behaviour of +Composer which do not apply to the majority of its users or if you wish to +accomplish something with composer in a way that is not desired by most users. + +In these cases you could consider creating a plugin to handle your +specific logic. + +## Creating a Plugin + +A plugin is a regular composer package which ships its code as part of the +package and may also depend on further packages. + +### Plugin Package + +The package file is the same as any other package file but with the following +requirements: + +1. the [type][1] attribute must be `composer-plugin`. +2. the [extra][2] attribute must contain an element `class` defining the + class name of the plugin (including namespace). If a package contains + multiple plugins this can be array of class names. + +Additionally you must require the special package called `composer-plugin-api` +to define which composer API versions your plugin is compatible with. The +current composer plugin API version is 1.0.0. + +For example + +```json +{ + "name": "my/plugin-package", + "type": "composer-plugin", + "require": { + "composer-plugin-api": "1.0.0" + } +} +``` + +### Plugin Class + +Every plugin has to supply a class which implements the +[`Composer\Plugin\PluginInterface`][3]. The `activate()` method of the plugin +is called after the plugin is loaded and receives an instance of +[`Composer\Composer`][4] as well as an instance of +[`Composer\IO\IOInterface`][5]. Using these two objects all configuration can +be read and all internal objects and state can be manipulated as desired. + +Example: + +```php +getInstallationManager()->addInstaller($installer); + } +} +``` + +## Event Handler + +Furthermore plugins may implement the +[`Composer\EventDispatcher\EventSubscriberInterface`][6] in order to have its +event handlers automatically registered with the `EventDispatcher` when the +plugin is loaded. + +The events available for plugins are: + +* **COMMAND**, is called at the beginning of all commands that load plugins. + It provides you with access to the input and output objects of the program. +* **PRE_FILE_DOWNLOAD**, is triggered before files are downloaded and allows + you to manipulate the `RemoteFilesystem` object prior to downloading files + based on the URL to be downloaded. + +> A plugin can also subscribe to [script events][7]. + +Example: + +```php +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); + + 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 + +Plugin packages are automatically loaded as soon as they are installed and will +be loaded when composer starts up if they are found in the current project's +list of installed packages. Additionally all plugin packages installed in the +`COMPOSER_HOME` directory using the composer global command are loaded before +local project plugins are loaded. + +> You may pass the `--no-plugins` option to composer commands to disable all +> installed commands. This may be particularly helpful if any of the plugins +> causes errors and you wish to update or uninstall it. + +[1]: ../04-schema.md#type +[2]: ../04-schema.md#extra +[3]: https://github.com/composer/composer/blob/master/src/Composer/Plugin/PluginInterface.php +[4]: https://github.com/composer/composer/blob/master/src/Composer/Composer.php +[5]: https://github.com/composer/composer/blob/master/src/Composer/IO/IOInterface.php +[6]: https://github.com/composer/composer/blob/master/src/Composer/EventDispatcher/EventSubscriberInterface.php +[7]: ./scripts.md#event-names diff --git a/doc/articles/scripts.md b/doc/articles/scripts.md index 600235938..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 @@ -24,13 +24,33 @@ Composer fires the following named events during its execution process: - **post-install-cmd**: occurs after the `install` command is executed. - **pre-update-cmd**: occurs before the `update` command is executed. - **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. - **post-package-update**: occurs after a package is updated. - **pre-package-uninstall**: occurs before a package has been uninstalled. - **post-package-uninstall**: occurs after a package has been uninstalled. - +- **pre-autoload-dump**: occurs before the autoloader is dumped, either + during `install`/`update`, or via the `dump-autoload` command. +- **post-autoload-dump**: occurs after the autoloader is dumped, either + during `install`/`update`, or via the `dump-autoload` command. +- **post-root-package-install**: occurs after the root package has been + 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. ## Defining scripts @@ -43,54 +63,61 @@ 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 + } - class MyClass + public static function postPackageInstall(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 - } + $installedPackage = $event->getOperation()->getPackage(); + // do stuff } + public static function warmCache(Event $event) + { + // 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 PHP callback. This `Event` object has getters for other contextual objects: @@ -99,3 +126,38 @@ PHP callback. This `Event` object has getters for other contextual objects: - `getName()`: returns the name of the event being fired as a string - `getIO()`: returns the current input/output stream which implements `Composer\IO\IOInterface` for writing to the console + +## Running scripts manually + +If you would like to run the scripts for an event manually, the syntax is: + +```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" + } +} +``` + +> **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 99cbe9bce..922526a52 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -7,17 +7,22 @@ This is a list of common pitfalls on using Composer, and how to avoid them. ## General -1. When facing any kind of problems using Composer, be sure to **work with the +1. Before asking anyone, run [`composer diagnose`](../03-cli.md#diagnose) to check + for common problems. If it all checks out, proceed to the next steps. + +2. When facing any kind of problems using Composer, be sure to **work with the latest version**. See [self-update](../03-cli.md#self-update) for details. -2. Make sure you have no problems with your setup by running the installer's - checks via `curl -s https://getcomposer.org/installer | php -- --check`. +3. Make sure you have no problems with your setup by running the installer's + checks via `curl -sS https://getcomposer.org/installer | php -- --check`. -3. Ensure you're **installing vendors straight from your `composer.json`** via +4. Ensure you're **installing vendors straight from your `composer.json`** via `rm -rf vendor && composer update -v` when troubleshooting, excluding any 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 @@ -35,11 +40,50 @@ 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. + +2. If the package tested is a dependency of one of its dependencies (cyclic + dependency), the problem might be that composer is not able to detect the version + of the package properly. If it is a git clone it is generally alright and Composer + will detect the version of the current branch, but travis does shallow clones so + that process can fail when testing pull requests and feature branches in general. + The best solution is to define the version you are on via an environment variable + called COMPOSER_ROOT_VERSION. You set it to `dev-master` for example to define + the root package's version as `dev-master`. + Use: `before_script: COMPOSER_ROOT_VERSION=dev-master composer install` to export + the variable for the call to composer. + +## Need to override a package version + +Let say your project depends on package A which in turn depends on a specific +version of package B (say 0.1) and you need a different version of that +package - version 0.11. + +You can fix this by aliasing version 0.11 to 0.1: + +composer.json: + +```json +{ + "require": { + "A": "0.2", + "B": "0.11 as 0.1" + } +} +``` + +See [aliases](aliases.md) for more information. + ## Memory limit errors 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. @@ -49,15 +93,66 @@ 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```. +3. Check if it contains any path to non-existent file, if it's the case, just remove them. + +## 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. + +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-bins.md b/doc/articles/vendor-binaries.md similarity index 50% rename from doc/articles/vendor-bins.md rename to doc/articles/vendor-binaries.md index d9b87214d..b65b6bcf4 100644 --- a/doc/articles/vendor-bins.md +++ b/doc/articles/vendor-binaries.md @@ -2,66 +2,71 @@ tagline: Expose command-line scripts from packages --> -# bin and vendor/bin +# Vendor binaries and the `vendor/bin` directory -## What is a bin? +## What is a vendor binary? Any command line script that a Composer package would like to pass along -to a user who installs the package should be listed as a bin. +to a user who installs the package should be listed as a vendor binary. If a package contains other scripts that are not needed by the package users (like build or compile scripts) that code should not be listed -as a bin. +as a vendor binary. ## How is it defined? It is defined by adding the `bin` key to a project's `composer.json`. -It is specified as an array of files so multiple bins can be added +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 bin in composer.json do? +## What does defining a vendor binary in composer.json do? -It instructs Composer to install the package's bins to `vendor/bin` +It instructs Composer to install the package's binaries to `vendor/bin` for any project that **depends** on that project. This is a convenient way to expose useful scripts that would otherwise be hidden deep in the `vendor/` directory. -## What happens when Composer is run on a composer.json that defines bins? +## What happens when Composer is run on a composer.json that defines vendor binaries? -For the bins that a package defines directly, nothing happens. +For the binaries that a package defines directly, nothing happens. -## What happens when Composer is run on a composer.json that has dependencies with bins listed? +## What happens when Composer is run on a composer.json that has dependencies with vendor binaries listed? -Composer looks for the bins defined in all of the dependencies. A -symlink is created from each dependency's bins to `vendor/bin`. +Composer looks for the binaries defined in all of the dependencies. A +symlink is created from each dependency's binaries to `vendor/bin`. -Say package `my-vendor/project-a` has bins setup like this: +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", - "requires": { - "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`. @@ -75,32 +80,36 @@ this is accomplished by creating a symlink. Packages managed entirely by Composer do not *need* to contain any `.bat` files for Windows compatibility. Composer handles installation -of bins in a special way when run in a Windows environment: +of binaries in a special way when run in a Windows environment: - * A `.bat` files is generated automatically to reference the bin - * A Unix-style proxy file with the same name as the bin is generated + * A `.bat` file is generated automatically to reference the binary + * A Unix-style proxy file with the same name as the binary is generated automatically (useful for Cygwin or Git Bash) Packages that need to support workflows that may not include Composer are welcome to maintain custom `.bat` files. In this case, the package -should **not** list the `.bat` file as a bin as it is not needed. +should **not** list the `.bat` file as a binary as it is not needed. -## Can vendor bins be installed somewhere other than vendor/bin? +## Can vendor binaries be installed somewhere other than vendor/bin? -Yes, there are two ways that an alternate vendor bin location can be specified. +Yes, there are two ways an alternate vendor binary location can be specified: - * Setting the `bin-dir` configuration setting in `composer.json` - * Setting the environment variable `COMPOSER_BIN_DIR` + 1. Setting the `bin-dir` configuration setting in `composer.json` + 1. Setting the environment variable `COMPOSER_BIN_DIR` 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 bins being installed in `scripts/` instead of +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 e64c00a2c..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": "*" - } +```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 8d4e63af2..8e50f7264 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 @@ -16,11 +16,16 @@ problems: submodules. This is problematic because they are not real submodules, and you will run into issues. -If you really feel like you must do this, you have two options: +If you really feel like you must do this, you have a few options: -- Limit yourself to installing tagged releases (no dev versions), so that you - only get zipped installs, and avoid problems with the git "submodules". -- 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` - but this means you will have to delete those dependencies from disk before - running composer update. \ No newline at end of file +1. Limit yourself to installing tagged releases (no dev versions), so that you + only get zipped installs, and avoid problems with the git "submodules". +2. Use --prefer-dist or set `preferred-install` to `dist` in your + [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` + 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. + This approach does not require that you delete dependencies from disk prior to + running a composer update. 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/res/composer-schema.json b/res/composer-schema.json index b307264cf..9b1b31d42 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -9,11 +9,11 @@ "required": true }, "type": { - "description": "Package type, either 'library' for common packages, 'composer-installer' for custom installers, 'metapackage' for empty packages, or a custom type ([a-z0-9-]+) defined by whatever project this package applies to.", + "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.", "type": "string" }, "target-dir": { - "description": "Forces the package to be installed into the given subdirectory path. This is used for autoloading PSR-0 packages that do not contain their full path. Use forward slashes for cross-platform compatibility.", + "description": "DEPRECATED: Forces the package to be installed into the given subdirectory path. This is used for autoloading PSR-0 packages that do not contain their full path. Use forward slashes for cross-platform compatibility.", "type": "string" }, "description": { @@ -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"], @@ -105,9 +105,46 @@ "additionalProperties": true }, "config": { - "type": ["object"], + "type": "object", "description": "Composer options.", "properties": { + "process-timeout": { + "type": "integer", + "description": "The timeout in seconds for process executions, defaults to 300 (5mins)." + }, + "use-include-path": { + "type": "boolean", + "description": "If true, the Composer autoloader will also look for classes in the PHP include path." + }, + "preferred-install": { + "type": "string", + "description": "The install method Composer will prefer to use, defaults to auto and can be any of source, dist or auto." + }, + "notify-on-install": { + "type": "boolean", + "description": "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, defaults to true." + }, + "github-protocols": { + "type": "array", + "description": "A list of protocols to use for github.com clones, in priority order, defaults to [\"git\", \"https\", \"http\"].", + "items": { + "type": "string" + } + }, + "github-oauth": { + "type": "object", + "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\"." @@ -116,17 +153,53 @@ "type": "string", "description": "The location where all binaries are linked, defaults to \"vendor/bin\"." }, - "process-timeout": { + "cache-dir": { + "type": "string", + "description": "The location where all caches are located, defaults to \"~/.composer/cache\" on *nix and \"%LOCALAPPDATA%\\Composer\" on windows." + }, + "cache-files-dir": { + "type": "string", + "description": "The location where files (zip downloads) are cached, defaults to \"{$cache-dir}/files\"." + }, + "cache-repo-dir": { + "type": "string", + "description": "The location where repo (git/hg repo clones) are cached, defaults to \"{$cache-dir}/repo\"." + }, + "cache-vcs-dir": { + "type": "string", + "description": "The location where vcs infos (git clones, github api calls, etc. when reading vcs repos) are cached, defaults to \"{$cache-dir}/vcs\"." + }, + "cache-ttl": { "type": "integer", - "description": "The timeout in seconds for process executions, defaults to 300 (5mins)." + "description": "The default cache time-to-live, defaults to 15552000 (6 months)." }, - "notify-on-install": { + "cache-files-ttl": { + "type": "integer", + "description": "The cache time-to-live for files, defaults to the value of cache-ttl." + }, + "cache-files-maxsize": { + "type": ["string", "integer"], + "description": "The cache max size for the files cache, defaults to \"300MiB\"." + }, + "discard-changes": { + "type": ["string", "boolean"], + "description": "The default style of handling dirty updates, defaults to false and can be any of true, false or \"stash\"." + }, + "autoloader-suffix": { + "type": "string", + "description": "Optional string to be used as a suffix for the generated Composer autoloader. When null a random one will be generated." + }, + "optimize-autoloader": { "type": "boolean", - "description": "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, defaults to true." + "description": "Always optimize when dumping the autoloader." }, - "github-protocols": { + "prepend-autoloader": { + "type": "boolean", + "description": "If false, the composer autoloader will not be prepended to existing autoloaders, defaults to true." + }, + "github-domains": { "type": "array", - "description": "A list of protocols to use for github.com clones, in priority order, defaults to [\"git\", \"https\", \"http\"].", + "description": "A list of domains to use in github mode. This is used for GitHub Enterprise setups, defaults to [\"github.com\"].", "items": { "type": "string" } @@ -135,7 +208,7 @@ }, "extra": { "type": ["object", "array"], - "description": "Arbitrary extra data that can be used by custom installers, for example, package of type composer-installer must have a 'class' key defining the installer class name.", + "description": "Arbitrary extra data that can be used by plugins, for example, package of type composer-plugin may have a 'class' key defining an installer class name.", "additionalProperties": true }, "autoload": { @@ -147,6 +220,35 @@ "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." + } + } + }, + "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." @@ -157,6 +259,16 @@ } } }, + "archive": { + "type": ["object"], + "description": "Options for creating package archives for distribution.", + "properties": { + "exclude": { + "type": "array", + "description": "A list of patterns for paths to exclude or include if prefixed with an exclamation mark." + } + } + }, "repositories": { "type": ["object", "array"], "description": "A set of additional repositories where packages can be found.", @@ -166,6 +278,10 @@ "type": ["string"], "description": "The minimum stability the packages must have to be install-able. Possible values are: dev, alpha, beta, RC, stable." }, + "prefer-stable": { + "type": ["boolean"], + "description": "If set to true, stable packages will be prefered to dev packages when possible, even if the minimum-stability allows unstable packages." + }, "bin": { "type": ["array"], "description": "A set of files that should be treated as binaries and symlinked into bin-dir (from config).", @@ -186,43 +302,67 @@ "properties": { "pre-install-cmd": { "type": ["array", "string"], - "description": "Occurs before the install command is executed, contains one or more Class::method callables." + "description": "Occurs before the install command is executed, contains one or more Class::method callables or shell commands." }, "post-install-cmd": { "type": ["array", "string"], - "description": "Occurs after the install command is executed, contains one or more Class::method callables." + "description": "Occurs after the install command is executed, contains one or more Class::method callables or shell commands." }, "pre-update-cmd": { "type": ["array", "string"], - "description": "Occurs before the update command is executed, contains one or more Class::method callables." + "description": "Occurs before the update command is executed, contains one or more Class::method callables or shell commands." }, "post-update-cmd": { "type": ["array", "string"], - "description": "Occurs after the update command is executed, contains one or more Class::method callables." + "description": "Occurs after the update command is executed, contains one or more Class::method callables or shell commands." + }, + "pre-status-cmd": { + "type": ["array", "string"], + "description": "Occurs before the status command is executed, contains one or more Class::method callables or shell commands." + }, + "post-status-cmd": { + "type": ["array", "string"], + "description": "Occurs after the status command is executed, contains one or more Class::method callables or shell commands." }, "pre-package-install": { "type": ["array", "string"], - "description": "Occurs before a package is installed, contains one or more Class::method callables." + "description": "Occurs before a package is installed, contains one or more Class::method callables or shell commands." }, "post-package-install": { "type": ["array", "string"], - "description": "Occurs after a package is installed, contains one or more Class::method callables." + "description": "Occurs after a package is installed, contains one or more Class::method callables or shell commands." }, "pre-package-update": { "type": ["array", "string"], - "description": "Occurs before a package is updated, contains one or more Class::method callables." + "description": "Occurs before a package is updated, contains one or more Class::method callables or shell commands." }, "post-package-update": { "type": ["array", "string"], - "description": "Occurs after a package is updated, contains one or more Class::method callables." + "description": "Occurs after a package is updated, contains one or more Class::method callables or shell commands." }, "pre-package-uninstall": { "type": ["array", "string"], - "description": "Occurs before a package has been uninstalled, contains one or more Class::method callables." + "description": "Occurs before a package has been uninstalled, contains one or more Class::method callables or shell commands." }, "post-package-uninstall": { "type": ["array", "string"], - "description": "Occurs after a package has been uninstalled, contains one or more Class::method callables." + "description": "Occurs after a package has been uninstalled, contains one or more Class::method callables or shell commands." + }, + "pre-autoload-dump": { + "type": ["array", "string"], + "description": "Occurs before the autoloader is dumped, contains one or more Class::method callables or shell commands." + }, + "post-autoload-dump": { + "type": ["array", "string"], + "description": "Occurs after the autoloader is dumped, contains one or more Class::method callables or shell commands." + }, + "post-root-package-install": { + "type": ["array", "string"], + "description": "Occurs after the root-package is installed, contains one or more Class::method callables or shell commands." + }, + "post-create-project-cmd": { + "type": ["array", "string"], + "description": "Occurs after the create-project command is executed, contains one or more Class::method callables or shell commands." } } }, diff --git a/res/spdx-identifier.json b/res/spdx-identifier.json index 104d41a68..b90531711 100644 --- a/res/spdx-identifier.json +++ b/res/spdx-identifier.json @@ -1,34 +1,44 @@ [ - "AFL-1.1", "AFL-1.2", "AFL-2.0", "AFL-2.1", "AFL-3.0", "APL-1.0", + "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", - "BSL-1.0", "BSD-2-Clause", "BSD-2-Clause-NetBSD", "BSD-2-Clause-FreeBSD", - "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-GPL-Compatible", "CNRI-Python", "CDDL-1.0", "CDDL-1.1", - "CPAL-1.0", "CPL-1.0", "CATOSL-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", + "APSL-1.1", "APSL-1.2", "APSL-2.0", "Artistic-1.0", "Artistic-1.0-cl8", + "Artistic-1.0-Perl", "Artistic-2.0", "AAL", "BitTorrent-1.0", + "BitTorrent-1.1", "BSL-1.0", "BSD-2-Clause", "BSD-2-Clause-FreeBSD", + "BSD-2-Clause-NetBSD", "BSD-3-Clause", "BSD-3-Clause-Clear", "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", + "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-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", "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", "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", "IPA", "ISC", "LPPL-1.0", "LPPL-1.1", "LPPL-1.2", - "LPPL-1.3c", "Libpng", "LPL-1.0", "LPL-1.02", "MS-PL", "MS-RL", "MirOS", - "MIT", "Motosoto", "MPL-1.0", "MPL-1.1", "MPL-2.0", "Multics", "NASA-1.3", - "Naumen", "NGPL", "Nokia", "NPOSL-3.0", "NTP", "OCLC-2.0", "ODbL-1.0", - "PDDL-1.0", "OGTSL", "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", - "OFL-1.0", "OFL-1.1", "SimPL-2.0", "Sleepycat", "SugarCRM-1.1.3", "SPL-1.0", - "Watcom-1.0", "NCSA", "VSL-1.0", "W3C", "WXwindows", "Xnet", "XFree86-1.1", - "YPL-1.0", "YPL-1.1", "Zimbra-1.3", "Zlib", "ZPL-1.1", "ZPL-2.0", "ZPL-2.1" + "CUA-OPL-1.0", "D-FSL-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-1.0", + "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", "IBM-pibs", "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", + "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.1", + "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", "SISSL-1.2", "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", + "Unlicense" ] \ No newline at end of file diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index 0691434c3..45c105ef9 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -13,11 +13,14 @@ 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\RepositoryInterface; +use Composer\Repository\InstalledRepositoryInterface; use Composer\Util\Filesystem; +use Composer\Script\ScriptEvents; /** * @author Igor Wiedler @@ -25,57 +28,109 @@ use Composer\Util\Filesystem; */ class AutoloadGenerator { - public function dump(Config $config, RepositoryInterface $localRepo, PackageInterface $mainPackage, InstallationManager $installationManager, $targetDir, $scanPsr0Packages = false, $suffix = '') + /** + * @var EventDispatcher + */ + private $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->devMode, array(), array( + 'optimize' => (bool) $scanPsr0Packages + )); + $filesystem = new Filesystem(); $filesystem->ensureDirectoryExists($config->get('vendor-dir')); - $vendorPath = strtr(realpath($config->get('vendor-dir')), '\\', '/'); + $basePath = $filesystem->normalizePath(realpath(getcwd())); + $vendorPath = $filesystem->normalizePath(realpath($config->get('vendor-dir'))); + $useGlobalIncludePath = (bool) $config->get('use-include-path'); + $prependAutoloader = $config->get('prepend-autoloader') === false ? 'false' : 'true'; $targetDir = $vendorPath.'/'.$targetDir; $filesystem->ensureDirectoryExists($targetDir); - $relVendorPath = $filesystem->findShortestPath(getcwd(), $vendorPath, true); $vendorPathCode = $filesystem->findShortestPathCode(realpath($targetDir), $vendorPath, true); + $vendorPathCode52 = str_replace('__DIR__', 'dirname(__FILE__)', $vendorPathCode); $vendorPathToTargetDirCode = $filesystem->findShortestPathCode($vendorPath, realpath($targetDir), true); - $appBaseDirCode = $filesystem->findShortestPathCode($vendorPath, getcwd(), true); + $appBaseDirCode = $filesystem->findShortestPathCode($vendorPath, $basePath, true); $appBaseDirCode = str_replace('__DIR__', '$vendorDir', $appBaseDirCode); $namespacesFile = <<buildPackageMap($installationManager, $mainPackage, $localRepo->getPackages()); + $psr4File = <<buildPackageMap($installationManager, $mainPackage, $localRepo->getCanonicalPackages()); $autoloads = $this->parseAutoloads($packageMap, $mainPackage); + // Process the 'psr-0' base directories. foreach ($autoloads['psr-0'] as $namespace => $paths) { $exportedPaths = array(); foreach ($paths as $path) { - $exportedPaths[] = $this->getPathCode($filesystem, $relVendorPath, $vendorPath, $path); + $exportedPaths[] = $this->getPathCode($filesystem, $basePath, $vendorPath, $path); } $exportedPrefix = var_export($namespace, true); $namespacesFile .= " $exportedPrefix => "; - if (count($exportedPaths) > 1) { - $namespacesFile .= "array(".implode(', ', $exportedPaths)."),\n"; - } else { - $namespacesFile .= $exportedPaths[0].",\n"; - } + $namespacesFile .= "array(".implode(', ', $exportedPaths)."),\n"; } $namespacesFile .= ");\n"; + // Process the 'psr-4' base directories. + foreach ($autoloads['psr-4'] as $namespace => $paths) { + $exportedPaths = array(); + foreach ($paths as $path) { + $exportedPaths[] = $this->getPathCode($filesystem, $basePath, $vendorPath, $path); + } + $exportedPrefix = var_export($namespace, true); + $psr4File .= " $exportedPrefix => "; + $psr4File .= "array(".implode(', ', $exportedPaths)."),\n"; + } + $psr4File .= ");\n"; + $classmapFile = <<getAutoload(); if ($mainPackage->getTargetDir() && !empty($mainAutoload['psr-0'])) { - $levels = count(explode('/', trim(strtr($mainPackage->getTargetDir(), '\\', '/'), '/'))); + $levels = count(explode('/', $filesystem->normalizePath($mainPackage->getTargetDir()))); $prefixes = implode(', ', array_map(function ($prefix) { return var_export($prefix, true); }, array_keys($mainAutoload['psr-0']))); - $baseDirFromTargetDirCode = $filesystem->findShortestPathCode($targetDir, getcwd(), true); + $baseDirFromTargetDirCode = $filesystem->findShortestPathCode($targetDir, $basePath, true); $targetDirLoader = << $paths) { - foreach ($paths as $dir) { - $dir = $this->getPath($filesystem, $relVendorPath, $vendorPath, $dir); - $whitelist = sprintf( - '{%s/%s.+(? $path) { - if ('' === $namespace || 0 === strpos($class, $namespace)) { - $path = '/'.$filesystem->findShortestPath(getcwd(), $path, true); + // Scan the PSR-0/4 directories for class files, and add them to the class map + foreach (array('psr-0', 'psr-4') as $psrType) { + foreach ($autoloads[$psrType] as $namespace => $paths) { + foreach ($paths as $dir) { + $dir = $filesystem->normalizePath($filesystem->isAbsolutePath($dir) ? $dir : $basePath.'/'.$dir); + if (!is_dir($dir)) { + continue; + } + $whitelist = sprintf( + '{%s/%s.+(?io, $namespaceFilter) as $class => $path) { if (!isset($classMap[$class])) { - $classMap[$class] = '$baseDir . '.var_export($path, true).",\n"; + $path = $this->getPathCode($filesystem, $basePath, $vendorPath, $path); + $classMap[$class] = $path.",\n"; } } } @@ -141,11 +199,10 @@ EOF; } } - $autoloads['classmap'] = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($autoloads['classmap'])); foreach ($autoloads['classmap'] as $dir) { - foreach (ClassMapGenerator::createMap($dir) as $class => $path) { - $path = '/'.$filesystem->findShortestPath(getcwd(), $path, true); - $classMap[$class] = '$baseDir . '.var_export($path, true).",\n"; + foreach (ClassMapGenerator::createMap($dir, null, $this->io) as $class => $path) { + $path = $this->getPathCode($filesystem, $basePath, $vendorPath, $path); + $classMap[$class] = $path.",\n"; } } @@ -155,24 +212,34 @@ EOF; } $classmapFile .= ");\n"; - $filesCode = ""; - $autoloads['files'] = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($autoloads['files'])); - foreach ($autoloads['files'] as $functionFile) { - $filesCode .= ' require '.$this->getPathCode($filesystem, $relVendorPath, $vendorPath, $functionFile).";\n"; - } - if (!$suffix) { - $suffix = md5(uniqid('', true)); + $suffix = $config->get('autoloader-suffix') ?: md5(uniqid('', true)); } file_put_contents($targetDir.'/autoload_namespaces.php', $namespacesFile); + file_put_contents($targetDir.'/autoload_psr4.php', $psr4File); file_put_contents($targetDir.'/autoload_classmap.php', $classmapFile); - if ($includePathFile = $this->getIncludePathsFile($packageMap, $filesystem, $relVendorPath, $vendorPath, $vendorPathCode, $appBaseDirCode)) { + if ($includePathFile = $this->getIncludePathsFile($packageMap, $filesystem, $basePath, $vendorPath, $vendorPathCode52, $appBaseDirCode)) { file_put_contents($targetDir.'/include_paths.php', $includePathFile); } + if ($includeFilesFile = $this->getIncludeFilesFile($autoloads['files'], $filesystem, $basePath, $vendorPath, $vendorPathCode52, $appBaseDirCode)) { + 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, true, (bool) $includePathFile, $targetDirLoader, $filesCode, $vendorPathCode, $appBaseDirCode, $suffix)); - copy(__DIR__.'/ClassLoader.php', $targetDir.'/ClassLoader.php'); + file_put_contents($targetDir.'/autoload_real.php', $this->getAutoloadRealFile(true, (bool) $includePathFile, $targetDirLoader, (bool) $includeFilesFile, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader)); + + // use stream_copy_to_stream instead of copy + // to work around https://bugs.php.net/bug.php?id=64634 + $sourceLoader = fopen(__DIR__.'/ClassLoader.php', 'r'); + $targetLoader = fopen($targetDir.'/ClassLoader.php', 'w+'); + stream_copy_to_stream($sourceLoader, $targetLoader); + fclose($sourceLoader); + fclose($targetLoader); + unset($sourceLoader, $targetLoader); + + $this->eventDispatcher->dispatchScript(ScriptEvents::POST_AUTOLOAD_DUMP, $this->devMode, array(), array( + 'optimize' => (bool) $scanPsr0Packages, + )); } public function buildPackageMap(InstallationManager $installationManager, PackageInterface $mainPackage, array $packages) @@ -184,6 +251,7 @@ EOF; if ($package instanceof AliasPackage) { continue; } + $this->validatePackage($package); $packageMap[] = array( $package, @@ -194,6 +262,28 @@ EOF; return $packageMap; } + /** + * @param PackageInterface $package + * + * @throws \InvalidArgumentException Throws an exception, if the package has illegal settings. + */ + protected function validatePackage(PackageInterface $package) + { + $autoload = $package->getAutoload(); + if (!empty($autoload['psr-4']) && null !== $package->getTargetDir()) { + $name = $package->getName(); + $package->getTargetDir(); + throw new \InvalidArgumentException("PSR-4 autoloading is incompatible with the target-dir property, remove the target-dir in package '$name'."); + } + if (!empty($autoload['psr-4'])) { + foreach ($autoload['psr-4'] as $namespace => $dirs) { + if ($namespace !== '' && '\\' !== substr($namespace, -1)) { + throw new \InvalidArgumentException("psr-4 namespaces must end with a namespace separator, '$namespace' does not, use '$namespace\\'."); + } + } + } + } + /** * Compiles an ordered list of namespace => path mappings * @@ -209,12 +299,14 @@ EOF; array_unshift($packageMap, $mainPackageMap); $psr0 = $this->parseAutoloadsType($packageMap, 'psr-0', $mainPackage); + $psr4 = $this->parseAutoloadsType($packageMap, 'psr-4', $mainPackage); $classmap = $this->parseAutoloadsType($sortedPackageMap, 'classmap', $mainPackage); $files = $this->parseAutoloadsType($sortedPackageMap, 'files', $mainPackage); krsort($psr0); + krsort($psr4); - return array('psr-0' => $psr0, 'classmap' => $classmap, 'files' => $files); + return array('psr-0' => $psr0, 'psr-4' => $psr4, 'classmap' => $classmap, 'files' => $files); } /** @@ -233,10 +325,16 @@ EOF; } } + if (isset($autoloads['psr-4'])) { + foreach ($autoloads['psr-4'] as $namespace => $path) { + $loader->addPsr4($namespace, $path); + } + } + return $loader; } - protected function getIncludePathsFile(array $packageMap, Filesystem $filesystem, $relVendorPath, $vendorPath, $vendorPathCode, $appBaseDirCode) + protected function getIncludePathsFile(array $packageMap, Filesystem $filesystem, $basePath, $vendorPath, $vendorPathCode, $appBaseDirCode) { $includePaths = array(); @@ -257,59 +355,78 @@ EOF; return; } - $includePathsFile = <<getPathCode($filesystem, $basePath, $vendorPath, $path) . ",\n"; + } + + return <<getPathCode($filesystem, $relVendorPath, $vendorPath, $path) . ",\n"; + protected function getIncludeFilesFile(array $files, Filesystem $filesystem, $basePath, $vendorPath, $vendorPathCode, $appBaseDirCode) + { + $filesCode = ''; + foreach ($files as $functionFile) { + $filesCode .= ' '.$this->getPathCode($filesystem, $basePath, $vendorPath, $functionFile).",\n"; } - return $includePathsFile . ");\n"; + if (!$filesCode) { + return FALSE; + } + + return <<isAbsolutePath($path)) { - if (strpos($path, $relVendorPath) === 0) { - // path starts with vendor dir - $path = substr($path, strlen($relVendorPath)); - $baseDir = '$vendorDir . '; - } else { - $path = '/'.$path; - $baseDir = '$baseDir . '; - } - } elseif (strpos($path, $vendorPath) === 0) { - $path = substr($path, strlen($vendorPath)); - $baseDir = '$vendorDir . '; + $path = $basePath . '/' . $path; } + $path = $filesystem->normalizePath($path); - return $baseDir.var_export($path, true); - } + $baseDir = ''; + if (strpos($path.'/', $vendorPath.'/') === 0) { + $path = substr($path, strlen($vendorPath)); + $baseDir = '$vendorDir'; - protected function getPath(Filesystem $filesystem, $relVendorPath, $vendorPath, $path) - { - $path = strtr($path, '\\', '/'); - if (!$filesystem->isAbsolutePath($path)) { - if (strpos($path, $relVendorPath) === 0) { - // path starts with vendor dir - return $vendorPath . substr($path, strlen($relVendorPath)); + if ($path !== false) { + $baseDir .= " . "; + } + } else { + $path = $filesystem->normalizePath($filesystem->findShortestPath($basePath, $path, true)); + if (!$filesystem->isAbsolutePath($path)) { + $baseDir = '$baseDir . '; + $path = '/' . $path; } + } - return strtr(getcwd(), '\\', '/').'/'.$path; + if (preg_match('/\.phar$/', $path)) { + $baseDir = "'phar://' . " . $baseDir; } - return $path; + return $baseDir . (($path !== false) ? var_export($path, true) : ""); } protected function getAutoloadFile($vendorPathToTargetDirCode, $suffix) @@ -317,7 +434,7 @@ EOF; return << $path) { - $loader->add($namespace, $path); + $loader->set($namespace, $path); } PSR0; + + $file .= <<<'PSR4' + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); } + +PSR4; + if ($useClassMap) { $file .= <<<'CLASSMAP' $classMap = require __DIR__ . '/autoload_classmap.php'; @@ -403,18 +520,41 @@ PSR0; CLASSMAP; } + if ($useGlobalIncludePath) { + $file .= <<<'INCLUDEPATH' + $loader->setUseIncludePath(true); + +INCLUDEPATH; + } + if ($targetDirLoader) { $file .= <<register();{$filesCode} + $file .= <<register($prependAutoloader); + + +REGISTER_LOADER; + + if ($useIncludeFiles) { + $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])) { @@ -448,13 +596,25 @@ FOOTER; foreach ($autoload[$type] as $namespace => $paths) { foreach ((array) $paths as $path) { - // remove target-dir from classmap entries of the root package - if ($type === 'classmap' && $package === $mainPackage && $package->getTargetDir()) { - $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; + } + } + + $relativePath = empty($installPath) ? (empty($path) ? '.' : $path) : $installPath.'/'.$path; + + if ($type === 'files' || $type === 'classmap') { + $autoloads[] = $relativePath; + continue; } - $autoloads[$namespace][] = empty($installPath) ? $path : $installPath.'/'.$path; + $autoloads[$namespace][] = $relativePath; } } } @@ -462,47 +622,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 a4a0dc5a6..443364959 100644 --- a/src/Composer/Autoload/ClassLoader.php +++ b/src/Composer/Autoload/ClassLoader.php @@ -42,19 +42,36 @@ namespace Composer\Autoload; */ class ClassLoader { - private $prefixes = array(); - private $fallbackDirs = array(); + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + private $useIncludePath = false; private $classMap = array(); public function getPrefixes() { - return $this->prefixes; + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; } public function getFallbackDirs() { - return $this->fallbackDirs; + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; } public function getClassMap() @@ -75,27 +92,134 @@ class ClassLoader } /** - * Registers a set of classes + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. * - * @param string $prefix The classes prefix - * @param array|string $paths The location(s) of the classes + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories */ - public function add($prefix, $paths) + public function add($prefix, $paths, $prepend = false) { if (!$prefix) { - foreach ((array) $paths as $path) { - $this->fallbackDirs[] = $path; + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); } return; } - if (isset($this->prefixes[$prefix])) { - $this->prefixes[$prefix] = array_merge( - $this->prefixes[$prefix], + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], (array) $paths ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @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) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; } else { - $this->prefixes[$prefix] = (array) $paths; + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * 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 + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; } } @@ -142,12 +266,12 @@ class ClassLoader * Loads the given class or interface. * * @param string $class The name of the class - * @return bool|null True, if loaded + * @return bool|null True if loaded, null otherwise */ public function loadClass($class) { if ($file = $this->findFile($class)) { - include $file; + includeFile($file); return true; } @@ -158,50 +282,102 @@ class ClassLoader * * @param string $class The name of the class * - * @return string|null The path, if found + * @return string|false The path if found, false otherwise */ public function findFile($class) { + // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 if ('\\' == $class[0]) { $class = substr($class, 1); } + // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } + $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) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name - $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR; - $className = substr($class, $pos + 1); + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name - $classPath = null; - $className = $class; + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } - $classPath .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php'; - - foreach ($this->prefixes as $prefix => $dirs) { - if (0 === strpos($class, $prefix)) { - foreach ($dirs as $dir) { - if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) { - return $dir . DIRECTORY_SEPARATOR . $classPath; + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } } } } } - foreach ($this->fallbackDirs as $dir) { - if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) { - return $dir . DIRECTORY_SEPARATOR . $classPath; + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; } } - if ($this->useIncludePath && $file = stream_resolve_include_path($classPath)) { + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } - - 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 0a4db9bdc..0f289bcbd 100644 --- a/src/Composer/Autoload/ClassMapGenerator.php +++ b/src/Composer/Autoload/ClassMapGenerator.php @@ -13,18 +13,22 @@ 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) { @@ -40,20 +44,22 @@ 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 $whitelist Regex that matches against the file path + * @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, $whitelist = null) + public static function createMap($path, $whitelist = null, IOInterface $io = null, $namespace = null) { if (is_string($path)) { if (is_file($path)) { $path = array(new \SplFileInfo($path)); - } else if (is_dir($path)) { - $path = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path)); + } elseif (is_dir($path)) { + $path = Finder::create()->files()->followLinks()->name('/\.(php|inc|hh)$/')->in($path); } else { throw new \RuntimeException( 'Could not scan for classes inside "'.$path. @@ -65,13 +71,9 @@ class ClassMapGenerator $map = array(); foreach ($path as $file) { - if (!$file->isFile()) { - continue; - } - $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; } @@ -82,9 +84,20 @@ class ClassMapGenerator $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.' + ); + } } - } return $map; @@ -93,32 +106,43 @@ class ClassMapGenerator /** * Extract the classes in the given file * - * @param string $path The file to check - * - * @return array The found classes + * @param string $path The file to check + * @throws \RuntimeException + * @return array The found classes */ private static function findClasses($path) { $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); } // return early if there is no chance of matching anything in this file - if (!preg_match('{\b(?:class|interface'.$traits.')\b}i', $contents)) { + if (!preg_match('{\b(?:class|interface'.$traits.')\s}i', $contents)) { return array(); } // 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('{"[^"\\\\]*(\\\\.[^"\\\\]*)*"|\'[^\'\\\\]*(\\\\.[^\'\\\\]*)*\'}', 'null', $contents); + $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); @@ -142,7 +166,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 a3b9114ba..f79642066 100644 --- a/src/Composer/Cache.php +++ b/src/Composer/Cache.php @@ -23,6 +23,7 @@ use Symfony\Component\Finder\Finder; */ class Cache { + private static $cacheCollected = false; private $io; private $root; private $enabled = true; @@ -31,8 +32,8 @@ class Cache /** * @param IOInterface $io - * @param string $cacheDir location of the cache - * @param string $whitelist List of characters that are allowed in path names (used in a regex character class) + * @param string $cacheDir location of the cache + * @param string $whitelist List of characters that are allowed in path names (used in a regex character class) * @param Filesystem $filesystem optional filesystem instance */ public function __construct(IOInterface $io, $cacheDir, $whitelist = 'a-z0-9.', Filesystem $filesystem = null) @@ -63,6 +64,10 @@ class Cache { $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); if ($this->enabled && file_exists($this->root . $file)) { + if ($this->io->isDebug()) { + $this->io->write('Reading '.$this->root . $file.' from cache'); + } + return file_get_contents($this->root . $file); } @@ -74,57 +79,97 @@ class Cache if ($this->enabled) { $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + if ($this->io->isDebug()) { + $this->io->write('Writing '.$this->root . $file.' into cache'); + } + return file_put_contents($this->root . $file, $contents); } return false; } + /** + * Copy a file into the cache + */ public function copyFrom($file, $source) { if ($this->enabled) { $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); $this->filesystem->ensureDirectoryExists(dirname($this->root . $file)); + if ($this->io->isDebug()) { + $this->io->write('Writing '.$this->root . $file.' into cache'); + } + return copy($source, $this->root . $file); } return false; } + /** + * Copy a file out of the cache + */ public function copyTo($file, $target) { $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); if ($this->enabled && file_exists($this->root . $file)) { touch($this->root . $file); + if ($this->io->isDebug()) { + $this->io->write('Reading '.$this->root . $file.' from cache'); + } + return copy($this->root . $file, $target); } return false; } + public function gcIsNecessary() + { + 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; } - public function gc($ttl) + 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) { + $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()->getPathname(); + $totalSize -= $this->filesystem->size($filepath); + $this->filesystem->unlink($filepath); + $iterator->next(); + } + } + + self::$cacheCollected = true; - $finder = Finder::create()->files()->in($this->root)->date('until '.$expire->format('Y-m-d H:i:s')); - foreach ($finder as $file) { - unlink($file->getRealPath()); + return true; } - return true; + return false; } public function sha1($file) @@ -146,4 +191,9 @@ class Cache return false; } + + protected function getFinder() + { + return Finder::create()->in($this->root)->files(); + } } diff --git a/src/Composer/Command/ArchiveCommand.php b/src/Composer/Command/ArchiveCommand.php new file mode 100644 index 000000000..34f6fe8a6 --- /dev/null +++ b/src/Composer/Command/ArchiveCommand.php @@ -0,0 +1,143 @@ + + * 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\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; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Creates an archive of a package for distribution. + * + * @author Nils Adermann + */ +class ArchiveCommand extends Command +{ + protected function configure() + { + $this + ->setName('archive') + ->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, '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', '.'), + )) + ->setHelp(<<archive command creates an archive of the specified format +containing the files and directories of the Composer project or the specified +package in the specified version and writes it to the specified directory. + +php composer.phar archive [--format=zip] [--dir=/foo] [package [version]] + +EOT + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $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 = '.') + { + $config = Factory::createConfig(); + $factory = new Factory; + $downloadManager = $factory->createDownloadManager($io, $config); + $archiveManager = $factory->createArchiveManager($config, $downloadManager); + + if ($packageName) { + $package = $this->selectPackage($io, $packageName, $version); + + if (!$package) { + return 1; + } + } else { + $package = $this->getComposer()->getPackage(); + } + + $io->write('Creating the archive.'); + $archiveManager->archive($package, $format, $dest); + + return 0; + } + + protected function selectPackage(IOInterface $io, $packageName, $version = null) + { + $io->write('Searching for the specified package.'); + + if ($composer = $this->getComposer(false)) { + $localRepo = $composer->getRepositoryManager()->getLocalRepository(); + $repos = new CompositeRepository(array_merge(array($localRepo), $composer->getRepositoryManager()->getRepositories())); + } else { + $defaultRepos = Factory::createDefaultRepositories($this->getIO()); + $io->write('No composer.json found in the current directory, searching packages from ' . implode(', ', array_keys($defaultRepos))); + $repos = new CompositeRepository($defaultRepos); + } + + $pool = new Pool(); + $pool->addRepository($repos); + + $parser = new VersionParser(); + $constraint = ($version) ? $parser->parseConstraints($version) : null; + $packages = $pool->whatProvides($packageName, $constraint, true); + + if (count($packages) > 1) { + $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 = reset($packages); + $io->write('Found an exact match '.$package->getPrettyString().'.'); + } else { + $io->write('Could not find a package matching '.$packageName.'.'); + + return false; + } + + return $package; + } +} diff --git a/src/Composer/Command/ClearCacheCommand.php b/src/Composer/Command/ClearCacheCommand.php new file mode 100644 index 000000000..47d139144 --- /dev/null +++ b/src/Composer/Command/ClearCacheCommand.php @@ -0,0 +1,69 @@ + + * 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 849f83778..862b54e58 100644 --- a/src/Composer/Command/Command.php +++ b/src/Composer/Command/Command.php @@ -37,16 +37,18 @@ abstract class Command extends BaseCommand private $io; /** - * @param bool $required + * @param bool $required + * @param bool $disablePlugins + * @throws \RuntimeException * @return Composer */ - public function getComposer($required = true) + public function getComposer($required = true, $disablePlugins = false) { if (null === $this->composer) { $application = $this->getApplication(); if ($application instanceof Application) { /* @var $application Application */ - $this->composer = $application->getComposer($required); + $this->composer = $application->getComposer($required, $disablePlugins); } elseif ($required) { throw new \RuntimeException( 'Could not create a Composer\Composer instance, you must inject '. diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index abe5fd4f0..67a2f70a8 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -53,6 +53,7 @@ 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'), @@ -76,7 +77,7 @@ You can add a repository to the global config.json file by passing in the To edit the file in an external editor: - %command.full_name% --edit + %command.full_name% --editor To choose your editor you can set the "EDITOR" env variable. @@ -87,7 +88,7 @@ To get a list of configuration values in the file: You can always pass more than one option. As an example, if you want to edit the global config.json file. - %command.full_name% --edit --global + %command.full_name% --editor --global EOT ) ; @@ -102,7 +103,7 @@ EOT 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,11 +114,23 @@ 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); + @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()) { @@ -132,7 +145,7 @@ EOT { // Open file in editor if ($input->getOption('editor')) { - $editor = getenv('EDITOR'); + $editor = escapeshellcmd(getenv('EDITOR')); if (!$editor) { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $editor = 'notepad'; @@ -146,18 +159,20 @@ 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 if ($input->getOption('list')) { - $this->listConfiguration($this->config->all(), $output); + $this->listConfiguration($this->config->all(), $this->config->raw(), $output); return 0; } @@ -235,17 +250,79 @@ EOT )); } + // handle github-oauth + if (preg_match('/^(github-oauth|http-basic)\.(.+)/', $settingKey, $matches)) { + if ($input->getOption('unset')) { + $this->authConfigSource->removeConfigSetting($matches[1].'.'.$matches[2]); + $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]); + + return; + } + + 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; + } + + $booleanValidator = function ($val) { return in_array($val, array('true', 'false', '1', '0'), true); }; + $booleanNormalizer = function ($val) { return $val !== 'false' && (bool) $val; }; + // handle config values $uniqueConfigValues = array( 'process-timeout' => array('is_numeric', 'intval'), - 'cache-ttl' => array('is_numeric', 'intval'), - 'cache-files-ttl' => array('is_numeric', 'intval'), + 'use-include-path' => array($booleanValidator, $booleanNormalizer), + 'preferred-install' => array( + 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; }), - 'notify-on-install' => array( - function ($val) { return true; }, - function ($val) { return $val !== 'false' && (bool) $val; } + 'cache-dir' => array('is_string', function ($val) { return $val; }), + 'cache-files-dir' => array('is_string', function ($val) { return $val; }), + 'cache-repo-dir' => array('is_string', function ($val) { return $val; }), + 'cache-vcs-dir' => array('is_string', function ($val) { return $val; }), + 'cache-ttl' => array('is_numeric', 'intval'), + 'cache-files-ttl' => array('is_numeric', 'intval'), + 'cache-files-maxsize' => array( + function ($val) { return preg_match('/^\s*([0-9.]+)\s*(?:([kmg])(?:i?b)?)?\s*$/i', $val) > 0; }, + function ($val) { return $val; } ), + 'discard-changes' => array( + function ($val) { return in_array($val, array('stash', 'true', 'false', '1', '0'), true); }, + function ($val) { + if ('stash' === $val) { + return 'stash'; + } + + return $val !== 'false' && (bool) $val; + } + ), + 'autoloader-suffix' => array('is_string', function ($val) { return $val === 'null' ? null : $val; }), + 'optimize-autoloader' => array($booleanValidator, $booleanNormalizer), + 'prepend-autoloader' => array($booleanValidator, $booleanNormalizer), ); $multiConfigValues = array( 'github-protocols' => array( @@ -255,8 +332,8 @@ EOT } foreach ($vals as $val) { - if (!in_array($val, array('git', 'https', 'http'))) { - return 'valid protocols include: git, https, http'; + if (!in_array($val, array('git', 'https', 'ssh'))) { + return 'valid protocols include: git, https, ssh'; } } @@ -266,6 +343,18 @@ EOT return $vals; } ), + 'github-domains' => array( + function ($vals) { + if (!is_array($vals)) { + return 'array expected'; + } + + return true; + }, + function ($vals) { + return $vals; + } + ), ); foreach ($uniqueConfigValues as $name => $callbacks) { @@ -315,10 +404,11 @@ EOT * Display the contents of the file in a pretty formatted way * * @param array $contents + * @param array $rawContents * @param OutputInterface $output * @param string|null $k */ - protected function listConfiguration(array $contents, OutputInterface $output, $k = null) + protected function listConfiguration(array $contents, array $rawContents, OutputInterface $output, $k = null) { $origK = $k; foreach ($contents as $key => $value) { @@ -326,9 +416,11 @@ EOT continue; } + $rawVal = isset($rawContents[$key]) ? $rawContents[$key] : null; + if (is_array($value) && (!is_numeric(key($value)) || ($key === 'repositories' && null === $k))) { $k .= preg_replace('{^config\.}', '', $key . '.'); - $this->listConfiguration($value, $output, $k); + $this->listConfiguration($value, $rawVal, $output, $k); if (substr_count($k, '.') > 1) { $k = str_split($k, strrpos($k, '.', -2)); @@ -352,7 +444,11 @@ EOT $value = var_export($value, true); } - $output->writeln('[' . $k . $key . '] ' . $value . ''); + if (is_string($rawVal) && $rawVal != $value) { + $output->writeln('[' . $k . $key . '] ' . $rawVal . ' (' . $value . ')'); + } else { + $output->writeln('[' . $k . $key . '] ' . $value . ''); + } } } } diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 4c1aeb6d6..9928692eb 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -19,19 +19,21 @@ 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; use Composer\Repository\InstalledFilesystemRepository; +use Composer\Script\ScriptEvents; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Finder\Finder; use Composer\Json\JsonFile; +use Composer\Config\JsonConfigSource; use Composer\Util\Filesystem; use Composer\Util\RemoteFilesystem; use Composer\Package\Version\VersionParser; @@ -41,6 +43,8 @@ use Composer\Package\Version\VersionParser; * * @author Benjamin Eberlei * @author Jordi Boggiano + * @author Tobias Munk + * @author Nils Adermann */ class CreateProjectCommand extends Command { @@ -50,24 +54,29 @@ class CreateProjectCommand extends Command ->setName('create-project') ->setDescription('Create new project from a package into given directory.') ->setDefinition(array( - new InputArgument('package', InputArgument::REQUIRED, 'Package name to be installed'), + 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 InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum-stability allowed (unless a version is specified).', 'stable'), + 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.'), new InputOption('repository-url', null, InputOption::VALUE_REQUIRED, 'Pick a different repository url to look for the package.'), - new InputOption('dev', null, InputOption::VALUE_NONE, 'Whether to install dependencies for development.'), - new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'Whether to disable custom installers.'), + new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of require-dev packages (enabled by default, only present for BC).'), + new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'), + new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Whether to disable plugins.'), + new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'DEPRECATED: Use no-plugins instead.'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Whether to prevent execution of all defined scripts in the root package.'), 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.'), )) ->setHelp(<<create-project command creates a new project from a given -package into a new directory. You can use this command to bootstrap new -projects or setup a clean version-controlled installation -for developers of your project. +package into a new directory. If executed without params and in a directory +with a composer.json file it installs the packages for the current project. + +You can use this command to bootstrap new projects or setup a clean +version-controlled installation for developers of your project. php composer.phar create-project vendor/project target-directory [version] @@ -77,9 +86,7 @@ To install unstable packages, either specify the version you want, or use the --stability=dev (where dev can be one of RC, beta, alpha or dev). To setup a developer workable version you should create the project using the source -controlled code by appending the '--prefer-source' flag. Also, it is -advisable to install all dependencies required for development by appending the -'--dev' flag. +controlled code by appending the '--prefer-source' flag. To install a package from another repository than the default one you can pass the '--repository-url=http://myrepository.org' flag. @@ -91,39 +98,144 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { + $config = Factory::createConfig(); + + $preferSource = false; + $preferDist = false; + $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.'); + $input->setOption('no-plugins', true); + } + return $this->installProject( $this->getIO(), + $config, $input->getArgument('package'), $input->getArgument('directory'), $input->getArgument('version'), $input->getOption('stability'), - $input->getOption('prefer-source'), - $input->getOption('prefer-dist'), - $input->getOption('dev'), + $preferSource, + $preferDist, + !$input->getOption('no-dev'), $input->getOption('repository-url'), - $input->getOption('no-custom-installers'), + $input->getOption('no-plugins'), $input->getOption('no-scripts'), $input->getOption('keep-vcs'), - $input->getOption('no-progress') + $input->getOption('no-progress'), + $input->getOption('no-install'), + $input ); } - public function installProject(IOInterface $io, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repositoryUrl = null, $disableCustomInstallers = false, $noScripts = false, $keepVcs = false, $noProgress = 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, InputInterface $input) { - $config = Factory::createConfig(); + $oldCwd = getcwd(); + + // we need to manually load the configuration to pass the auth credentials to the io interface! + $io->loadConfiguration($config); - $stability = strtolower($stability); - if ($stability === 'rc') { - $stability = 'RC'; + if ($packageName !== null) { + $installedFromVcs = $this->installRootPackage($io, $config, $packageName, $directory, $packageVersion, $stability, $preferSource, $preferDist, $installDevPackages, $repositoryUrl, $disablePlugins, $noScripts, $keepVcs, $noProgress); + } else { + $installedFromVcs = false; } - if (!isset(BasePackage::$stabilities[$stability])) { - throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities))); + + $composer = Factory::create($io, null, $disablePlugins); + $fs = new Filesystem(); + + if ($noScripts === false) { + // dispatch event + $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); + + if ($disablePlugins) { + $installer->disablePlugins(); + } + + $status = $installer->run(); + if (0 !== $status) { + return $status; + } + } + + $hasVcs = $installedFromVcs; + if (!$keepVcs && $installedFromVcs + && ( + !$io->isInteractive() + || $io->askConfirmation('Do you want to remove the existing VCS (.git, .svn..) history? [Y,n]? ', true) + ) + ) { + $finder = new Finder(); + $finder->depth(0)->directories()->in(getcwd())->ignoreVCS(false)->ignoreDotFiles(false); + foreach (array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg') as $vcsName) { + $finder->name($vcsName); + } + + try { + $dirs = iterator_to_array($finder); + unset($finder); + foreach ($dirs as $dir) { + if (!$fs->removeDirectory($dir)) { + throw new \RuntimeException('Could not remove '.$dir); + } + } + } catch (\Exception $e) { + $io->write('An error occurred while removing the VCS metadata: '.$e->getMessage().''); + } + + $hasVcs = false; + } + + // rewriting self.version dependencies with explicit version numbers if the package's vcs metadata is gone + if (!$hasVcs) { + $package = $composer->getPackage(); + $configSource = new JsonConfigSource(new JsonFile('composer.json')); + foreach (BasePackage::$supportedLinkTypes as $type => $meta) { + foreach ($package->{'get'.$meta['method']}() as $link) { + if ($link->getPrettyConstraint() === 'self.version') { + $configSource->addLink($type, $link->getTarget(), $package->getPrettyVersion()); + } + } + } } + if ($noScripts === false) { + // dispatch event + $composer->getEventDispatcher()->dispatchCommandEvent(ScriptEvents::POST_CREATE_PROJECT_CMD, $installDevPackages); + } + + chdir($oldCwd); + $vendorComposerDir = $composer->getConfig()->get('vendor-dir').'/composer'; + if (is_dir($vendorComposerDir) && $fs->isDirEmpty($vendorComposerDir)) { + @rmdir($vendorComposerDir); + $vendorDir = $composer->getConfig()->get('vendor-dir'); + if (is_dir($vendorDir) && $fs->isDirEmpty($vendorDir)) { + @rmdir($vendorDir); + } + } + + return 0; + } + + 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))); + $sourceRepo = new FilesystemRepository(new JsonFile($repositoryUrl, new RemoteFilesystem($io, $config))); } elseif (0 === strpos($repositoryUrl, 'http')) { $sourceRepo = new ComposerRepository(array('url' => $repositoryUrl), $io, $config); } else { @@ -131,25 +243,34 @@ EOT } $parser = new VersionParser(); - $candidates = array(); $requirements = $parser->parseNameVersionPairs(array($packageName)); $name = strtolower($requirements[0]['name']); if (!$packageVersion && isset($requirements[0]['version'])) { $packageVersion = $requirements[0]['version']; } - $pool = new Pool($packageVersion ? 'dev' : $stability); - $pool->addRepository($sourceRepo); - - $constraint = $packageVersion ? new VersionConstraint('=', $parser->normalize($packageVersion)) : null; - $candidates = $pool->whatProvides($name, $constraint); - foreach ($candidates as $key => $candidate) { - if ($candidate->getName() !== $name) { - unset($candidates[$key]); + if (null === $stability) { + if (preg_match('{^[^,\s]*?@('.implode('|', array_keys(BasePackage::$stabilities)).')$}i', $packageVersion, $match)) { + $stability = $match[1]; + } else { + $stability = VersionParser::parseStability($packageVersion); } } - if (!$candidates) { + $stability = VersionParser::normalizeStability($stability); + + if (!isset(BasePackage::$stabilities[$stability])) { + throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities))); + } + + $pool = new Pool($stability); + $pool->addRepository($sourceRepo); + + // find the latest version if there are multiple + $versionSelector = new VersionSelector($pool); + $package = $versionSelector->findBestCandidate($name, $packageVersion); + + if (!$package) { throw new \InvalidArgumentException("Could not find package $name" . ($packageVersion ? " with version $packageVersion." : " with stability $stability.")); } @@ -158,19 +279,10 @@ EOT $directory = getcwd() . DIRECTORY_SEPARATOR . array_pop($parts); } - // select highest version if we have many - $package = $candidates[0]; - 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 ($disableCustomInstallers) { - $io->write('Custom installers have been disabled.'); + if ($disablePlugins) { + $io->write('Plugins have been disabled.'); } if (0 === strpos($package->getPrettyVersion(), 'dev-') && in_array($package->getSourceType(), array('git', 'hg'))) { @@ -195,53 +307,7 @@ EOT putenv('COMPOSER_ROOT_VERSION='.$package->getPrettyVersion()); - // clean up memory - unset($dm, $im, $config, $projectInstaller, $sourceRepo, $package); - - // install dependencies of the created project - $composer = Factory::create($io); - $installer = Installer::create($io, $composer); - - $installer->setPreferSource($preferSource) - ->setPreferDist($preferDist) - ->setDevMode($installDevPackages) - ->setRunScripts( ! $noScripts); - - if ($disableCustomInstallers) { - $installer->disableCustomInstallers(); - } - - if (!$installer->run()) { - return 1; - } - - if (!$keepVcs && $installedFromVcs - && ( - !$io->isInteractive() - || $io->askConfirmation('Do you want to remove the existing VCS (.git, .svn..) history? [Y,n]? ', true) - ) - ) { - $finder = new Finder(); - $finder->depth(0)->directories()->in(getcwd())->ignoreVCS(false)->ignoreDotFiles(false); - foreach (array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg') as $vcsName) { - $finder->name($vcsName); - } - - try { - $fs = new Filesystem(); - $dirs = iterator_to_array($finder); - unset($finder); - foreach ($dirs as $dir) { - if (!$fs->removeDirectory($dir)) { - throw new \RuntimeException('Could not remove '.$dir); - } - } - } catch (\Exception $e) { - $io->write('An error occurred while removing the VCS metadata: '.$e->getMessage().''); - } - } - - return 0; + return $installedFromVcs; } protected function createDownloadManager(IOInterface $io, Config $config) @@ -255,4 +321,35 @@ 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/DependsCommand.php b/src/Composer/Command/DependsCommand.php index fce6f9032..755b40b90 100644 --- a/src/Composer/Command/DependsCommand.php +++ b/src/Composer/Command/DependsCommand.php @@ -13,6 +13,8 @@ namespace Composer\Command; use Composer\DependencyResolver\Pool; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -25,8 +27,8 @@ use Symfony\Component\Console\Output\OutputInterface; class DependsCommand extends Command { protected $linkTypes = array( - 'require' => 'requires', - 'require-dev' => 'devRequires', + 'require' => array('requires', 'requires'), + 'require-dev' => array('devRequires', 'requires (dev)'), ); protected function configure() @@ -50,13 +52,16 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { - $repos = $this->getComposer()->getRepositoryManager()->getLocalRepositories(); + $composer = $this->getComposer(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'depends', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + + $repo = $composer->getRepositoryManager()->getLocalRepository(); $needle = $input->getArgument('package'); $pool = new Pool(); - foreach ($repos as $repo) { - $pool->addRepository($repo); - } + $pool->addRepository($repo); $packages = $pool->whatProvides($needle); if (empty($packages)) { @@ -65,7 +70,6 @@ EOT $linkTypes = $this->linkTypes; - $verbose = (bool) $input->getOption('verbose'); $types = array_map(function ($type) use ($linkTypes) { $type = rtrim($type, 's'); if (!isset($linkTypes[$type])) { @@ -75,28 +79,25 @@ EOT return $type; }, $input->getOption('link-type')); - $dependsOnPackages = false; - foreach ($repos as $repo) { - $repo->filterPackages(function ($package) use ($needle, $types, $linkTypes, $output, $verbose, &$dependsOnPackages) { - static $outputPackages = array(); - - foreach ($types as $type) { - foreach ($package->{'get'.$linkTypes[$type]}() as $link) { - if ($link->getTarget() === $needle) { - $dependsOnPackages = true; - if ($verbose) { - $output->writeln($package->getPrettyName() . ' ' . $package->getPrettyVersion() . ' ' . $type . ' ' . $link->getPrettyConstraint()); - } elseif (!isset($outputPackages[$package->getName()])) { - $output->writeln($package->getPrettyName()); - $outputPackages[$package->getName()] = true; - } + $messages = array(); + $outputPackages = array(); + foreach ($repo->getPackages() as $package) { + foreach ($types as $type) { + foreach ($package->{'get'.$linkTypes[$type][0]}() as $link) { + if ($link->getTarget() === $needle) { + if (!isset($outputPackages[$package->getName()])) { + $messages[] = ''.$package->getPrettyName() . ' ' . $linkTypes[$type][1] . ' ' . $needle .' (' . $link->getPrettyConstraint() . ')'; + $outputPackages[$package->getName()] = true; } } } - }); + } } - if (!$dependsOnPackages) { + if ($messages) { + sort($messages); + $output->writeln($messages); + } else { $output->writeln('There is no installed package depending on "'.$needle.'".'); } } diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php new file mode 100644 index 000000000..ced252787 --- /dev/null +++ b/src/Composer/Command/DiagnoseCommand.php @@ -0,0 +1,413 @@ + + * 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\Composer; +use Composer\Factory; +use Composer\Downloader\TransportException; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; +use Composer\Util\ConfigValidator; +use Composer\Util\ProcessExecutor; +use Composer\Util\RemoteFilesystem; +use Composer\Util\StreamContextFactory; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Jordi Boggiano + */ +class DiagnoseCommand extends Command +{ + protected $rfs; + protected $process; + protected $failures = 0; + + protected function configure() + { + $this + ->setName('diagnose') + ->setDescription('Diagnoses the system to identify common errors.') + ->setHelp(<<diagnose command checks common errors to help debugging problems. + +EOT + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $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: '); + $this->outputResult($output, $this->checkPlatform()); + + $output->write('Checking git settings: '); + $this->outputResult($output, $this->checkGit()); + + $output->write('Checking http connectivity: '); + $this->outputResult($output, $this->checkHttp()); + + $opts = stream_context_get_options(StreamContextFactory::getContext('http://example.org')); + if (!empty($opts['http']['proxy'])) { + $output->write('Checking HTTP proxy: '); + $this->outputResult($output, $this->checkHttpProxy()); + $output->write('Checking HTTP proxy support for request_fulluri: '); + $this->outputResult($output, $this->checkHttpProxyFullUriRequestParam()); + $output->write('Checking HTTPS proxy support for request_fulluri: '); + $this->outputResult($output, $this->checkHttpsProxyFullUriRequestParam()); + } + + if ($oauth = $config->get('github-oauth')) { + foreach ($oauth as $domain => $token) { + $output->write('Checking '.$domain.' oauth access: '); + $this->outputResult($output, $this->checkGithubOauth($domain, $token)); + } + } + + $output->write('Checking disk free space: '); + $this->outputResult($output, $this->checkDiskSpace($config)); + + $output->write('Checking composer version: '); + $this->outputResult($output, $this->checkVersion()); + + return $this->failures; + } + + private function checkComposerSchema() + { + $validator = new ConfigValidator($this->getIO()); + list($errors, $publishErrors, $warnings) = $validator->validate(Factory::getComposerFile()); + + if ($errors || $publishErrors || $warnings) { + $messages = array( + 'error' => array_merge($errors, $publishErrors), + 'warning' => $warnings, + ); + + $output = ''; + foreach ($messages as $style => $msgs) { + foreach ($msgs as $msg) { + $output .= '<' . $style . '>' . $msg . '' . PHP_EOL; + } + } + + return rtrim($output); + } + + return true; + } + + private function checkGit() + { + $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 true; + } + + private function checkHttp() + { + $protocol = extension_loaded('openssl') ? 'https' : 'http'; + try { + $json = $this->rfs->getContents('packagist.org', $protocol . '://packagist.org/packages.json', false); + } catch (\Exception $e) { + return $e; + } + + return true; + } + + private function checkHttpProxy() + { + $protocol = extension_loaded('openssl') ? 'https' : 'http'; + try { + $json = json_decode($this->rfs->getContents('packagist.org', $protocol . '://packagist.org/packages.json', false), true); + $hash = reset($json['provider-includes']); + $hash = $hash['sha256']; + $path = str_replace('%hash%', $hash, key($json['provider-includes'])); + $provider = $this->rfs->getContents('packagist.org', $protocol . '://packagist.org/'.$path, false); + + if (hash('sha256', $provider) !== $hash) { + return 'It seems that your proxy is modifying http traffic on the fly'; + } + } catch (\Exception $e) { + return $e; + } + + return true; + } + + /** + * Due to various proxy servers configurations, some servers can't handle non-standard HTTP "http_proxy_request_fulluri" parameter, + * and will return error 500/501 (as not implemented), see discussion @ https://github.com/composer/composer/pull/1825. + * This method will test, if you need to disable this parameter via setting extra environment variable in your system. + * + * @return bool|string + */ + private function checkHttpProxyFullUriRequestParam() + { + $url = 'http://packagist.org/packages.json'; + try { + $this->rfs->getContents('packagist.org', $url, false); + } catch (TransportException $e) { + try { + $this->rfs->getContents('packagist.org', $url, false, array('http' => array('request_fulluri' => false))); + } catch (TransportException $e) { + 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"'; + } + + return true; + } + + /** + * Due to various proxy servers configurations, some servers can't handle non-standard HTTP "http_proxy_request_fulluri" parameter, + * and will return error 500/501 (as not implemented), see discussion @ https://github.com/composer/composer/pull/1825. + * This method will test, if you need to disable this parameter via setting extra environment variable in your system. + * + * @return bool|string + */ + private function checkHttpsProxyFullUriRequestParam() + { + if (!extension_loaded('openssl')) { + return 'You need the openssl extension installed for this check'; + } + + $url = 'https://api.github.com/repos/Seldaek/jsonlint/zipball/1.0.0'; + try { + $rfcResult = $this->rfs->getContents('github.com', $url, false); + } catch (TransportException $e) { + try { + $this->rfs->getContents('github.com', $url, false, array('http' => array('request_fulluri' => false))); + } catch (TransportException $e) { + 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"'; + } + + return true; + } + + private function checkGithubOauth($domain, $token) + { + $this->getIO()->setAuthentication($domain, $token, 'x-oauth-basic'); + 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'; + } 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 checkDiskSpace($config) + { + $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) + ) { + return 'The disk hosting '.$dir.' is full'; + } + + return true; + } + + private function checkVersion() + { + $protocol = extension_loaded('openssl') ? 'https' : 'http'; + $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 true; + } + + private function outputResult(OutputInterface $output, $result) + { + if (true === $result) { + $output->writeln('OK'); + } else { + $this->failures++; + $output->writeln('FAIL'); + if ($result instanceof \Exception) { + $output->writeln('['.get_class($result).'] '.$result->getMessage()); + } elseif ($result) { + $output->writeln($result); + } + } + } + + private function checkPlatform() + { + $output = ''; + $out = function ($msg, $style) use (&$output) { + $output .= '<'.$style.'>'.$msg.''; + }; + + // code below taken from getcomposer.org/installer, any changes should be made there and replicated here + $errors = array(); + $warnings = array(); + + $iniPath = php_ini_loaded_file(); + $displayIniMessage = false; + if ($iniPath) { + $iniMessage = PHP_EOL.PHP_EOL.'The php.ini used by your command-line PHP is: ' . $iniPath; + } else { + $iniMessage = PHP_EOL.PHP_EOL.'A php.ini file does not exist. You will have to create one.'; + } + $iniMessage .= PHP_EOL.'If you can not modify the ini file, you can also run `php -d option=value` to modify ini values on the fly. You can use -d multiple times.'; + + if (!ini_get('allow_url_fopen')) { + $errors['allow_url_fopen'] = true; + } + + if (version_compare(PHP_VERSION, '5.3.2', '<')) { + $errors['php'] = PHP_VERSION; + } + + if (!isset($errors['php']) && version_compare(PHP_VERSION, '5.3.4', '<')) { + $warnings['php'] = PHP_VERSION; + } + + if (!extension_loaded('openssl')) { + $warnings['openssl'] = true; + } + + if (!defined('HHVM_VERSION') && !extension_loaded('apcu') && ini_get('apc.enable_cli')) { + $warnings['apc_cli'] = true; + } + + if (ini_get('xdebug.profiler_enabled')) { + $warnings['xdebug_profile'] = true; + } elseif (extension_loaded('xdebug')) { + $warnings['xdebug_loaded'] = true; + } + + ob_start(); + phpinfo(INFO_GENERAL); + $phpinfo = ob_get_clean(); + if (preg_match('{Configure Command(?: *| *=> *)(.*?)(?:|$)}m', $phpinfo, $match)) { + $configure = $match[1]; + + if (false !== strpos($configure, '--enable-sigchild')) { + $warnings['sigchild'] = true; + } + + if (false !== strpos($configure, '--with-curlwrappers')) { + $warnings['curlwrappers'] = true; + } + } + + if (!empty($errors)) { + 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."; + break; + + case 'allow_url_fopen': + $text = PHP_EOL."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"; + $displayIniMessage = true; + break; + } + $out($text, 'error'); + } + + $output .= PHP_EOL; + } + + if (!empty($warnings)) { + foreach ($warnings as $warning => $current) { + switch ($warning) { + case 'apc_cli': + $text = PHP_EOL."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"; + $displayIniMessage = true; + break; + + case 'sigchild': + $text = PHP_EOL."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"; + 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"; + 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"; + 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."; + 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."; + break; + + case 'xdebug_profile': + $text = PHP_EOL."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"; + $displayIniMessage = true; + break; + } + $out($text, 'warning'); + } + } + + if ($displayIniMessage) { + $out($iniMessage, 'warning'); + } + + return !$warnings && !$errors ? true : $output; + } +} diff --git a/src/Composer/Command/DumpAutoloadCommand.php b/src/Composer/Command/DumpAutoloadCommand.php old mode 100755 new mode 100644 index 710c25bc6..b30fbd140 --- a/src/Composer/Command/DumpAutoloadCommand.php +++ b/src/Composer/Command/DumpAutoloadCommand.php @@ -12,11 +12,11 @@ namespace Composer\Command; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; -use Composer\Repository\CompositeRepository; use Symfony\Component\Console\Output\OutputInterface; -use Composer\Autoload\AutoloadGenerator; /** * @author Jordi Boggiano @@ -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 @@ -41,15 +42,26 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { - $output->writeln('Generating autoload files'); - $composer = $this->getComposer(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'dump-autoload', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $installationManager = $composer->getInstallationManager(); - $localRepos = new CompositeRepository($composer->getRepositoryManager()->getLocalRepositories()); + $localRepo = $composer->getRepositoryManager()->getLocalRepository(); $package = $composer->getPackage(); $config = $composer->getConfig(); - $generator = new AutoloadGenerator(); - $generator->dump($config, $localRepos, $package, $installationManager, 'composer', $input->getOption('optimize')); + $optimize = $input->getOption('optimize') || $config->get('optimize-autoloader'); + + if ($optimize) { + $output->writeln('Generating optimized autoload files'); + } else { + $output->writeln('Generating autoload files'); + } + + $generator = $composer->getAutoloadGenerator(); + $generator->setDevMode(!$input->getOption('no-dev')); + $generator->dump($config, $localRepo, $package, $installationManager, 'composer', $optimize); } } diff --git a/src/Composer/Command/GlobalCommand.php b/src/Composer/Command/GlobalCommand.php new file mode 100644 index 000000000..15f1fff08 --- /dev/null +++ b/src/Composer/Command/GlobalCommand.php @@ -0,0 +1,82 @@ + + * 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\Factory; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Jordi Boggiano + */ +class GlobalCommand extends Command +{ + protected function configure() + { + $this + ->setName('global') + ->setDescription('Allows running commands in the global composer dir ($COMPOSER_HOME).') + ->setDefinition(array( + new InputArgument('command-name', InputArgument::REQUIRED, ''), + new InputArgument('args', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, ''), + )) + ->setHelp(<<\AppData\Roaming\Composer on Windows +and /home//.composer on unix systems. + +Note: This path may vary depending on customizations to bin-dir in +composer.json or the environmental variable COMPOSER_BIN_DIR. + +EOT + ) + ; + } + + public function run(InputInterface $input, OutputInterface $output) + { + // extract real command name + $tokens = preg_split('{\s+}', $input->__toString()); + $args = array(); + foreach ($tokens as $token) { + if ($token && $token[0] !== '-') { + $args[] = $token; + if (count($args) >= 2) { + break; + } + } + } + + // show help for this command if no command was found + if (count($args) < 2) { + return parent::run($input, $output); + } + + // change to global dir + $config = Factory::createConfig(); + chdir($config->get('home')); + $output->writeln('Changed current directory to '.$config->get('home').''); + + // create new input without "global" command prefix + $input = new StringInput(preg_replace('{\bg(?:l(?:o(?:b(?:a(?:l)?)?)?)?)?\b}', '', $input->__toString(), 1)); + + return $this->getApplication()->run($input, $output); + } +} diff --git a/src/Composer/Command/HomeCommand.php b/src/Composer/Command/HomeCommand.php new file mode 100644 index 000000000..023fb0843 --- /dev/null +++ b/src/Composer/Command/HomeCommand.php @@ -0,0 +1,163 @@ + + * 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\Package\Loader\InvalidPackageException; +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; +use Symfony\Component\Process\Exception\InvalidArgumentException; + +/** + * @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.'), + )) + ->setHelp(<<initializeRepo($input, $output); + $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; + } + + $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 + * + * @param InputInterface $input + * @param OutputInterface $output + * @return CompositeRepository + */ + private function initializeRepo(InputInterface $input, OutputInterface $output) + { + $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 2a7132fa5..64e30de59 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; @@ -32,11 +35,12 @@ class InitCommand extends Command { private $gitConfig; private $repos; + private $pool; public function parseAuthorString($author) { - if (preg_match('/^(?P[- \.,\w\'’]+) <(?P.+?)>$/u', $author, $match)) { - if (!function_exists('filter_var') || version_compare(PHP_VERSION, '5.3.3', '<') || $match['email'] === filter_var($match['email'], FILTER_VALIDATE_EMAIL)) { + if (preg_match('/^(?P[- \.,\p{L}\'’]+) <(?P.+?)>$/u', $author, $match)) { + if ($this->isValidEmail($match['email'])) { return array( 'name' => trim($match['name']), 'email' => $match['email'] @@ -64,6 +68,7 @@ class InitCommand extends Command new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum stability (empty or one of: '.implode(', ', array_keys(BasePackage::$stabilities)).')'), + new InputOption('license', 'l', InputOption::VALUE_REQUIRED, 'License of package'), )) ->setHelp(<<init command creates a basic composer.json file @@ -80,7 +85,7 @@ EOT { $dialog = $this->getHelperSet()->get('dialog'); - $whitelist = array('name', 'description', 'author', 'homepage', 'require', 'require-dev', 'stability'); + $whitelist = array('name', 'description', 'author', 'homepage', 'require', 'require-dev', 'stability', 'license'); $options = array_filter(array_intersect_key($input->getOptions(), array_flip($whitelist))); @@ -207,7 +212,8 @@ EOT $description = $input->getOption('description') ?: false; $description = $dialog->ask( $output, - $dialog->getQuestion('Description', $description) + $dialog->getQuestion('Description', $description), + $description ); $input->setOption('description', $description); @@ -222,10 +228,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']); @@ -254,6 +257,14 @@ EOT ); $input->setOption('stability', $minimumStability); + $license = $input->getOption('license') ?: false; + $license = $dialog->ask( + $output, + $dialog->getQuestion('License', $license), + $license + ); + $input->setOption('license', $license); + $output->writeln(array( '', 'Define your dependencies.', @@ -274,9 +285,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), @@ -284,15 +297,7 @@ EOT )); } - $token = strtolower($name); - - $this->repos->filterPackages(function ($package) use ($token, &$packages) { - if (false !== strpos($package->getName(), $token)) { - $packages[] = $package; - } - }); - - return $packages; + return $this->repos; } protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array()) @@ -304,15 +309,18 @@ 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']; @@ -325,37 +333,74 @@ EOT $matches = $this->findPackages($package); if (count($matches)) { - $output->writeln(array( - '', - sprintf('Found %s packages matching %s', count($matches), $package), - '' - )); - - foreach ($matches as $position => $package) { - $output->writeln(sprintf(' %5s %s %s', "[$position]", $package->getPrettyName(), $package->getPrettyVersion())); + $exactMatch = null; + $choices = array(); + foreach ($matches as $position => $foundPackage) { + $choices[] = sprintf(' %5s %s', "[$position]", $foundPackage['name']); + if ($foundPackage['name'] === $package) { + $exactMatch = true; + break; + } } - $output->writeln(''); + // no match, prompt which to pick + if (!$exactMatch) { + $output->writeln(array( + '', + sprintf('Found %s packages matching %s', count($matches), $package), + '' + )); - $validator = function ($selection) use ($matches) { - if ('' === $selection) { - return false; - } + $output->writeln($choices); + $output->writeln(''); - if (!is_numeric($selection) && preg_match('{^\s*(\S+) +(\S.*)\s*}', $selection, $matches)) { - return $matches[1].' '.$matches[2]; - } + $validator = function ($selection) use ($matches) { + if ('' === $selection) { + return false; + } - if (!isset($matches[(int) $selection])) { - throw new \Exception('Not a valid selection'); - } + if (!is_numeric($selection) && preg_match('{^\s*(\S+)\s+(\S.*)\s*$}', $selection, $matches)) { + return $matches[1].' '.$matches[2]; + } + + if (!isset($matches[(int) $selection])) { + throw new \Exception('Not a valid selection'); + } + + $package = $matches[(int) $selection]; - $package = $matches[(int) $selection]; + return $package['name']; + }; - return sprintf('%s %s', $package->getName(), $package->getPrettyVersion()); - }; + $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, determine the best version automatically + if (false !== $package && false === strpos($package, ' ')) { + $validator = function ($input) { + $input = trim($input); + + return $input ?: false; + }; + + $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) { + $constraint = $this->findBestVersionForPackage($input, $package); + + $output->writeln(sprintf( + 'Using version %s for %s', + $constraint, + $package + )); + } - $package = $dialog->askAndValidate($output, $dialog->getQuestion('Enter package # to add, or a "[package] [version]" couple if it is not listed', false, ':'), $validator, 3); + $package .= ' '.$constraint; + } if (false !== $package) { $requires[] = $package; @@ -391,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()) { @@ -429,10 +474,7 @@ EOT return false; } - $pattern = sprintf( - '~^/?%s(/|/\*)?$~', - preg_quote($vendor, '~') - ); + $pattern = sprintf('{^/?%s(/\*?)?$}', preg_quote($vendor)); $lines = file($ignoreFile, FILE_IGNORE_NEW_LINES); foreach ($lines as $line) { @@ -451,7 +493,7 @@ EOT return $parser->parseNameVersionPairs($requirements); } - protected function addVendorIgnore($ignoreFile, $vendor = 'vendor') + protected function addVendorIgnore($ignoreFile, $vendor = '/vendor/') { $contents = ""; if (file_exists($ignoreFile)) { @@ -464,4 +506,72 @@ EOT file_put_contents($ignoreFile, $contents . $vendor. "\n"); } + + protected function isValidEmail($email) + { + // assume it's valid if we can't validate it + if (!function_exists('filter_var')) { + return true; + } + + // php <5.3.3 has a very broken email validator, so bypass checks + if (version_compare(PHP_VERSION, '5.3.3', '<')) { + return true; + } + + 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 0d8ffce2d..4f40837f1 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -13,14 +13,18 @@ namespace Composer\Command; use Composer\Installer; +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; /** * @author Jordi Boggiano * @author Ryan Weaver * @author Konstantin Kudryashov + * @author Nils Adermann */ class InstallCommand extends Command { @@ -33,12 +37,15 @@ class InstallCommand extends Command new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), - new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of dev-require packages.'), - new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'Disables all custom installers.'), + new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of require-dev packages (enabled by default, only present for BC).'), + 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-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', 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('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 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 @@ -55,25 +62,64 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { - $composer = $this->getComposer(); + 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); + } + + $composer = $this->getComposer(true, $input->getOption('no-plugins')); $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $io = $this->getIO(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $install = Installer::create($io, $composer); + $preferSource = false; + $preferDist = false; + + $config = $composer->getConfig(); + + 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'); + } + + $optimize = $input->getOption('optimize-autoloader') || $config->get('optimize-autoloader'); + $install ->setDryRun($input->getOption('dry-run')) ->setVerbose($input->getOption('verbose')) - ->setPreferSource($input->getOption('prefer-source')) - ->setPreferDist($input->getOption('prefer-dist')) - ->setDevMode($input->getOption('dev')) + ->setPreferSource($preferSource) + ->setPreferDist($preferDist) + ->setDevMode(!$input->getOption('no-dev')) ->setRunScripts(!$input->getOption('no-scripts')) - ->setOptimizeAutoloader($input->getOption('optimize-autoloader')) + ->setOptimizeAutoloader($optimize) ; - if ($input->getOption('no-custom-installers')) { - $install->disableCustomInstallers(); + if ($input->getOption('no-plugins')) { + $install->disablePlugins(); } - return $install->run() ? 0 : 1; + return $install->run(); } } diff --git a/src/Composer/Command/LicensesCommand.php b/src/Composer/Command/LicensesCommand.php new file mode 100644 index 000000000..5d05ef74f --- /dev/null +++ b/src/Composer/Command/LicensesCommand.php @@ -0,0 +1,105 @@ + + * 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\Json\JsonFile; +use Composer\Package\Version\VersionParser; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; +use Symfony\Component\Console\Helper\TableHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Benoît Merlet + */ +class LicensesCommand extends Command +{ + protected function configure() + { + $this + ->setName('licenses') + ->setDescription('Show information about licenses of dependencies') + ->setDefinition(array( + new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), + )) + ->setHelp(<<getComposer(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'licenses', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + + $root = $composer->getPackage(); + $repo = $composer->getRepositoryManager()->getLocalRepository(); + + $versionParser = new VersionParser; + + $packages = array(); + foreach ($repo->getPackages() as $package) { + $packages[$package->getName()] = $package; + } + + ksort($packages); + + switch ($format = $input->getOption('format')) { + case 'text': + $output->writeln('Name: '.$root->getPrettyName().''); + $output->writeln('Version: '.$versionParser->formatVersion($root).''); + $output->writeln('Licenses: '.(implode(', ', $root->getLicense()) ?: 'none').''); + $output->writeln('Dependencies:'); + + $table = $this->getHelperSet()->get('table'); + $table->setLayout(TableHelper::LAYOUT_BORDERLESS); + $table->setHorizontalBorderChar(''); + foreach ($packages as $package) { + $table->addRow(array( + $package->getPrettyName(), + $versionParser->formatVersion($package), + implode(', ', $package->getLicense()) ?: 'none', + )); + } + $table->render($output); + break; + + case 'json': + foreach ($packages as $package) { + $dependencies[$package->getPrettyName()] = array( + 'version' => $versionParser->formatVersion($package), + 'license' => $package->getLicense(), + ); + } + + $output->writeln(JsonFile::encode(array( + 'name' => $root->getPrettyName(), + 'version' => $versionParser->formatVersion($root), + 'license' => $root->getLicense(), + 'dependencies' => $dependencies, + ))); + break; + + default: + throw new \RuntimeException(sprintf('Unsupported format "%s". See help for supported formats.', $format)); + } + } +} diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php new file mode 100755 index 000000000..ee1754c65 --- /dev/null +++ b/src/Composer/Command/RemoveCommand.php @@ -0,0 +1,118 @@ + + * 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.'), + )) + ->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')); + ; + + $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 f75ec44db..682a44e66 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -20,6 +20,9 @@ use Composer\Factory; use Composer\Installer; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; +use Composer\Package\Version\VersionParser; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; /** * @author Jérémy Romey @@ -39,6 +42,8 @@ class RequireCommand extends InitCommand 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.'), )) ->setHelp(<<writeln(''.$file.' not found.'); + $newlyCreated = !file_exists($file); + if (!file_exists($file) && !file_put_contents($file, "{\n}\n")) { + $output->writeln(''.$file.' could not be created.'); return 1; } @@ -64,52 +70,84 @@ EOT return 1; } + if (!is_writable($file)) { + $output->writeln(''.$file.' is not writable.'); - $dialog = $this->getHelperSet()->get('dialog'); + return 1; + } $json = new JsonFile($file); $composer = $json->read(); + $composerBackup = file_get_contents($json->getPath()); $requirements = $this->determineRequirements($input, $output, $input->getArgument('packages')); $requireKey = $input->getOption('dev') ? 'require-dev' : 'require'; + $removeKey = $input->getOption('dev') ? 'require' : 'require-dev'; $baseRequirements = array_key_exists($requireKey, $composer) ? $composer[$requireKey] : array(); $requirements = $this->formatRequirements($requirements); - if (!$this->updateFileCleanly($json, $baseRequirements, $requirements, $requireKey)) { + // validate requirements format + $versionParser = new VersionParser(); + foreach ($requirements as $constraint) { + $versionParser->parseConstraints($constraint); + } + + if (!$this->updateFileCleanly($json, $baseRequirements, $requirements, $requireKey, $removeKey)) { foreach ($requirements as $package => $version) { $baseRequirements[$package] = $version; + + if (isset($composer[$removeKey][$package])) { + unset($composer[$removeKey][$package]); + } } $composer[$requireKey] = $baseRequirements; $json->write($composer); } - $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 $composer = $this->getComposer(); $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $io = $this->getIO(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $install = Installer::create($io, $composer); $install ->setVerbose($input->getOption('verbose')) ->setPreferSource($input->getOption('prefer-source')) ->setPreferDist($input->getOption('prefer-dist')) - ->setDevMode($input->getOption('dev')) + ->setDevMode($updateDevMode) ->setUpdate(true) - ->setUpdateWhitelist($requirements); + ->setUpdateWhitelist(array_keys($requirements)) + ->setWhitelistDependencies($input->getOption('update-with-dependencies')); ; - return $install->run() ? 0 : 1; + $status = $install->run(); + if ($status !== 0) { + 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) { $contents = file_get_contents($json->getPath()); @@ -119,6 +157,9 @@ EOT if (!$manipulator->addLink($requireKey, $package, $constraint)) { return false; } + if (!$manipulator->removeSubNode($removeKey, $package)) { + return false; + } } file_put_contents($json->getPath(), $manipulator->getContents()); diff --git a/src/Composer/Command/RunScriptCommand.php b/src/Composer/Command/RunScriptCommand.php new file mode 100644 index 000000000..f01a5febe --- /dev/null +++ b/src/Composer/Command/RunScriptCommand.php @@ -0,0 +1,100 @@ + + * 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\Script\CommandEvent; +use Composer\Script\ScriptEvents; +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 Fabien Potencier + */ +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 + ->setName('run-script') + ->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.'), + )) + ->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) + { + $script = $input->getArgument('script'); + 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'); + + if (in_array($script, $this->commandEvents)) { + return $composer->getEventDispatcher()->dispatchCommandEvent($script, $input->getOption('dev') || !$input->getOption('no-dev'), $args); + } + + 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/SearchCommand.php b/src/Composer/Command/SearchCommand.php index 061786300..b9aaa8d74 100644 --- a/src/Composer/Command/SearchCommand.php +++ b/src/Composer/Command/SearchCommand.php @@ -18,9 +18,10 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; -use Composer\Package\CompletePackageInterface; -use Composer\Package\AliasPackage; +use Composer\Repository\RepositoryInterface; use Composer\Factory; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; /** * @author Robert Schönthal @@ -66,79 +67,18 @@ EOT $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos)); } - $this->onlyName = $input->getOption('only-name'); - $this->tokens = $input->getArgument('tokens'); - $this->output = $output; - $repos->filterPackages(array($this, 'processPackage'), 'Composer\Package\CompletePackage'); - - foreach ($this->lowMatches as $details) { - $output->writeln($details['name'] . ': '. $details['description']); - } - } - - public function processPackage($package) - { - if ($package instanceof AliasPackage || isset($this->matches[$package->getName()])) { - return; + if ($composer) { + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'search', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); } - foreach ($this->tokens as $token) { - if (!$score = $this->matchPackage($package, $token)) { - continue; - } + $onlyName = $input->getOption('only-name'); - if (false !== ($pos = stripos($package->getName(), $token))) { - $name = substr($package->getPrettyName(), 0, $pos) - . '' . substr($package->getPrettyName(), $pos, strlen($token)) . '' - . substr($package->getPrettyName(), $pos + strlen($token)); - } else { - $name = $package->getPrettyName(); - } + $flags = $onlyName ? RepositoryInterface::SEARCH_NAME : RepositoryInterface::SEARCH_FULLTEXT; + $results = $repos->search(implode(' ', $input->getArgument('tokens')), $flags); - $description = strtok($package->getDescription(), "\r\n"); - if (false !== ($pos = stripos($description, $token))) { - $description = substr($description, 0, $pos) - . '' . substr($description, $pos, strlen($token)) . '' - . substr($description, $pos + strlen($token)); - } - - if ($score >= 3) { - $this->output->writeln($name . ': '. $description); - $this->matches[$package->getName()] = true; - } else { - $this->lowMatches[$package->getName()] = array( - 'name' => $name, - 'description' => $description, - ); - } - - return; + foreach ($results as $result) { + $output->writeln($result['name'] . (isset($result['description']) ? ' '. $result['description'] : '')); } } - - /** - * tries to find a token within the name/keywords/description - * - * @param CompletePackageInterface $package - * @param string $token - * @return boolean - */ - private function matchPackage(CompletePackageInterface $package, $token) - { - $score = 0; - - if (false !== stripos($package->getName(), $token)) { - $score += 5; - } - - if (!$this->onlyName && false !== stripos(join(',', $package->getKeywords() ?: array()), $token)) { - $score += 3; - } - - if (!$this->onlyName && false !== stripos($package->getDescription(), $token)) { - $score += 1; - } - - return $score; - } } diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index e36c860b8..13abee32b 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -13,21 +13,37 @@ namespace Composer\Command; use Composer\Composer; +use Composer\Factory; +use Composer\Util\Filesystem; use Composer\Util\RemoteFilesystem; +use Composer\Downloader\FilesystemException; 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 + * @author Kevin Ran + * @author Jordi Boggiano */ class SelfUpdateCommand extends Command { + const HOMEPAGE = 'getcomposer.org'; + const OLD_INSTALL_EXT = '-old.phar'; + protected function configure() { $this ->setName('self-update') ->setAliases(array('selfupdate')) ->setDescription('Updates composer.phar to the latest version.') + ->setDefinition(array( + 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'), + )) ->setHelp(<<self-update command checks getcomposer.org for newer versions of composer and if found, installs the latest. @@ -41,35 +57,168 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { - $rfs = new RemoteFilesystem($this->getIO()); - $latest = trim($rfs->getContents('getcomposer.org', 'http://getcomposer.org/version', false)); + $baseUrl = (extension_loaded('openssl') ? 'https' : 'http') . '://' . self::HOMEPAGE; + $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]; + + // check if current dir is writable and if not try the cache dir from settings + $tmpDir = is_writable(dirname($localFilename)) ? dirname($localFilename) : $cacheDir; + + // check for permissions in local filesystem before start connection process + if (!is_writable($tmpDir)) { + throw new FilesystemException('Composer update failed: the "'.$tmpDir.'" directory used to download the temp file could not be written'); + } + if (!is_writable($localFilename)) { + throw new FilesystemException('Composer update failed: the "'.$localFilename.'" file could not be written'); + } + + if ($input->getOption('rollback')) { + return $this->rollback($output, $rollbackDir, $localFilename); + } + + $latestVersion = trim($remoteFilesystem->getContents(self::HOMEPAGE, $baseUrl. '/version', false)); + $updateVersion = $input->getArgument('version') ?: $latestVersion; + + if (preg_match('{^[0-9a-f]{40}$}', $updateVersion) && $updateVersion !== $latestVersion) { + $output->writeln('You can not update to a specific SHA-1 as those phars are not available for download'); + + return 1; + } + + if (Composer::VERSION === $updateVersion) { + $output->writeln('You are already using composer version '.$updateVersion.'.'); + + return 0; + } + + $tempFilename = $tmpDir . '/' . basename($localFilename, '.phar').'-temp.phar'; + $backupFile = sprintf( + '%s/%s-%s%s', + $rollbackDir, + strtr(Composer::RELEASE_DATE, ' :', '_-'), + preg_replace('{^([0-9a-f]{7})[0-9a-f]{33}$}', '$1', Composer::VERSION), + self::OLD_INSTALL_EXT + ); + + $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); + if (!file_exists($tempFilename)) { + $output->writeln('The download of the new composer version failed for an unexpected reason'); + + return 1; + } + + // remove saved installations of composer + if ($input->getOption('clean-backups')) { + $finder = $this->getOldInstallationFinder($rollbackDir); + + $fs = new Filesystem; + foreach ($finder as $file) { + $file = (string) $file; + $output->writeln('Removing: '.$file.''); + $fs->remove($file); + } + } + + if ($err = $this->setLocalPhar($localFilename, $tempFilename, $backupFile)) { + $output->writeln('The file is corrupted ('.$err->getMessage().').'); + $output->writeln('Please re-run the self-update command to try again.'); + + return 1; + } + + if (file_exists($backupFile)) { + $output->writeln('Use composer self-update --rollback to return to version '.Composer::VERSION); + } else { + $output->writeln('A backup of the current version could not be written to '.$backupFile.', no rollback possible'); + } + } + + protected function rollback(OutputInterface $output, $rollbackDir, $localFilename) + { + $rollbackVersion = $this->getLastBackupVersion($rollbackDir); + if (!$rollbackVersion) { + throw new \UnexpectedValueException('Composer rollback failed: no installation to roll back to in "'.$rollbackDir.'"'); + } + + if (!is_writable($rollbackDir)) { + throw new FilesystemException('Composer rollback failed: the "'.$rollbackDir.'" dir could not be written to'); + } - if (Composer::VERSION !== $latest) { - $output->writeln(sprintf("Updating to version %s.", $latest)); + $old = $rollbackDir . '/' . $rollbackVersion . self::OLD_INSTALL_EXT; - $remoteFilename = 'http://getcomposer.org/composer.phar'; - $localFilename = $_SERVER['argv'][0]; - $tempFilename = basename($localFilename, '.phar').'-temp.phar'; + if (!is_file($old)) { + throw new FilesystemException('Composer rollback failed: "'.$old.'" could not be found'); + } + if (!is_readable($old)) { + throw new FilesystemException('Composer rollback failed: "'.$old.'" could not be read'); + } - $rfs->copy('getcomposer.org', $remoteFilename, $tempFilename); + $oldFile = $rollbackDir . "/{$rollbackVersion}" . self::OLD_INSTALL_EXT; + $output->writeln(sprintf("Rolling back to version %s.", $rollbackVersion)); + if ($err = $this->setLocalPhar($localFilename, $oldFile)) { + $output->writeln('The backup file was corrupted ('.$err->getMessage().') and has been removed.'); - try { - chmod($tempFilename, 0777 & ~umask()); + return 1; + } + + return 0; + } + + protected function setLocalPhar($localFilename, $newFilename, $backupTarget = null) + { + try { + @chmod($newFilename, 0777 & ~umask()); + if (!ini_get('phar.readonly')) { // test the phar validity - $phar = new \Phar($tempFilename); + $phar = new \Phar($newFilename); // free the variable to unlock the file unset($phar); - rename($tempFilename, $localFilename); - } catch (\Exception $e) { - @unlink($tempFilename); - if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) { - throw $e; - } - $output->writeln('The download is corrupted ('.$e->getMessage().').'); - $output->writeln('Please re-run the self-update command to try again.'); } - } else { - $output->writeln("You are using the latest composer version."); + + // copy current file into installations dir + if ($backupTarget && file_exists($localFilename)) { + @copy($localFilename, $backupTarget); + } + + rename($newFilename, $localFilename); + } catch (\Exception $e) { + if ($backupTarget) { + @unlink($newFilename); + } + if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) { + throw $e; + } + + return $e; + } + } + + protected function getLastBackupVersion($rollbackDir) + { + $finder = $this->getOldInstallationFinder($rollbackDir); + $finder->sortByName(); + $files = iterator_to_array($finder); + + if (count($files)) { + return basename(end($files), self::OLD_INSTALL_EXT); } + + return false; + } + + protected function getOldInstallationFinder($rollbackDir) + { + $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 ea3f9f38e..dbf41ceb9 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -12,16 +12,20 @@ namespace Composer\Command; -use Composer\Composer; +use Composer\DependencyResolver\Pool; +use Composer\DependencyResolver\DefaultPolicy; use Composer\Factory; use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionParser; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Repository\ArrayRepository; use Composer\Repository\CompositeRepository; +use Composer\Repository\ComposerRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; @@ -40,11 +44,13 @@ class ShowCommand extends Command ->setDescription('Show information about packages') ->setDefinition(array( new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect'), - new InputArgument('version', InputArgument::OPTIONAL, 'Version to inspect'), + new InputArgument('version', InputArgument::OPTIONAL, 'Version or version constraint to inspect'), new InputOption('installed', 'i', InputOption::VALUE_NONE, 'List installed packages only'), new InputOption('platform', 'p', InputOption::VALUE_NONE, 'List platform packages only'), + 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('dev', null, InputOption::VALUE_NONE, 'Enables display of dev-require packages.'), + new InputOption('name-only', 'N', InputOption::VALUE_NONE, 'List package names only'), + new InputOption('path', 'P', InputOption::VALUE_NONE, 'Show package paths'), )) ->setHelp(<<getRepositoryManager(); - $repos = new CompositeRepository(array($manager->getLocalRepository())); - if ($dev) { - $repos->addRepository($manager->getLocalDevRepository()); - } - - return $repos; - }; + $composer = $this->getComposer(false); if ($input->getOption('self')) { - $package = $this->getComposer(false)->getPackage(); + $package = $this->getComposer()->getPackage(); $repos = $installedRepo = new ArrayRepository(array($package)); } elseif ($input->getOption('platform')) { $repos = $installedRepo = $platformRepo; } elseif ($input->getOption('installed')) { - $repos = $installedRepo = $getRepositories($this->getComposer(), $input->getOption('dev')); - } elseif ($composer = $this->getComposer(false)) { - $localRepo = $getRepositories($composer, $input->getOption('dev')); + $repos = $installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository(); + } elseif ($input->getOption('available')) { + $installedRepo = $platformRepo; + if ($composer) { + $repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories()); + } else { + $defaultRepos = Factory::createDefaultRepositories($this->getIO()); + $repos = new CompositeRepository($defaultRepos); + $output->writeln('No composer.json found in the current directory, showing available packages from ' . implode(', ', array_keys($defaultRepos))); + } + } elseif ($composer) { + $localRepo = $composer->getRepositoryManager()->getLocalRepository(); $installedRepo = new CompositeRepository(array($localRepo, $platformRepo)); $repos = new CompositeRepository(array_merge(array($installedRepo), $composer->getRepositoryManager()->getRepositories())); } else { $defaultRepos = Factory::createDefaultRepositories($this->getIO()); - $output->writeln('No composer.json found in the current directory, showing packages from ' . implode(', ', array_keys($defaultRepos))); + $output->writeln('No composer.json found in the current directory, showing available packages from ' . implode(', ', array_keys($defaultRepos))); $installedRepo = $platformRepo; $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos)); } + if ($composer) { + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'show', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + } + // show single package or single version if ($input->getArgument('package') || !empty($package)) { $versions = array(); @@ -120,29 +132,100 @@ EOT // list packages $packages = array(); - $repos->filterPackages(function ($package) use (&$packages, $platformRepo, $installedRepo) { - if ($platformRepo->hasPackage($package)) { + + if ($repos instanceof CompositeRepository) { + $repos = $repos->getRepositories(); + } elseif (!is_array($repos)) { + $repos = array($repos); + } + + foreach ($repos as $repo) { + if ($repo === $platformRepo) { $type = 'platform:'; - } elseif ($installedRepo->hasPackage($package)) { + } elseif ( + $repo === $installedRepo + || ($installedRepo instanceof CompositeRepository && in_array($repo, $installedRepo->getRepositories(), true)) + ) { $type = 'installed:'; } else { $type = 'available:'; } - if (!isset($packages[$type][$package->getName()]) - || version_compare($packages[$type][$package->getName()]->getVersion(), $package->getVersion(), '<') - ) { - $packages[$type][$package->getName()] = $package; + if ($repo instanceof ComposerRepository && $repo->hasProviders()) { + foreach ($repo->getProviderNames() as $name) { + $packages[$type][$name] = $name; + } + } else { + foreach ($repo->getPackages() as $package) { + if (!isset($packages[$type][$package->getName()]) + || !is_object($packages[$type][$package->getName()]) + || version_compare($packages[$type][$package->getName()]->getVersion(), $package->getVersion(), '<') + ) { + $packages[$type][$package->getName()] = $package; + } + } } - }, 'Composer\Package\CompletePackage'); + } + $tree = !$input->getOption('platform') && !$input->getOption('installed') && !$input->getOption('available'); + $indent = $tree ? ' ' : ''; foreach (array('platform:' => true, 'available:' => false, 'installed:' => true) as $type => $showVersion) { if (isset($packages[$type])) { - $output->writeln($type); + if ($tree) { + $output->writeln($type); + } ksort($packages[$type]); + + $nameLength = $versionLength = 0; + foreach ($packages[$type] as $package) { + if (is_object($package)) { + $nameLength = max($nameLength, strlen($package->getPrettyName())); + $versionLength = max($versionLength, strlen($this->versionParser->formatVersion($package))); + } else { + $nameLength = max($nameLength, $package); + } + } + list($width) = $this->getApplication()->getTerminalDimensions(); + if (null === $width) { + // In case the width is not detected, we're probably running the command + // outside of a real terminal, use space without a limit + $width = PHP_INT_MAX; + } + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $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) { - $output->writeln(' '.$package->getPrettyName() .' '.($showVersion ? '['.$this->versionParser->formatVersion($package).']' : '').' : '. strtok($package->getDescription(), "\r\n")); + if (is_object($package)) { + $output->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false); + + if ($writeVersion) { + $output->write(' ' . str_pad($this->versionParser->formatVersion($package), $versionLength, ' '), false); + } + + if ($writeDescription) { + $description = strtok($package->getDescription(), "\r\n"); + $remaining = $width - $nameLength - $versionLength - 4; + if (strlen($description) > $remaining) { + $description = substr($description, 0, $remaining - 3) . '...'; + } + $output->write(' ' . $description); + } + + if ($writePath) { + $path = strtok(realpath($composer->getInstallationManager()->getInstallPath($package)), "\r\n"); + $output->write(' ' . $path); + } + } else { + $output->write($indent . $package); + } + $output->writeln(''); + } + if ($tree) { + $output->writeln(''); } - $output->writeln(''); } } } @@ -150,61 +233,50 @@ EOT /** * finds a package by name and version if provided * - * @param RepositoryInterface $installedRepo - * @param RepositoryInterface $repos - * @param string $name - * @param string $version + * @param RepositoryInterface $installedRepo + * @param RepositoryInterface $repos + * @param string $name + * @param string $version * @return array array(CompletePackageInterface, array of versions) * @throws \InvalidArgumentException */ protected function getPackage(RepositoryInterface $installedRepo, RepositoryInterface $repos, $name, $version = null) { $name = strtolower($name); + $constraint = null; if ($version) { - $version = $this->versionParser->normalize($version); + $constraint = $this->versionParser->parseConstraints($version); } - $match = null; - $matches = array(); - $repos->filterPackages(function ($package) use ($name, $version, &$matches) { - if ($package->getName() === $name) { - $matches[] = $package; - } - }, 'Composer\Package\CompletePackage'); - - if (null === $version) { - // search for a locally installed version - foreach ($matches as $package) { - if ($installedRepo->hasPackage($package)) { - $match = $package; - break; - } - } + $policy = new DefaultPolicy(); + $pool = new Pool('dev'); + $pool->addRepository($repos); - if (!$match) { - // fallback to the highest version - foreach ($matches as $package) { - if (null === $match || version_compare($package->getVersion(), $match->getVersion(), '>=')) { - $match = $package; - } - } + $matchedPackage = null; + $versions = array(); + $matches = $pool->whatProvides($name, $constraint); + foreach ($matches as $index => $package) { + // skip providers/replacers + if ($package->getName() !== $name) { + unset($matches[$index]); + continue; } - } else { - // select the specified version - foreach ($matches as $package) { - if ($package->getVersion() === $version) { - $match = $package; - } + + // select an exact match if it is in the installed repo and no specific version was required + if (null === $version && $installedRepo->hasPackage($package)) { + $matchedPackage = $package; } - } - // build versions array - $versions = array(); - foreach ($matches as $package) { $versions[$package->getPrettyVersion()] = $package->getVersion(); + $matches[$index] = $package->getId(); } - return array($match, $versions); + // select prefered package according to policy rules + if (!$matchedPackage && $matches && $prefered = $policy->selectPreferedPackages($pool, array(), $matches)) { + $matchedPackage = $pool->literalToPackage($prefered[0]); + } + + return array($matchedPackage, $versions); } /** @@ -236,7 +308,11 @@ EOT if ($type === 'psr-0') { foreach ($autoloads as $name => $path) { - $output->writeln(($name ?: '*') . ' => ' . ($path ?: '.')); + $output->writeln(($name ?: '*') . ' => ' . (is_array($path) ? implode(', ', $path) : ($path ?: '.'))); + } + } elseif ($type === 'psr-4') { + foreach ($autoloads as $name => $path) { + $output->writeln(($name ?: '*') . ' => ' . (is_array($path) ? implode(', ', $path) : ($path ?: '.'))); } } elseif ($type === 'classmap') { $output->writeln(implode(', ', $autoloads)); @@ -254,12 +330,6 @@ EOT */ protected function printVersions(InputInterface $input, OutputInterface $output, CompletePackageInterface $package, array $versions, RepositoryInterface $installedRepo, RepositoryInterface $repos) { - if ($input->getArgument('version')) { - $output->writeln('version : ' . $package->getPrettyVersion()); - - return; - } - uasort($versions, 'version_compare'); $versions = array_keys(array_reverse($versions)); diff --git a/src/Composer/Command/StatusCommand.php b/src/Composer/Command/StatusCommand.php index a96d62353..a125bdd3c 100644 --- a/src/Composer/Command/StatusCommand.php +++ b/src/Composer/Command/StatusCommand.php @@ -15,7 +15,10 @@ namespace Composer\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Composer\Downloader\VcsDownloader; +use Composer\Downloader\ChangeReportInterface; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; +use Composer\Script\ScriptEvents; /** * @author Tiago Ribeiro @@ -29,7 +32,7 @@ class StatusCommand extends Command ->setName('status') ->setDescription('Show a list of locally modified packages') ->setDefinition(array( - new InputOption('verbose', 'v', InputOption::VALUE_NONE, 'Show modified files for each directory that contains changes.'), + new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Show modified files for each directory that contains changes.'), )) ->setHelp(<<getComposer(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'status', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $installedRepo = $composer->getRepositoryManager()->getLocalRepository(); $dm = $composer->getDownloadManager(); $im = $composer->getInstallationManager(); + // Dispatch pre-status-command + $composer->getEventDispatcher()->dispatchCommandEvent(ScriptEvents::PRE_STATUS_CMD, true); + $errors = array(); // list packages foreach ($installedRepo->getPackages() as $package) { $downloader = $dm->getDownloaderForInstalledPackage($package); - if ($downloader instanceof VcsDownloader) { + if ($downloader instanceof ChangeReportInterface) { $targetDir = $im->getInstallPath($package); - if ($changes = $downloader->getLocalChanges($targetDir)) { + if ($changes = $downloader->getLocalChanges($package, $targetDir)) { $errors[$targetDir] = $changes; } } @@ -87,6 +97,9 @@ EOT $output->writeln('Use --verbose (-v) to see modified files'); } + // Dispatch post-status-command + $composer->getEventDispatcher()->dispatchCommandEvent(ScriptEvents::POST_STATUS_CMD, true); + return $errors ? 1 : 0; } } diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index a6aa3a476..d45e386ed 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -13,6 +13,8 @@ namespace Composer\Command; use Composer\Installer; +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; @@ -20,6 +22,7 @@ use Symfony\Component\Console\Output\OutputInterface; /** * @author Jordi Boggiano + * @author Nils Adermann */ class UpdateCommand extends Command { @@ -33,12 +36,16 @@ class UpdateCommand extends Command new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), - new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of dev-require packages.'), - new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'Disables all custom installers.'), + new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of require-dev packages (enabled by default, only present for BC).'), + new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'), + 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-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', 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('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.') )) ->setHelp(<<update command reads the composer.json file from the @@ -51,6 +58,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 ) ; @@ -58,27 +70,61 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { - $composer = $this->getComposer(); + 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); + } + + $composer = $this->getComposer(true, $input->getOption('no-plugins')); $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $io = $this->getIO(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $install = Installer::create($io, $composer); + $preferSource = false; + $preferDist = false; + + $config = $composer->getConfig(); + + 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'); + } + + $optimize = $input->getOption('optimize-autoloader') || $config->get('optimize-autoloader'); + $install ->setDryRun($input->getOption('dry-run')) ->setVerbose($input->getOption('verbose')) - ->setPreferSource($input->getOption('prefer-source')) - ->setPreferDist($input->getOption('prefer-dist')) - ->setDevMode($input->getOption('dev')) + ->setPreferSource($preferSource) + ->setPreferDist($preferDist) + ->setDevMode(!$input->getOption('no-dev')) ->setRunScripts(!$input->getOption('no-scripts')) - ->setOptimizeAutoloader($input->getOption('optimize-autoloader')) + ->setOptimizeAutoloader($optimize) ->setUpdate(true) - ->setUpdateWhitelist($input->getArgument('packages')) + ->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $input->getArgument('packages')) + ->setWhitelistDependencies($input->getOption('with-dependencies')) ; - if ($input->getOption('no-custom-installers')) { - $install->disableCustomInstallers(); + if ($input->getOption('no-plugins')) { + $install->disablePlugins(); } - return $install->run() ? 0 : 1; + return $install->run(); } } 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 311a764ac..41c30d6d1 100644 --- a/src/Composer/Compiler.php +++ b/src/Composer/Compiler.php @@ -23,6 +23,9 @@ use Symfony\Component\Process\Process; */ class Compiler { + private $version; + private $versionDate; + /** * Compiles composer into a single phar file * @@ -35,12 +38,20 @@ class Compiler unlink($pharFile); } - $process = new Process('git log --pretty="%h" -n1 HEAD', __DIR__); + $process = new Process('git log --pretty="%H" -n1 HEAD', __DIR__); 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.'); } $this->version = trim($process->getOutput()); + $process = new Process('git log -n1 --pretty=%ci HEAD', __DIR__); + 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'); if ($process->run() == 0) { $this->version = trim($process->getOutput()); @@ -92,8 +103,12 @@ class Compiler $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/autoload.php')); $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/autoload_namespaces.php')); + $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/autoload_psr4.php')); $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/autoload_classmap.php')); $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/autoload_real.php')); + if (file_exists(__DIR__.'/../../vendor/composer/include_paths.php')) { + $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/include_paths.php')); + } $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/ClassLoader.php')); $this->addComposerBin($phar); @@ -112,7 +127,7 @@ class Compiler private function addFile($phar, $file, $strip = true) { - $path = str_replace(dirname(dirname(__DIR__)).DIRECTORY_SEPARATOR, '', $file->getRealPath()); + $path = strtr(str_replace(dirname(dirname(__DIR__)).DIRECTORY_SEPARATOR, '', $file->getRealPath()), '\\', '/'); $content = file_get_contents($file); if ($strip) { @@ -121,7 +136,10 @@ class Compiler $content = "\n".$content."\n"; } - $content = str_replace('@package_version@', $this->version, $content); + if ($path === 'src/Composer/Composer.php') { + $content = str_replace('@package_version@', $this->version, $content); + $content = str_replace('@release_date@', $this->versionDate, $content); + } $phar->addFromString($path, $content); } diff --git a/src/Composer/Composer.php b/src/Composer/Composer.php index 2222f85f9..6731ea9a4 100644 --- a/src/Composer/Composer.php +++ b/src/Composer/Composer.php @@ -16,15 +16,20 @@ use Composer\Package\RootPackageInterface; use Composer\Package\Locker; use Composer\Repository\RepositoryManager; use Composer\Installer\InstallationManager; +use Composer\Plugin\PluginManager; use Composer\Downloader\DownloadManager; +use Composer\EventDispatcher\EventDispatcher; +use Composer\Autoload\AutoloadGenerator; /** * @author Jordi Boggiano * @author Konstantin Kudryashiv + * @author Nils Adermann */ class Composer { const VERSION = '@package_version@'; + const RELEASE_DATE = '@release_date@'; /** * @var Package\RootPackageInterface @@ -51,11 +56,26 @@ class Composer */ private $installationManager; + /** + * @var Plugin\PluginManager + */ + private $pluginManager; + /** * @var Config */ private $config; + /** + * @var EventDispatcher + */ + private $eventDispatcher; + + /** + * @var Autoload\AutoloadGenerator + */ + private $autoloadGenerator; + /** * @param Package\RootPackageInterface $package * @return void @@ -152,4 +172,52 @@ class Composer { return $this->installationManager; } + + /** + * @param Plugin\PluginManager $manager + */ + public function setPluginManager(PluginManager $manager) + { + $this->pluginManager = $manager; + } + + /** + * @return Plugin\PluginManager + */ + public function getPluginManager() + { + return $this->pluginManager; + } + + /** + * @param EventDispatcher $eventDispatcher + */ + public function setEventDispatcher(EventDispatcher $eventDispatcher) + { + $this->eventDispatcher = $eventDispatcher; + } + + /** + * @return EventDispatcher + */ + public function getEventDispatcher() + { + return $this->eventDispatcher; + } + + /** + * @param Autoload\AutoloadGenerator $autoloadGenerator + */ + public function setAutoloadGenerator(AutoloadGenerator $autoloadGenerator) + { + $this->autoloadGenerator = $autoloadGenerator; + } + + /** + * @return Autoload\AutoloadGenerator + */ + public function getAutoloadGenerator() + { + return $this->autoloadGenerator; + } } diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 53f93faff..d5b41efe2 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -21,27 +21,42 @@ class Config { public static $defaultConfig = array( 'process-timeout' => 300, - 'cache-ttl' => 15552000, // 6 months + 'use-include-path' => false, + 'preferred-install' => 'auto', + 'notify-on-install' => true, + 'github-protocols' => array('git', 'https', 'ssh'), 'vendor-dir' => 'vendor', 'bin-dir' => '{$vendor-dir}/bin', - 'notify-on-install' => true, - 'github-protocols' => array('git', 'https', 'http'), 'cache-dir' => '{$home}/cache', 'cache-files-dir' => '{$cache-dir}/files', 'cache-repo-dir' => '{$cache-dir}/repo', 'cache-vcs-dir' => '{$cache-dir}/vcs', + 'cache-ttl' => 15552000, // 6 months + 'cache-files-ttl' => null, // fallback to cache-ttl + 'cache-files-maxsize' => '300MiB', + 'discard-changes' => false, + 'autoloader-suffix' => null, + 'optimize-autoloader' => false, + 'prepend-autoloader' => true, + 'github-domains' => array('github.com'), + 'store-auths' => 'prompt', + // valid keys without defaults (auth config stuff): + // github-oauth + // http-basic ); public static $defaultRepositories = array( 'packagist' => array( 'type' => 'composer', 'url' => 'https?://packagist.org', + 'allow_ssl_downgrade' => true, ) ); private $config; private $repositories; private $configSource; + private $authConfigSource; public function __construct() { @@ -60,17 +75,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; @@ -116,7 +141,8 @@ class Config /** * Returns a setting * - * @param string $key + * @param string $key + * @throws \RuntimeException * @return mixed */ public function get($key) @@ -132,11 +158,37 @@ 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(getenv($env) ?: $this->config[$key]), '/\\'); + $val = preg_replace('#^(\$HOME|~)(/|$)#', rtrim(getenv('HOME') ?: getenv('USERPROFILE'), '/\\') . '/', $val); + + return $val; case 'cache-ttl': return (int) $this->config[$key]; + case 'cache-files-maxsize': + if (!preg_match('/^\s*([0-9.]+)\s*(?:([kmg])(?:i?b)?)?\s*$/i', $this->config[$key], $matches)) { + throw new \RuntimeException( + "Could not parse the value of 'cache-files-maxsize': {$this->config[$key]}" + ); + } + $size = $matches[1]; + if (isset($matches[2])) { + switch (strtolower($matches[2])) { + case 'g': + $size *= 1024; + // intentional fallthrough + case 'm': + $size *= 1024; + // intentional fallthrough + case 'k': + $size *= 1024; + break; + } + } + + return $size; + case 'cache-files-ttl': if (isset($this->config[$key])) { return (int) $this->config[$key]; @@ -147,6 +199,36 @@ class Config case 'home': return rtrim($this->process($this->config[$key]), '/\\'); + case 'discard-changes': + if ($env = getenv('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" + ); + } + if ('stash' === $env) { + return 'stash'; + } + + // convert string value to bool + return $env !== 'false' && (bool) $env; + } + + if (!in_array($this->config[$key], array(true, false, 'stash'), true)) { + throw new \RuntimeException( + "Invalid value for 'discard-changes': {$this->config[$key]}. Expected true, false or stash" + ); + } + + return $this->config[$key]; + + 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", "git" or "ssh"'); + } + + return $this->config[$key]; + default: if (!isset($this->config[$key])) { return null; @@ -168,6 +250,14 @@ class Config return $all; } + public function raw() + { + return array( + 'repositories' => $this->getRepositories(), + 'config' => $this->config, + ); + } + /** * Checks whether a setting exists * @@ -182,7 +272,7 @@ 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} * @return string */ private function process($value) 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 8b8b25e6f..4d0a873f5 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]); + } }); } @@ -81,7 +116,7 @@ class JsonConfigSource implements ConfigSourceInterface */ public function addLink($type, $name, $value) { - $this->manipulateJson('addLink', $type, $name, $value, function (&$config, $key) { + $this->manipulateJson('addLink', $type, $name, $value, function (&$config, $type, $name, $value) { $config[$type][$name] = $value; }); } @@ -91,7 +126,7 @@ class JsonConfigSource implements ConfigSourceInterface */ public function removeLink($type, $name) { - $this->manipulateJson('removeSubNode', $type, $name, function (&$config, $key) { + $this->manipulateJson('removeSubNode', $type, $name, function (&$config, $type, $name) { unset($config[$type][$name]); }); } @@ -105,26 +140,55 @@ 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); } if ($newFile) { - chmod($this->file->getPath(), 0600); + @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 old mode 100755 new mode 100644 index 293c67a60..7148a9a11 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -24,6 +24,8 @@ use Composer\Composer; use Composer\Factory; use Composer\IO\IOInterface; use Composer\IO\ConsoleIO; +use Composer\Json\JsonValidationException; +use Composer\Json\JsonFile; use Composer\Util\ErrorHandler; /** @@ -45,13 +47,21 @@ class Application extends BaseApplication */ protected $io; + private static $logo = ' ______ + / ____/___ ____ ___ ____ ____ ________ _____ + / / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/ +/ /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / +\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/ + /_/ +'; + 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()); } @@ -85,22 +95,51 @@ 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') { + 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 (getenv('COMPOSER_NO_INTERACTION')) { + $input->setInteractive(false); + } + + // 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); } - $oldWorkingDir = getcwd(); - $this->switchWorkingDir($input); - $result = parent::doRun($input, $output); - chdir($oldWorkingDir); + if (isset($oldWorkingDir)) { + chdir($oldWorkingDir); + } if (isset($startTime)) { $output->writeln('Memory usage: '.round(memory_get_usage() / 1024 / 1024, 2).'MB (peak: '.round(memory_get_peak_usage() / 1024 / 1024, 2).'MB), time: '.round(microtime(true) - $startTime, 2).'s'); @@ -111,32 +150,63 @@ class Application extends BaseApplication /** * @param InputInterface $input + * @return string * @throws \RuntimeException */ - private function switchWorkingDir(InputInterface $input) + private function getNewWorkingDir(InputInterface $input) { - $workingDir = $input->getParameterOption(array('--working-dir', '-d'), getcwd()); - if (!is_dir($workingDir)) { + $workingDir = $input->getParameterOption(array('--working-dir', '-d')); + if (false !== $workingDir && !is_dir($workingDir)) { throw new \RuntimeException('Invalid working directory specified.'); } - chdir($workingDir); + + return $workingDir; } /** - * @param bool $required + * {@inheritDoc} + */ + public function renderException($exception, $output) + { + try { + $composer = $this->getComposer(false); + 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) + ) { + $output->writeln('The disk hosting '.$dir.' is full, this may be the cause of the following exception'); + } + } + } catch (\Exception $e) {} + + return parent::renderException($exception, $output); + } + + /** + * @param bool $required + * @param bool $disablePlugins + * @throws JsonValidationException * @return \Composer\Composer */ - public function getComposer($required = true) + public function getComposer($required = true, $disablePlugins = false) { if (null === $this->composer) { try { - $this->composer = Factory::create($this->io); + $this->composer = Factory::create($this->io, null, $disablePlugins); } catch (\InvalidArgumentException $e) { if ($required) { $this->io->write($e->getMessage()); exit(1); } + } catch (JsonValidationException $e) { + $errors = ' - ' . implode(PHP_EOL . ' - ', $e->getErrors()); + $message = $e->getMessage() . ':' . PHP_EOL . $errors; + throw new JsonValidationException($message); } + } return $this->composer; @@ -150,6 +220,11 @@ class Application extends BaseApplication return $this->io; } + public function getHelp() + { + return self::$logo . parent::getHelp(); + } + /** * Initializes all the composer commands */ @@ -169,6 +244,14 @@ class Application extends BaseApplication $commands[] = new Command\RequireCommand(); $commands[] = new Command\DumpAutoloadCommand(); $commands[] = new Command\StatusCommand(); + $commands[] = new Command\ArchiveCommand(); + $commands[] = new Command\DiagnoseCommand(); + $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(); @@ -177,6 +260,14 @@ class Application extends BaseApplication return $commands; } + /** + * {@inheritDoc} + */ + public function getLongVersion() + { + return parent::getLongVersion() . ' ' . Composer::RELEASE_DATE; + } + /** * {@inheritDoc} */ diff --git a/src/Composer/Console/HtmlOutputFormatter.php b/src/Composer/Console/HtmlOutputFormatter.php index 264377ee2..56652bb69 100644 --- a/src/Composer/Console/HtmlOutputFormatter.php +++ b/src/Composer/Console/HtmlOutputFormatter.php @@ -48,7 +48,7 @@ class HtmlOutputFormatter extends OutputFormatter ); /** - * @param array $styles Array of "name => FormatterStyle" instances + * @param array $styles Array of "name => FormatterStyle" instances */ public function __construct(array $styles = array()) { diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index 9217b5ba4..a58cf6184 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -14,26 +14,39 @@ namespace Composer\DependencyResolver; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; +use Composer\Package\BasePackage; use Composer\Package\LinkConstraint\VersionConstraint; /** * @author Nils Adermann + * @author Jordi Boggiano */ class DefaultPolicy implements PolicyInterface { + private $preferStable; + + public function __construct($preferStable = false) + { + $this->preferStable = $preferStable; + } + public function versionCompare(PackageInterface $a, PackageInterface $b, $operator) { + if ($this->preferStable && ($stabA = $a->getStability()) !== ($stabB = $b->getStability())) { + return BasePackage::$stabilities[$stabA] < BasePackage::$stabilities[$stabB]; + } + $constraint = new VersionConstraint($operator, $b->getVersion()); $version = new VersionConstraint('==', $a->getVersion()); - return $constraint->matchSpecific($version); + 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; } @@ -47,14 +60,14 @@ class DefaultPolicy implements PolicyInterface return $pool->getPriority($package->getRepository()); } - public function selectPreferedPackages(Pool $pool, array $installedMap, array $literals) + public function selectPreferedPackages(Pool $pool, array $installedMap, array $literals, $requiredPackage = null) { $packages = $this->groupLiteralsByNamePreferInstalled($pool, $installedMap, $literals); foreach ($packages as &$literals) { $policy = $this; - usort($literals, function ($a, $b) use ($policy, $pool, $installedMap) { - return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), true); + usort($literals, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) { + return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true); }); } @@ -69,8 +82,8 @@ class DefaultPolicy implements PolicyInterface $selected = call_user_func_array('array_merge', $packages); // now sort the result across all packages to respect replaces across packages - usort($selected, function ($a, $b) use ($policy, $pool, $installedMap) { - return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b)); + usort($selected, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) { + return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage); }); return $selected; @@ -96,7 +109,10 @@ class DefaultPolicy implements PolicyInterface return $packages; } - public function compareByPriorityPreferInstalled(Pool $pool, array $installedMap, PackageInterface $a, PackageInterface $b, $ignoreReplace = false) + /** + * @protected + */ + public function compareByPriorityPreferInstalled(Pool $pool, array $installedMap, PackageInterface $a, PackageInterface $b, $requiredPackage = null, $ignoreReplace = false) { if ($a->getRepository() === $b->getRepository()) { // prefer aliases to the original package @@ -119,6 +135,19 @@ class DefaultPolicy implements PolicyInterface if ($this->replaces($b, $a)) { return -1; // use a } + + // for replacers not replacing each other, put a higher prio on replacing + // packages with the same vendor as the required package + if ($requiredPackage && false !== ($pos = strpos($requiredPackage, '/'))) { + $requiredVendor = substr($requiredPackage, 0, $pos); + + $aIsSameVendor = substr($a->getName(), 0, $pos) === $requiredVendor; + $bIsSameVendor = substr($b->getName(), 0, $pos) === $requiredVendor; + + if ($bIsSameVendor !== $aIsSameVendor) { + return $aIsSameVendor ? -1 : 1; + } + } } // priority equal, sort by package id to make reproducible diff --git a/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php b/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php index 4e8c81874..c901bd190 100644 --- a/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php +++ b/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php @@ -26,8 +26,8 @@ class MarkAliasInstalledOperation extends SolverOperation /** * Initializes operation. * - * @param PackageInterface $package package instance - * @param string $reason operation reason + * @param AliasPackage $package package instance + * @param string $reason operation reason */ public function __construct(AliasPackage $package, $reason = null) { diff --git a/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php b/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php index 5585011b3..56f7ac19b 100644 --- a/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php +++ b/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php @@ -26,8 +26,8 @@ class MarkAliasUninstalledOperation extends SolverOperation /** * Initializes operation. * - * @param PackageInterface $package package instance - * @param string $reason operation reason + * @param AliasPackage $package package instance + * @param string $reason operation reason */ public function __construct(AliasPackage $package, $reason = null) { diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 9e18b120e..d0c660f82 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -15,15 +15,16 @@ 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; 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. @@ -38,6 +39,7 @@ class Pool const MATCH = 1; const MATCH_PROVIDE = 2; const MATCH_REPLACE = 3; + const MATCH_FILTERED = 4; protected $repositories = array(); protected $providerRepos = array(); @@ -47,9 +49,11 @@ class Pool protected $stabilityFlags; protected $versionParser; protected $providerCache = array(); + protected $filterRequires; + protected $whitelist = null; protected $id = 1; - public function __construct($minimumStability = 'stable', array $stabilityFlags = array()) + public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) { $stabilities = BasePackage::$stabilities; $this->versionParser = new VersionParser; @@ -60,6 +64,13 @@ class Pool } } $this->stabilityFlags = $stabilityFlags; + $this->filterRequires = $filterRequires; + } + + public function setWhitelist($whitelist) + { + $this->whitelist = $whitelist; + $this->providerCache = array(); } /** @@ -90,27 +101,30 @@ class Pool $name = $package['name']; $version = $package['version']; $stability = VersionParser::parseStability($version); - if ($exempt || $this->isPackageAcceptable($name, $stability)) { - $package['id'] = $this->id++; - $this->packages[] = $package; - // collect names - $names = array( - $name => true, - ); - if (isset($package['provide'])) { - foreach ($package['provide'] as $target => $constraint) { - $names[$target] = true; - } + // 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; - } + } + if (isset($package['replace'])) { + foreach ($package['replace'] as $target => $constraint) { + $names[$target] = true; } + } + $names = array_keys($names); - foreach (array_keys($names) as $provided) { - $this->packageByName[$provided][] =& $this->packages[$this->id - 2]; + 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 @@ -131,8 +145,8 @@ class Pool $alias['root_alias'] = true; $this->packages[] = $alias; - foreach (array_keys($names) as $provided) { - $this->packageByName[$provided][] =& $this->packages[$this->id - 2]; + foreach ($names as $provided) { + $this->packageByName[$provided][$alias['id']] = $this->packages[$this->id - 2]; } } @@ -146,33 +160,36 @@ class Pool $alias['id'] = $this->id++; $this->packages[] = $alias; - foreach (array_keys($names) as $provided) { - $this->packageByName[$provided][] =& $this->packages[$this->id - 2]; + foreach ($names as $provided) { + $this->packageByName[$provided][$alias['id']] = $this->packages[$this->id - 2]; } } } } } else { foreach ($repo->getPackages() as $package) { - $name = $package->getName(); + $names = $package->getNames(); $stability = $package->getStability(); - if ($exempt || $this->isPackageAcceptable($name, $stability)) { + if ($exempt || $this->isPackageAcceptable($names, $stability)) { $package->setId($this->id++); $this->packages[] = $package; - foreach ($package->getNames() as $provided) { + foreach ($names as $provided) { $this->packageByName[$provided][] = $package; } // handle root package aliases + $name = $package->getName(); if (isset($rootAliases[$name][$package->getVersion()])) { $alias = $rootAliases[$name][$package->getVersion()]; - $package->setAlias($alias['alias_normalized']); - $package->setPrettyAlias($alias['alias']); - $package->getRepository()->addPackage($aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias'])); + if ($package instanceof AliasPackage) { + $package = $package->getAliasOf(); + } + $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); $aliasPackage->setRootPackageAlias(true); $aliasPackage->setId($this->id++); + $package->getRepository()->addPackage($aliasPackage); $this->packages[] = $aliasPackage; foreach ($aliasPackage->getNames() as $name) { @@ -204,32 +221,33 @@ class Pool */ public function packageById($id) { - $this->ensurePackageIsLoaded($this->packages[$id - 1]); - - return $this->packages[$id - 1]; + return $this->ensurePackageIsLoaded($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(); @@ -247,18 +265,25 @@ class Pool $candidates = array_merge($candidates, $this->packageByName[$name]); } - if (null === $constraint) { - foreach ($candidates as $key => $candidate) { - $candidates[$key] = $this->ensurePackageIsLoaded($candidate); - } - - return $candidates; - } - $matches = $provideMatches = array(); $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 && ( + (is_array($candidate) && isset($candidate['id']) && !isset($this->whitelist[$candidate['id']])) || + (is_object($candidate) && !($candidate instanceof AliasPackage) && !isset($this->whitelist[$candidate->getId()])) || + (is_object($candidate) && $candidate instanceof AliasPackage && !isset($this->whitelist[$aliasOfCandidate->getId()])) + )) { + continue; + } switch ($this->match($candidate, $name, $constraint)) { case self::MATCH_NONE: break; @@ -280,11 +305,20 @@ class Pool $matches[] = $this->ensurePackageIsLoaded($candidate); break; + case self::MATCH_FILTERED: + break; + default: throw new \UnexpectedValueException('Unexpected match type'); } } + if ($mustMatchName) { + return array_filter($matches, function ($match) use ($name) { + return $match->getName() == $name; + }); + } + // if a package with the required name exists, we ignore providers if ($nameMatch) { return $matches; @@ -320,14 +354,16 @@ class Pool public function isPackageAcceptable($name, $stability) { - // allow if package matches the global stability requirement and has no exception - if (!isset($this->stabilityFlags[$name]) && isset($this->acceptableStabilities[$stability])) { - return true; - } + foreach ((array) $name as $n) { + // allow if package matches the global stability requirement and has no exception + if (!isset($this->stabilityFlags[$n]) && isset($this->acceptableStabilities[$stability])) { + return true; + } - // allow if package matches the package-specific stability flag - if (isset($this->stabilityFlags[$name]) && BasePackage::$stabilities[$stability] <= $this->stabilityFlags[$name]) { - return true; + // allow if package matches the package-specific stability flag + if (isset($this->stabilityFlags[$n]) && BasePackage::$stabilities[$stability] <= $this->stabilityFlags[$n]) { + return true; + } } return false; @@ -344,8 +380,12 @@ class Pool $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']); - $data = $package; + + return $package; } return $data; @@ -360,20 +400,36 @@ class Pool * @param LinkConstraintInterface $constraint The constraint to verify * @return int One of the MATCH* constants of this class or 0 if there is no match */ - private function match($candidate, $name, LinkConstraintInterface $constraint) + 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; + } + + if (!$isDev && !$isAlias && isset($this->filterRequires[$name])) { + $requireFilter = $this->filterRequires[$name]; + } else { + $requireFilter = new EmptyConstraint; } if ($candidateName === $name) { - return $constraint->matches(new VersionConstraint('==', $candidateVersion)) ? self::MATCH : self::MATCH_NAME; + $pkgConstraint = new VersionConstraint('==', $candidateVersion); + + if ($constraint === null || $constraint->matches($pkgConstraint)) { + return $requireFilter->matches($pkgConstraint) ? self::MATCH : self::MATCH_FILTERED; + } + + return self::MATCH_NAME; } if (is_array($candidate)) { @@ -388,29 +444,29 @@ class Pool $replaces = $candidate->getReplaces(); } - // aliases create multiple replaces/provides for one target so they can not use the shortcut + // aliases create multiple replaces/provides for one target so they can not use the shortcut below if (isset($replaces[0]) || isset($provides[0])) { foreach ($provides as $link) { - if ($link->getTarget() === $name && $constraint->matches($link->getConstraint())) { - return self::MATCH_PROVIDE; + if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) { + return $requireFilter->matches($link->getConstraint()) ? self::MATCH_PROVIDE : self::MATCH_FILTERED; } } foreach ($replaces as $link) { - if ($link->getTarget() === $name && $constraint->matches($link->getConstraint())) { - return self::MATCH_REPLACE; + if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) { + return $requireFilter->matches($link->getConstraint()) ? self::MATCH_REPLACE : self::MATCH_FILTERED; } } return self::MATCH_NONE; } - if (isset($provides[$name]) && $constraint->matches($provides[$name]->getConstraint())) { - return self::MATCH_PROVIDE; + if (isset($provides[$name]) && ($constraint === null || $constraint->matches($provides[$name]->getConstraint()))) { + return $requireFilter->matches($provides[$name]->getConstraint()) ? self::MATCH_PROVIDE : self::MATCH_FILTERED; } - if (isset($replaces[$name]) && $constraint->matches($replaces[$name]->getConstraint())) { - return self::MATCH_REPLACE; + if (isset($replaces[$name]) && ($constraint === null || $constraint->matches($replaces[$name]->getConstraint()))) { + return $requireFilter->matches($replaces[$name]->getConstraint()) ? self::MATCH_REPLACE : self::MATCH_FILTERED; } return self::MATCH_NONE; diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 560fbb5dd..765b74a19 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -66,7 +66,8 @@ class Problem /** * A human readable textual representation of the problem's reasons * - * @param array $installedMap A map of all installed packages + * @param array $installedMap A map of all installed packages + * @return string */ public function getPrettyString(array $installedMap = array()) { @@ -79,20 +80,30 @@ 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); - $error = extension_loaded($ext) ? 'has the wrong version ('.phpversion($ext).') installed' : 'is missing from your system'; + $error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system'; return "\n - The requested PHP extension ".$job['packageName'].$this->constraintToText($job['constraint']).' '.$error.'.'; } // handle linked libs if (0 === stripos($job['packageName'], 'lib-')) { - $lib = substr($job['packageName'], 4); + if (strtolower($job['packageName']) === 'lib-icu') { + $error = extension_loaded('intl') ? 'has the wrong version installed, try upgrading the intl extension.' : 'is missing from your system, make sure the intl extension is loaded.'; - return "\n - The requested linked library ".$job['packageName'].$this->constraintToText($job['constraint']).' has the wrong version installed or is missing from your system, make sure to have the extension providing it.'; + return "\n - The requested linked library ".$job['packageName'].$this->constraintToText($job['constraint']).' '.$error; + } + + return "\n - The requested linked library ".$job['packageName'].$this->constraintToText($job['constraint']).' has the wrong version installed or is missing from your system, make sure to load the extension providing it.'; } if (!preg_match('{^[A-Za-z0-9_./-]+$}', $job['packageName'])) { @@ -156,33 +167,45 @@ 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) { - return implode(', ', array_unique(array_map(function ($package) { - return $package->getPrettyString(); - }, - $packages - ))); + $prepared = array(); + foreach ($packages as $package) { + $prepared[$package->getName()]['name'] = $package->getPrettyName(); + $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion(); + } + foreach ($prepared as $name => $package) { + $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']'; + } + + return implode(', ', $prepared); } /** * Turns a constraint into text usable in a sentence describing a job * - * @param LinkConstraint $constraint + * @param \Composer\Package\LinkConstraint\LinkConstraintInterface $constraint * @return string */ protected function constraintToText($constraint) diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 92c8aa175..bf74318f6 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -46,10 +46,8 @@ class Request protected function addJob($packageName, $cmd, LinkConstraintInterface $constraint = null) { $packageName = strtolower($packageName); - $packages = $this->pool->whatProvides($packageName, $constraint); $this->jobs[] = array( - 'packages' => $packages, 'cmd' => $cmd, 'packageName' => $packageName, 'constraint' => $constraint, @@ -58,7 +56,7 @@ class Request 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 d249f6d37..a47038431 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -35,6 +35,8 @@ class Rule protected $literals; protected $type; protected $id; + protected $reason; + protected $reasonData; protected $job; @@ -80,6 +82,27 @@ class Rule return $this->job; } + public function getReason() + { + return $this->reason; + } + + public function getReasonData() + { + return $this->reasonData; + } + + public function getRequiredPackage() + { + if ($this->reason === self::RULE_JOB_INSTALL) { + return $this->reasonData; + } + + if ($this->reason === self::RULE_PACKAGE_REQUIRES) { + return $this->reasonData->getTarget(); + } + } + /** * Checks if this rule is equal to another one * @@ -171,7 +194,7 @@ class Rule $package1 = $this->pool->literalToPackage($this->literals[0]); $package2 = $this->pool->literalToPackage($this->literals[1]); - return $package1->getPrettyString().' conflicts with '.$package2->getPrettyString().'.'; + return $package1->getPrettyString().' conflicts with '.$this->formatPackagesUnique(array($package2)).'.'; case self::RULE_PACKAGE_REQUIRES: $literals = $this->literals; @@ -185,18 +208,14 @@ class Rule $text = $this->reasonData->getPrettyString($sourcePackage); if ($requires) { - $requireText = array(); - foreach ($requires as $require) { - $requireText[] = $require->getPrettyString(); - } - $text .= ' -> satisfiable by '.implode(', ', $requireText).'.'; + $text .= ' -> satisfiable by ' . $this->formatPackagesUnique($requires) . '.'; } else { $targetName = $this->reasonData->getTarget(); // handle php extensions if (0 === strpos($targetName, 'ext-')) { $ext = substr($targetName, 4); - $error = extension_loaded($ext) ? 'has the wrong version ('.phpversion($ext).') installed' : 'is missing from your system'; + $error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system'; $text .= ' -> the requested PHP extension '.$ext.' '.$error.'.'; } elseif (0 === strpos($targetName, 'lib-')) { @@ -216,14 +235,7 @@ class Rule case self::RULE_INSTALLED_PACKAGE_OBSOLETES: return $ruleText; case self::RULE_PACKAGE_SAME_NAME: - $text = "Can only install one of: "; - - $packages = array(); - foreach ($this->literals as $i => $literal) { - $packages[] = $this->pool->literalToPackage($literal)->getPrettyString(); - } - - return $text.implode(', ', $packages).'.'; + return 'Can only install one of: ' . $this->formatPackagesUnique($this->literals) . '.'; case self::RULE_PACKAGE_IMPLICIT_OBSOLETES: return $ruleText; case self::RULE_LEARNED: @@ -233,6 +245,23 @@ class Rule } } + protected function formatPackagesUnique(array $packages) + { + $prepared = array(); + foreach ($packages as $package) { + if (!is_object($package)) { + $package = $this->pool->literalToPackage($package); + } + $prepared[$package->getName()]['name'] = $package->getPrettyName(); + $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion(); + } + foreach ($prepared as $name => $package) { + $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']'; + } + + return implode(', ', $prepared); + } + /** * Formats a rule as a string of the format (Literal1|Literal2|...) * diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index b40ce1a60..5bcf9a079 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -25,6 +25,8 @@ class RuleSetGenerator protected $rules; protected $jobs; protected $installedMap; + protected $whitelistedMap; + protected $addedMap; public function __construct(PolicyInterface $policy, Pool $pool) { @@ -38,13 +40,13 @@ 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) { @@ -67,10 +69,10 @@ 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) @@ -88,11 +90,11 @@ 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) { @@ -105,13 +107,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) { @@ -141,6 +143,41 @@ class RuleSetGenerator $this->rules->add($newRule, $type); } + protected function whitelistFromPackage(PackageInterface $package) + { + $workQueue = new \SplQueue; + $workQueue->enqueue($package); + + while (!$workQueue->isEmpty()) { + $package = $workQueue->dequeue(); + if (isset($this->whitelistedMap[$package->getId()])) { + continue; + } + + $this->whitelistedMap[$package->getId()] = 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) { $workQueue = new \SplQueue; @@ -225,7 +262,7 @@ class RuleSetGenerator * Adds all rules for all update packages of a given package * * @param PackageInterface $package Rules for this package's updates are to - * be added + * be added */ private function addRulesForUpdatePackages(PackageInterface $package) { @@ -236,26 +273,51 @@ class RuleSetGenerator } } + private function whitelistFromUpdatePackages(PackageInterface $package) + { + $updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package, true); + + foreach ($updates as $update) { + $this->whitelistFromPackage($update); + } + } + + protected function whitelistFromJobs() + { + 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() { foreach ($this->jobs as $job) { switch ($job['cmd']) { case 'install': - if ($job['packages']) { - foreach ($job['packages'] as $package) { + $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); + if ($packages) { + foreach ($packages as $package) { if (!isset($this->installedMap[$package->getId()])) { $this->addRulesForPackage($package); } } - $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); } @@ -270,6 +332,16 @@ class RuleSetGenerator $this->rules = new RuleSet; $this->installedMap = $installedMap; + $this->whitelistedMap = array(); + foreach ($this->installedMap as $package) { + $this->whitelistFromPackage($package); + $this->whitelistFromUpdatePackages($package); + } + $this->whitelistFromJobs(); + + $this->pool->setWhitelist($this->whitelistedMap); + + $this->addedMap = array(); foreach ($this->installedMap as $package) { $this->addRulesForPackage($package); $this->addRulesForUpdatePackages($package); diff --git a/src/Composer/DependencyResolver/RuleWatchGraph.php b/src/Composer/DependencyResolver/RuleWatchGraph.php index 59c2d6ef5..a9f7414b2 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) @@ -127,9 +127,9 @@ class RuleWatchGraph * * The rule node's watched literals are updated accordingly. * - * @param $fromLiteral A literal the node used to watch - * @param $toLiteral A literal the node should watch now - * @param $node The rule node to be moved + * @param $fromLiteral mixed A literal the node used to watch + * @param $toLiteral mixed A literal the node should watch now + * @param $node mixed The rule node to be moved */ protected function moveWatch($fromLiteral, $toLiteral, $node) { diff --git a/src/Composer/DependencyResolver/RuleWatchNode.php b/src/Composer/DependencyResolver/RuleWatchNode.php index 0f1e5c08b..59482ffdb 100644 --- a/src/Composer/DependencyResolver/RuleWatchNode.php +++ b/src/Composer/DependencyResolver/RuleWatchNode.php @@ -54,7 +54,7 @@ class RuleWatchNode $literals = $this->rule->getLiterals(); // if there are only 2 elements, both are being watched anyway - if ($literals < 3) { + if (count($literals) < 3) { return; } @@ -64,7 +64,7 @@ class RuleWatchNode $level = $decisions->decisionLevel($literal); if ($level > $watchLevel) { - $this->rule->watch2 = $literal; + $this->watch2 = $literal; $watchLevel = $level; } } diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 1d736d009..6d6088729 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -127,11 +127,15 @@ class Solver foreach ($this->installed->getPackages() as $package) { $this->installedMap[$package->getId()] = $package; } + } + protected function checkForRootRequireProblems() + { foreach ($this->jobs as $job) { switch ($job['cmd']) { case 'update': - foreach ($job['packages'] as $package) { + $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); + foreach ($packages as $package) { if (isset($this->installedMap[$package->getId()])) { $this->updateMap[$package->getId()] = true; } @@ -145,7 +149,7 @@ class Solver break; case 'install': - if (!$job['packages']) { + if (!$this->pool->whatProvides($job['packageName'], $job['constraint'])) { $problem = new Problem($this->pool); $problem->addRule(new Rule($this->pool, array(), null, null, $job)); $this->problems[] = $problem; @@ -160,10 +164,9 @@ class Solver $this->jobs = $request->getJobs(); $this->setupInstalledMap(); - - $this->decisions = new Decisions($this->pool); - $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap); + $this->checkForRootRequireProblems(); + $this->decisions = new Decisions($this->pool); $this->watchGraph = new RuleWatchGraph; foreach ($this->rules as $rule) { @@ -204,7 +207,7 @@ class Solver * Evaluates each term affected by the decision (linked through watches) * If we find unit rules we make new decisions based on them * - * @param integer $level + * @param integer $level * @return Rule|null A rule on conflict, otherwise null. */ protected function propagate($level) @@ -321,7 +324,7 @@ class Solver private function selectAndInstall($level, array $decisionQueue, $disableRules, Rule $rule) { // choose best package to install from decisionQueue - $literals = $this->policy->selectPreferedPackages($this->pool, $this->installedMap, $decisionQueue); + $literals = $this->policy->selectPreferedPackages($this->pool, $this->installedMap, $decisionQueue, $rule->getRequiredPackage()); $selectedLiteral = array_shift($literals); @@ -756,7 +759,6 @@ class Solver if ($lastLiteral) { unset($this->branches[$lastBranchIndex][self::BRANCH_LITERALS][$lastBranchOffset]); - array_values($this->branches[$lastBranchIndex][self::BRANCH_LITERALS]); $level = $lastLevel; $this->revert($level); diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index 4b6df40ed..1ebd9e3b8 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -25,7 +25,7 @@ class SolverProblemsException extends \RuntimeException $this->problems = $problems; $this->installedMap = $installedMap; - parent::__construct($this->createMessage()); + parent::__construct($this->createMessage(), 2); } protected function createMessage() diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php index 4c1fb124a..214c502d1 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 @@ -79,6 +78,7 @@ 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 && diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index b8fc32c7d..9d78d7e2d 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 @@ -28,53 +29,63 @@ abstract class ArchiveDownloader extends FileDownloader */ public function download(PackageInterface $package, $path) { - parent::download($package, $path); + $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); + $retries = 3; + while ($retries--) { + $fileName = parent::download($package, $path); + + if ($this->io->isVerbose()) { + $this->io->write(' Extracting archive'); + } - $fileName = $this->getFileName($package, $path); - if ($this->io->isVerbose()) { - $this->io->write(' Unpacking archive'); - } - try { try { - $this->extract($fileName, $path); + $this->filesystem->ensureDirectoryExists($temporaryDir); + try { + $this->extract($fileName, $temporaryDir); + } catch (\Exception $e) { + // remove cache if the file was corrupted + parent::clearCache($package, $path); + throw $e; + } + + $this->filesystem->unlink($fileName); + + $contentDir = $this->getFolderContent($temporaryDir); + + // only one dir in the archive, extract its contents out of it + 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) { - // remove cache if the file was corrupted - parent::clearCache($package, $path); + // clean up + $this->filesystem->removeDirectory($path); + $this->filesystem->removeDirectory($temporaryDir); + + // retry downloading if we have an invalid zip file + if ($retries && $e instanceof \UnexpectedValueException && class_exists('ZipArchive') && $e->getCode() === \ZipArchive::ER_NOZIP) { + $this->io->write(' Invalid zip file, retrying...'); + usleep(500000); + continue; + } + throw $e; } - if ($this->io->isVerbose()) { - $this->io->write(' Cleaning up'); - } - unlink($fileName); - - // If we have only a one dir inside it suppose to be a package itself - $contentDir = glob($path . '/*'); - if (1 === count($contentDir)) { - $contentDir = $contentDir[0]; - - if (is_file($contentDir)) { - $this->filesystem->rename($contentDir, $path . '/' . basename($contentDir)); - } else { - // Rename the content directory to avoid error when moving up - // a child folder with the same name - $temporaryDir = sys_get_temp_dir().'/'.md5(time().rand()); - $this->filesystem->rename($contentDir, $temporaryDir); - $contentDir = $temporaryDir; - - foreach (array_merge(glob($contentDir . '/.*'), glob($contentDir . '/*')) as $file) { - if (trim(basename($file), '.')) { - $this->filesystem->rename($file, $path . '/' . basename($file)); - } - } - - $this->filesystem->removeDirectory($contentDir); - } - } - } catch (\Exception $e) { - // clean up - $this->filesystem->removeDirectory($path); - throw $e; + break; } $this->io->write(''); @@ -85,7 +96,7 @@ abstract class ArchiveDownloader extends FileDownloader */ protected function getFileName(PackageInterface $package, $path) { - return rtrim($path.'/'.md5($path.spl_object_hash($package)).'.'.pathinfo($package->getDistUrl(), PATHINFO_EXTENSION), '.'); + return rtrim($path.'/'.md5($path.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.'); } /** @@ -107,16 +118,7 @@ abstract class ArchiveDownloader extends FileDownloader } if (!extension_loaded('openssl') && (0 === strpos($url, 'https:') || 0 === strpos($url, 'http://github.com'))) { - // bypass https for github if openssl is disabled - if (preg_match('{^https://api\.github\.com/repos/([^/]+/[^/]+)/(zip|tar)ball/([^/]+)$}i', $url, $match)) { - $url = 'http://nodeload.github.com/'.$match[1].'/'.$match[2].'/'.$match[3]; - } elseif (preg_match('{^https://github\.com/([^/]+/[^/]+)/(zip|tar)ball/([^/]+)$}i', $url, $match)) { - $url = 'http://nodeload.github.com/'.$match[1].'/'.$match[2].'/'.$match[3]; - } elseif (preg_match('{^https://github\.com/([^/]+/[^/]+)/archive/([^/]+)\.(zip|tar\.gz)$}i', $url, $match)) { - $url = 'http://nodeload.github.com/'.$match[1].'/'.$match[3].'/'.$match[2]; - } else { - throw new \RuntimeException('You must enable the openssl extension to download files via https'); - } + throw new \RuntimeException('You must enable the openssl extension to download files via https'); } return parent::processUrl($package, $url); @@ -131,4 +133,21 @@ abstract class ArchiveDownloader extends FileDownloader * @throws \UnexpectedValueException If can not extract downloaded file to path */ abstract protected function extract($file, $path); + + /** + * Returns the folder content, excluding dotfiles + * + * @param string $dir Directory + * @return \SplFileInfo[] + */ + private function getFolderContent($dir) + { + $finder = Finder::create() + ->ignoreVCS(false) + ->ignoreDotFiles(false) + ->depth(0) + ->in($dir); + + return iterator_to_array($finder); + } } diff --git a/src/Composer/Downloader/ChangeReportInterface.php b/src/Composer/Downloader/ChangeReportInterface.php new file mode 100644 index 000000000..3fb1dc5d0 --- /dev/null +++ b/src/Composer/Downloader/ChangeReportInterface.php @@ -0,0 +1,32 @@ + + * 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\Package\PackageInterface; + +/** + * ChangeReport interface. + * + * @author Sascha Egerer + */ +interface ChangeReportInterface +{ + /** + * Checks for changes to the local copy + * + * @param PackageInterface $package package instance + * @param string $path package directory + * @return string|null changes or null + */ + public function getLocalChanges(PackageInterface $package, $path); +} diff --git a/src/Composer/Downloader/DownloadManager.php b/src/Composer/Downloader/DownloadManager.php index e52212f76..01ccc822b 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(); } @@ -43,7 +46,8 @@ class DownloadManager /** * Makes downloader prefer source installation over the dist. * - * @param bool $preferSource prefer downloading from source + * @param bool $preferSource prefer downloading from source + * @return DownloadManager */ public function setPreferSource($preferSource) { @@ -55,7 +59,8 @@ class DownloadManager /** * Makes downloader prefer dist installation over the source. * - * @param bool $preferDist prefer downloading from dist + * @param bool $preferDist prefer downloading from dist + * @return DownloadManager */ public function setPreferDist($preferDist) { @@ -83,8 +88,9 @@ class DownloadManager /** * Sets installer downloader for a specific installation type. * - * @param string $type installation type - * @param DownloaderInterface $downloader downloader instance + * @param string $type installation type + * @param DownloaderInterface $downloader downloader instance + * @return DownloadManager */ public function setDownloader($type, DownloaderInterface $downloader) { @@ -97,17 +103,16 @@ class DownloadManager /** * Returns downloader for a specific installation type. * - * @param string $type installation type - * + * @param string $type installation type * @return DownloaderInterface * - * @throws UnexpectedValueException if downloader for provided type is not registered + * @throws \InvalidArgumentException if downloader for provided type is not registered */ public function getDownloader($type) { $type = strtolower($type); if (!isset($this->downloaders[$type])) { - throw new \InvalidArgumentException('Unknown downloader type: '.$type); + throw new \InvalidArgumentException(sprintf('Unknown downloader type: %s. Available types: %s.', $type, implode(', ', array_keys($this->downloaders)))); } return $this->downloaders[$type]; @@ -116,18 +121,21 @@ class DownloadManager /** * Returns downloader for already installed package. * - * @param PackageInterface $package package instance - * - * @return DownloaderInterface + * @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 + * @throws \InvalidArgumentException if package has no installation source specified + * @throws \LogicException if specific downloader used to load package with + * wrong type */ public function getDownloaderForInstalledPackage(PackageInterface $package) { $installationSource = $package->getInstallationSource(); + if ('metapackage' === $package->getType()) { + return; + } + if ('dist' === $installationSource) { $downloader = $this->getDownloader($package->getDistType()); } elseif ('source' === $installationSource) { @@ -155,7 +163,8 @@ class DownloadManager * @param string $targetDir target dir * @param bool $preferSource prefer installation from source * - * @throws InvalidArgumentException if package have no urls to download from + * @throws \InvalidArgumentException if package have no urls to download from + * @throws \RuntimeException */ public function download(PackageInterface $package, $targetDir, $preferSource = null) { @@ -163,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().'' + ); + } + } } /** @@ -184,11 +223,15 @@ class DownloadManager * @param PackageInterface $target target package version * @param string $targetDir target dir * - * @throws InvalidArgumentException if initial package is not installed + * @throws \InvalidArgumentException if initial package is not installed */ public function update(PackageInterface $initial, PackageInterface $target, $targetDir) { $downloader = $this->getDownloaderForInstalledPackage($initial); + if (!$downloader) { + return; + } + $installationSource = $initial->getInstallationSource(); if ('dist' === $installationSource) { @@ -225,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 7f8d8e0b6..82a256b7c 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -17,8 +17,10 @@ use Composer\Cache; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; +use Composer\Plugin\PluginEvents; +use Composer\Plugin\PreFileDownloadEvent; +use Composer\EventDispatcher\EventDispatcher; use Composer\Util\Filesystem; -use Composer\Util\GitHub; use Composer\Util\RemoteFilesystem; /** @@ -27,10 +29,10 @@ use Composer\Util\RemoteFilesystem; * @author Kirill chEbba Chebunin * @author Jordi Boggiano * @author François Pluchino + * @author Nils Adermann */ class FileDownloader implements DownloaderInterface { - private static $cacheCollected = false; protected $io; protected $config; protected $rfs; @@ -41,24 +43,25 @@ class FileDownloader implements DownloaderInterface /** * Constructor. * - * @param IOInterface $io The IO instance - * @param Config $config The config - * @param Cache $cache Optional cache instance - * @param RemoteFilesystem $rfs The remote filesystem - * @param Filesystem $filesystem The filesystem + * @param IOInterface $io The IO instance + * @param Config $config The config + * @param EventDispatcher $eventDispatcher The event dispatcher + * @param Cache $cache Optional cache instance + * @param RemoteFilesystem $rfs The remote filesystem + * @param Filesystem $filesystem The filesystem */ - public function __construct(IOInterface $io, Config $config, Cache $cache = null, RemoteFilesystem $rfs = null, Filesystem $filesystem = null) + public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, RemoteFilesystem $rfs = null, Filesystem $filesystem = null) { $this->io = $io; $this->config = $config; - $this->rfs = $rfs ?: new RemoteFilesystem($io); + $this->eventDispatcher = $eventDispatcher; + $this->rfs = $rfs ?: new RemoteFilesystem($io, $config); $this->filesystem = $filesystem ?: new Filesystem(); $this->cache = $cache; - if ($this->cache && !self::$cacheCollected && !rand(0, 50)) { - $this->cache->gc($config->get('cache-ttl')); + if ($this->cache && $this->cache->gcIsNecessary()) { + $this->cache->gc($config->get('cache-files-ttl'), $config->get('cache-files-maxsize')); } - self::$cacheCollected = true; } /** @@ -74,50 +77,82 @@ 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->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->getMessage()); + } elseif (count($urls)) { + $this->io->write(''); + $this->io->write(' Failed, trying the next URL'); + } - $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); - if (strpos($hostname, '.github.com') === (strlen($hostname) - 11)) { - $hostname = 'github.com'; + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $processedUrl); + if ($this->eventDispatcher) { + $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); } + $rfs = $preFileDownloadEvent->getRemoteFilesystem(); try { - try { - if (!$this->cache || !$this->cache->copyTo($this->getCacheKey($package), $fileName)) { - $this->rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress); - if (!$this->outputProgress) { - $this->io->write(' Downloading'); - } - if ($this->cache) { - $this->cache->copyFrom($this->getCacheKey($package), $fileName); - } - } else { - $this->io->write(' Loading from cache'); + $checksum = $package->getDistSha1Checksum(); + $cacheKey = $this->getCacheKey($package); + + // 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'); } - } catch (TransportException $e) { - if (404 === $e->getCode() && 'github.com' === $hostname) { - $message = "\n".'Could not fetch '.$processedUrl.', enter your GitHub credentials to access private repos'; - $gitHubUtil = new GitHub($this->io, $this->config, null, $this->rfs); - if (!$gitHubUtil->authorizeOAuth($hostname) - && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($hostname, $message)) - ) { - throw $e; + + // 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); } - $this->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)) { @@ -125,7 +160,6 @@ class FileDownloader implements DownloaderInterface .' directory is writable and you have internet connectivity'); } - $checksum = $package->getDistSha1Checksum(); if ($checksum && hash_file('sha1', $fileName) !== $checksum) { throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url.')'); } @@ -135,6 +169,8 @@ class FileDownloader implements DownloaderInterface $this->clearCache($package, $path); throw $e; } + + return $fileName; } /** diff --git a/src/Composer/Downloader/FilesystemException.php b/src/Composer/Downloader/FilesystemException.php new file mode 100644 index 000000000..2e5c6b48d --- /dev/null +++ b/src/Composer/Downloader/FilesystemException.php @@ -0,0 +1,26 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Downloader; + +/** + * Exception thrown when issues exist on local filesystem + * + * @author Javier Spagnoletti + */ +class FilesystemException extends \Exception +{ + public function __construct($message = null, $code = null, \Exception $previous = null) + { + parent::__construct("Filesystem exception: \n".$message, $code, $previous); + } +} diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index de465f3c4..0584f675a 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -14,6 +14,11 @@ 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 @@ -21,64 +26,83 @@ use Composer\Util\GitHub; 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) { + GitUtil::cleanEnv(); + $path = $this->normalizePath($path); + $ref = $package->getSourceReference(); - $command = 'git clone %s %s && cd %2$s && git remote add composer %1$s && git fetch composer'; + $flag = defined('PHP_WINDOWS_VERSION_MAJOR') ? '/D ' : ''; + $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); - // added in git 1.7.1, prevents prompting the user - putenv('GIT_ASKPASS=echo'); - $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); - $this->setPushUrl($package, $path); + $this->gitUtil->runCommand($commandCallable, $url, $path, true); + $this->setPushUrl($path, $url); - $this->updateToCommit($path, $ref, $package->getPrettyVersion(), $package->getReleaseDate()); + if ($newRef = $this->updateToCommit($path, $ref, $package->getPrettyVersion(), $package->getReleaseDate())) { + if ($package->getDistReference() === $package->getSourceReference()) { + $package->setDistReference($newRef); + } + $package->setSourceReference($newRef); + } } /** * {@inheritDoc} */ - public function doUpdate(PackageInterface $initial, PackageInterface $target, $path) + public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { + 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'); + } + $ref = $target->getSourceReference(); $this->io->write(" Checking out ".$ref); - $command = 'cd %s && git remote set-url composer %s && git fetch composer && git fetch --tags composer'; - - if (!$this->io->hasAuthentication('github.com')) { - // capture username/password from github URL if there is one - $this->process->execute(sprintf('cd %s && git remote -v', escapeshellarg($path)), $output); - if (preg_match('{^composer\s+https://(.+):(.+)@github.com/}im', $output, $match)) { - $this->io->setAuthentication('github.com', $match[1], $match[2]); - } - } + $command = 'git remote set-url composer %s && git fetch composer && git fetch --tags composer'; - $commandCallable = function($url) use ($ref, $path, $command) { - return sprintf($command, escapeshellarg($path), escapeshellarg($url), escapeshellarg($ref)); + $commandCallable = function ($url) use ($command) { + return sprintf($command, ProcessExecutor::escape ($url)); }; - $this->runCommand($commandCallable, $target->getSourceUrl()); - $this->updateToCommit($path, $ref, $target->getPrettyVersion(), $target->getReleaseDate()); + $this->gitUtil->runCommand($commandCallable, $url, $path); + if ($newRef = $this->updateToCommit($path, $ref, $target->getPrettyVersion(), $target->getReleaseDate())) { + if ($target->getDistReference() === $target->getSourceReference()) { + $target->setDistReference($newRef); + } + $target->setSourceReference($newRef); + } } /** * {@inheritDoc} */ - public function getLocalChanges($path) + public function getLocalChanges(PackageInterface $package, $path) { + GitUtil::cleanEnv(); + $path = $this->normalizePath($path); if (!is_dir($path.'/.git')) { return; } - $command = sprintf('cd %s && git status --porcelain --untracked-files=no', escapeshellarg($path)); - if (0 !== $this->process->execute($command, $output)) { + $command = 'git status --porcelain --untracked-files=no'; + if (0 !== $this->process->execute($command, $output, $path)) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } @@ -88,14 +112,28 @@ class GitDownloader extends VcsDownloader /** * {@inheritDoc} */ - protected function cleanChanges($path, $update) + protected function cleanChanges(PackageInterface $package, $path, $update) { - if (!$this->io->isInteractive()) { - return parent::cleanChanges($path, $update); + GitUtil::cleanEnv(); + $path = $this->normalizePath($path); + if (!$changes = $this->getLocalChanges($package, $path)) { + return; } - if (!$changes = $this->getLocalChanges($path)) { - return; + if (!$this->io->isInteractive()) { + $discardChanges = $this->config->get('discard-changes'); + if (true === $discardChanges) { + return $this->discardChanges($path); + } + if ('stash' === $discardChanges) { + if (!$update) { + return parent::cleanChanges($package, $path, $update); + } + + return $this->stashChanges($path); + } + + return parent::cleanChanges($package, $path, $update); } $changes = array_map(function ($elem) { @@ -110,9 +148,7 @@ class GitDownloader extends VcsDownloader while (true) { switch ($this->io->ask(' Discard changes [y,n,v,'.($update ? 's,' : '').'?]? ', '?')) { case 'y': - if (0 !== $this->process->execute('git reset --hard', $output, $path)) { - throw new \RuntimeException("Could not reset changes\n\n:".$this->process->getErrorOutput()); - } + $this->discardChanges($path); break 2; case 's': @@ -120,11 +156,7 @@ class GitDownloader extends VcsDownloader goto help; } - if (0 !== $this->process->execute('git stash', $output, $path)) { - throw new \RuntimeException("Could not stash changes\n\n:".$this->process->getErrorOutput()); - } - - $this->hasStashedChanges = true; + $this->stashChanges($path); break 2; case 'n': @@ -156,15 +188,27 @@ class GitDownloader extends VcsDownloader */ protected function reapplyChanges($path) { + $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()); } } } + /** + * Updates the given path to the given commit ref + * + * @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) { $template = 'git checkout %s && git reset --hard %1$s'; @@ -179,9 +223,9 @@ class GitDownloader extends VcsDownloader $gitRef = $reference; if (!preg_match('{^[a-f0-9]{40}$}', $reference) && $branches - && preg_match('{^\s+composer/'.preg_quote($reference).'$}m', $output) + && 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; } @@ -194,19 +238,19 @@ 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; } @@ -225,7 +269,7 @@ class GitDownloader extends VcsDownloader 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)) { + if (0 === $this->process->execute(sprintf($guessTemplate, $date, ProcessExecutor::escape(trim($line))), $output, $path)) { $newReference = trim($output); } @@ -236,137 +280,96 @@ class GitDownloader extends VcsDownloader 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()); + throw new \RuntimeException('Failed to execute ' . GitUtil::sanitizeUrl($command) . "\n\n" . $this->process->getErrorOutput()); } $newReference = trim($output); } // checkout the new recovered ref - $command = sprintf($template, escapeshellarg($reference)); + $command = sprintf($template, ProcessExecutor::escape($newReference)); if (0 === $this->process->execute($command, $output, $path)) { $this->io->write(' '.$reference.' is gone (history was rewritten?), recovered by checking out '.$newReference); - return; + return $newReference; } } - 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 $path The directory to remove for each attempt (null if not needed) - * @throws \RuntimeException - */ - protected function runCommand($commandCallable, $url, $path = null) + protected function setPushUrl($path, $url) { - $handler = array($this, 'outputHandler'); - - 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)(://github.com/.*)}', $url, $match)) { + // set push url for github projects + if (preg_match('{^(?:https?|git)://'.GitUtil::getGitHubDomainsRegex($this->config).'/([^/]+)/([^/]+?)(?:\.git)?$}', $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), $handler)) { - return; - } - $messages[] = '- ' . $url . "\n" . preg_replace('#^#m', ' ', $this->process->getErrorOutput()); - if (null !== $path) { - $this->filesystem->removeDirectory($path); - } + $pushUrl = 'git@'.$match[1].':'.$match[2].'/'.$match[3].'.git'; + if ($protocols[0] !== 'git') { + $pushUrl = 'https://' . $match[1] . '/'.$match[2].'/'.$match[3].'.git'; } - - // failed to checkout, first check git accessibility - $this->throwException('Failed to clone ' . $this->sanitizeUrl($url) .' via git, https and http protocols, aborting.' . "\n\n" . implode("\n", $messages), $url); + $cmd = sprintf('git remote set-url --push origin %s', ProcessExecutor::escape($pushUrl)); + $this->process->execute($cmd, $ignoredOutput, $path); } + } - $command = call_user_func($commandCallable, $url); - if (0 !== $this->process->execute($command, $handler)) { - // private github repository without git access, try https with auth - if (preg_match('{^git@(github.com):(.+?)\.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://'.$auth['username'] . ':' . $auth['password'] . '@'.$match[1].'/'.$match[2].'.git'; - - $command = call_user_func($commandCallable, $url); - if (0 === $this->process->execute($command, $handler)) { - return; - } - } - } + /** + * {@inheritDoc} + */ + protected function getCommitLogs($fromReference, $toReference, $path) + { + $path = $this->normalizePath($path); + $command = sprintf('git log %s..%s --pretty=format:"%%h - %%an: %%s"', $fromReference, $toReference); - if (null !== $path) { - $this->filesystem->removeDirectory($path); - } - $this->throwException('Failed to execute ' . $this->sanitizeUrl($command) . "\n\n" . $this->process->getErrorOutput(), $url); + if (0 !== $this->process->execute($command, $output, $path)) { + throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } + + return $output; } - public function outputHandler($type, $buffer) + /** + * @param $path + * @throws \RuntimeException + */ + protected function discardChanges($path) { - if ($type !== 'out') { - return; - } - if ($this->io->isVerbose()) { - $this->io->write($buffer, false); + $path = $this->normalizePath($path); + if (0 !== $this->process->execute('git reset --hard', $output, $path)) { + throw new \RuntimeException("Could not reset changes\n\n:".$this->process->getErrorOutput()); } } - protected function throwException($message, $url) + /** + * @param $path + * @throws \RuntimeException + */ + protected function stashChanges($path) { - 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()); + $path = $this->normalizePath($path); + if (0 !== $this->process->execute('git stash', $output, $path)) { + throw new \RuntimeException("Could not stash changes\n\n:".$this->process->getErrorOutput()); } - throw new \RuntimeException($message); + $this->hasStashedChanges = true; } - protected function sanitizeUrl($message) + protected function normalizePath($path) { - return preg_replace('{://(.+?):.+?@}', '://$1:***@', $message); - } + if (defined('PHP_WINDOWS_VERSION_MAJOR') && strlen($path) > 0) { + $basePath = $path; + $removed = array(); - protected function setPushUrl(PackageInterface $package, $path) - { - // set push url for github projects - if (preg_match('{^(?:https?|git)://github.com/([^/]+)/([^/]+?)(?:\.git)?$}', $package->getSourceUrl(), $match)) { - $pushUrl = 'git@github.com:'.$match[1].'/'.$match[2].'.git'; - $cmd = sprintf('git remote set-url --push origin %s', escapeshellarg($pushUrl)); - $this->process->execute($cmd, $ignoredOutput, $path); - } - } + while (!is_dir($basePath) && $basePath !== '\\') { + array_unshift($removed, basename($basePath)); + $basePath = dirname($basePath); + } - /** - * {@inheritDoc} - */ - protected function getCommitLogs($fromReference, $toReference, $path) - { - $command = sprintf('cd %s && git log %s..%s --pretty=format:"%%h - %%an: %%s"', escapeshellarg($path), $fromReference, $toReference); + if ($basePath === '\\') { + return $path; + } - if (0 !== $this->process->execute($command, $output)) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); + $path = rtrim(realpath($basePath) . '/' . implode('/', $removed), '/'); } - return $output; + return $path; } } 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 a3ee59788..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,29 +23,36 @@ 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()); - $path = escapeshellarg($path); + $url = ProcessExecutor::escape($url); + $ref = ProcessExecutor::escape($package->getSourceReference()); $this->io->write(" Cloning ".$package->getSourceReference()); - $command = sprintf('hg clone %s %s && cd %2$s && hg up %s', $url, $path, $ref); + $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()); } + $command = sprintf('hg up %s', $ref); + if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { + throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); + } } /** * {@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()); - $path = escapeshellarg($path); + $url = ProcessExecutor::escape($url); + $ref = ProcessExecutor::escape($target->getSourceReference()); $this->io->write(" Updating to ".$target->getSourceReference()); - $command = sprintf('cd %s && hg pull %s && hg up %s', $path, $url, $ref); - if (0 !== $this->process->execute($command, $ignoredOutput)) { + + if (!is_dir($path.'/.hg')) { + throw new \RuntimeException('The .hg directory is missing from '.$path.', see http://getcomposer.org/commit-deps for more information'); + } + + $command = sprintf('hg pull %s && hg up %s', $url, $ref); + if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } } @@ -52,13 +60,13 @@ class HgDownloader extends VcsDownloader /** * {@inheritDoc} */ - public function getLocalChanges($path) + public function getLocalChanges(PackageInterface $package, $path) { if (!is_dir($path.'/.hg')) { return; } - $this->process->execute(sprintf('cd %s && hg st', escapeshellarg($path)), $output); + $this->process->execute('hg st', $output, realpath($path)); return trim($output) ?: null; } @@ -68,9 +76,9 @@ class HgDownloader extends VcsDownloader */ protected function getCommitLogs($fromReference, $toReference, $path) { - $command = sprintf('cd %s && hg log -r %s:%s --style compact', escapeshellarg($path), $fromReference, $toReference); + $command = sprintf('hg log -r %s:%s --style compact', $fromReference, $toReference); - if (0 !== $this->process->execute($command, $output)) { + if (0 !== $this->process->execute($command, $output, realpath($path))) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } diff --git a/src/Composer/Downloader/PearPackageExtractor.php b/src/Composer/Downloader/PearPackageExtractor.php index 4cd7fea02..3ed3ed7be 100644 --- a/src/Composer/Downloader/PearPackageExtractor.php +++ b/src/Composer/Downloader/PearPackageExtractor.php @@ -127,11 +127,11 @@ 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) @@ -194,7 +194,7 @@ class PearPackageExtractor } } - private function buildSourceList10($children, $targetRoles, $source = '', $target = '', $role = null, $packageName) + private function buildSourceList10($children, $targetRoles, $source, $target, $role, $packageName) { $result = array(); @@ -224,7 +224,7 @@ class PearPackageExtractor return $result; } - private function buildSourceList20($children, $targetRoles, $source = '', $target = '', $role = null, $packageName) + private function buildSourceList20($children, $targetRoles, $source, $target, $role, $packageName) { $result = array(); diff --git a/src/Composer/Downloader/PerforceDownloader.php b/src/Composer/Downloader/PerforceDownloader.php new file mode 100644 index 000000000..683ea9f34 --- /dev/null +++ b/src/Composer/Downloader/PerforceDownloader.php @@ -0,0 +1,107 @@ + + * 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\Package\PackageInterface; +use Composer\Repository\VcsRepository; +use Composer\Util\Perforce; + +/** + * @author Matt Whittom + */ +class PerforceDownloader extends VcsDownloader +{ + protected $perforce; + + /** + * {@inheritDoc} + */ + public function doDownload(PackageInterface $package, $path, $url) + { + $ref = $package->getSourceReference(); + $label = $this->getLabelFromSourceReference($ref); + + $this->io->write(' Cloning ' . $ref); + $this->initPerforce($package, $path, $url); + $this->perforce->setStream($ref); + $this->perforce->p4Login($this->io); + $this->perforce->writeP4ClientSpec(); + $this->perforce->connectClient(); + $this->perforce->syncCodeBase($label); + $this->perforce->cleanupClientSpec(); + } + + private function getLabelFromSourceReference($ref) + { + $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; + } + + $repository = $package->getRepository(); + $repoConfig = null; + if ($repository instanceof VcsRepository) { + $repoConfig = $this->getRepoConfig($repository); + } + $this->perforce = Perforce::create($repoConfig, $url, $path, $this->process, $this->io); + } + + private function getRepoConfig(VcsRepository $repository) + { + return $repository->getRepoConfig(); + } + + /** + * {@inheritDoc} + */ + public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) + { + $this->doDownload($target, $path, $url); + } + + /** + * {@inheritDoc} + */ + public function getLocalChanges(PackageInterface $package, $path) + { + $this->io->write('Perforce driver does not check for local changes before overriding', true); + + return; + } + + /** + * {@inheritDoc} + */ + protected function getCommitLogs($fromReference, $toReference, $path) + { + $commitLogs = $this->perforce->getCommitLogs($fromReference, $toReference); + + return $commitLogs; + } + + public function setPerforce($perforce) + { + $this->perforce = $perforce; + } +} diff --git a/src/Composer/Downloader/RarDownloader.php b/src/Composer/Downloader/RarDownloader.php new file mode 100644 index 000000000..12823422d --- /dev/null +++ b/src/Composer/Downloader/RarDownloader.php @@ -0,0 +1,94 @@ + + * 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\Util\ProcessExecutor; +use Composer\IO\IOInterface; +use RarArchive; + +/** + * RAR archive downloader. + * + * Based on previous work by Jordi Boggiano ({@see ZipDownloader}). + * + * @author Derrick Nelson + */ +class RarDownloader 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) + { + $processError = null; + + // Try to use unrar on *nix + if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + $command = 'unrar x ' . ProcessExecutor::escape($file) . ' ' . ProcessExecutor::escape($path) . ' && chmod -R u+w ' . ProcessExecutor::escape($path); + + if (0 === $this->process->execute($command, $ignoredOutput)) { + return; + } + + $processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); + } + + if (!class_exists('RarArchive')) { + // php.ini path is added to the error message to help users find the correct file + $iniPath = php_ini_loaded_file(); + + if ($iniPath) { + $iniMessage = 'The php.ini used by your command-line PHP is: ' . $iniPath; + } else { + $iniMessage = 'A php.ini file does not exist. You will have to create one.'; + } + + $error = "Could not decompress the archive, enable the PHP rar extension or install unrar.\n" + . $iniMessage . "\n" . $processError; + + if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + $error = "Could not decompress the archive, enable the PHP rar extension.\n" . $iniMessage; + } + + throw new \RuntimeException($error); + } + + $rarArchive = RarArchive::open($file); + + if (false === $rarArchive) { + throw new \UnexpectedValueException('Could not open RAR archive: ' . $file); + } + + $entries = $rarArchive->getEntries(); + + if (false === $entries) { + throw new \RuntimeException('Could not retrieve RAR archive entries'); + } + + foreach ($entries as $entry) { + if (false === $entry->extract($path)) { + throw new \RuntimeException('Could not extract entry'); + } + } + + $rarArchive->close(); + } +} diff --git a/src/Composer/Downloader/SvnDownloader.php b/src/Composer/Downloader/SvnDownloader.php index 48f6da741..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,19 +36,30 @@ 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); } /** * {@inheritDoc} */ - public function getLocalChanges($path) + public function getLocalChanges(PackageInterface $package, $path) { if (!is_dir($path.'/.svn')) { return; @@ -63,17 +74,17 @@ class SvnDownloader extends VcsDownloader * Execute an SVN command and try to fix up the process with credentials * if necessary. * - * @param string $baseUrl Base URL of the repository - * @param string $command SVN command to run - * @param string $url SVN url - * @param string $cwd Working directory - * @param string $path Target for a checkout - * + * @param string $baseUrl Base URL of the repository + * @param string $command SVN command to run + * @param string $url SVN url + * @param string $cwd Working directory + * @param string $path Target for a checkout + * @throws \RuntimeException * @return string */ 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) { @@ -86,14 +97,18 @@ class SvnDownloader extends VcsDownloader /** * {@inheritDoc} */ - protected function cleanChanges($path, $update) + protected function cleanChanges(PackageInterface $package, $path, $update) { - if (!$this->io->isInteractive()) { - return parent::cleanChanges($path, $update); + if (!$changes = $this->getLocalChanges($package, $path)) { + return; } - if (!$changes = $this->getLocalChanges($path)) { - return; + if (!$this->io->isInteractive()) { + if (true === $this->config->get('discard-changes')) { + return $this->discardChanges($path); + } + + return parent::cleanChanges($package, $path, $update); } $changes = array_map(function ($elem) { @@ -108,9 +123,7 @@ class SvnDownloader extends VcsDownloader while (true) { switch ($this->io->ask(' Discard changes [y,n,v,?]? ', '?')) { case 'y': - if (0 !== $this->process->execute('svn revert -R .', $output, $path)) { - throw new \RuntimeException("Could not reset changes\n\n:".$this->process->getErrorOutput()); - } + $this->discardChanges($path); break 2; case 'n': @@ -138,16 +151,29 @@ 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('cd %s && svn log -r%s:%s --incremental', escapeshellarg($path), $fromRevision, $toRevision); + $command = sprintf('svn log -r%s:%s --incremental', $fromRevision, $toRevision); - if (0 !== $this->process->execute($command, $output)) { - 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; } + + protected function discardChanges($path) + { + if (0 !== $this->process->execute('svn revert -R .', $output, $path)) { + throw new \RuntimeException("Could not reset changes\n\n:".$this->process->getErrorOutput()); + } + } } 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 55a189b7b..e653794ca 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -22,7 +22,7 @@ use Composer\Util\Filesystem; /** * @author Jordi Boggiano */ -abstract class VcsDownloader implements DownloaderInterface +abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterface { protected $io; protected $config; @@ -33,7 +33,7 @@ abstract class VcsDownloader implements DownloaderInterface { $this->io = $io; $this->config = $config; - $this->process = $process ?: new ProcessExecutor; + $this->process = $process ?: new ProcessExecutor($io); $this->filesystem = $fs ?: new Filesystem; } @@ -55,8 +55,28 @@ abstract class VcsDownloader implements DownloaderInterface } $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(''); } @@ -86,18 +106,32 @@ abstract class VcsDownloader implements DownloaderInterface $this->io->write(" - Updating " . $name . " (" . $from . " => " . $to . ")"); - $this->cleanChanges($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; + $this->cleanChanges($initial, $path, true); + $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); @@ -126,7 +160,7 @@ abstract class VcsDownloader implements DownloaderInterface public function remove(PackageInterface $package, $path) { $this->io->write(" - Removing " . $package->getName() . " (" . $package->getPrettyVersion() . ")"); - $this->cleanChanges($path, false); + $this->cleanChanges($package, $path, false); if (!$this->filesystem->removeDirectory($path)) { throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); } @@ -144,15 +178,16 @@ abstract class VcsDownloader implements DownloaderInterface /** * Prompt the user to check if changes should be stashed/removed or the operation aborted * - * @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($path, $update) + protected function cleanChanges(PackageInterface $package, $path, $update) { // the default implementation just fails if there are any changes, override in child classes to provide stash-ability - if (null !== $this->getLocalChanges($path)) { + if (null !== $this->getLocalChanges($package, $path)) { throw new \RuntimeException('Source directory ' . $path . ' has uncommitted changes.'); } } @@ -172,8 +207,9 @@ abstract class VcsDownloader implements DownloaderInterface * * @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. @@ -181,16 +217,9 @@ abstract class VcsDownloader implements DownloaderInterface * @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); - - /** - * Checks for changes to the local copy - * - * @param string $path package directory - * @return string|null changes or null - */ - abstract public function getLocalChanges($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 7a9fcdc92..1370d82af 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -14,6 +14,7 @@ namespace Composer\Downloader; use Composer\Config; use Composer\Cache; +use Composer\EventDispatcher\EventDispatcher; use Composer\Util\ProcessExecutor; use Composer\IO\IOInterface; use ZipArchive; @@ -25,14 +26,30 @@ class ZipDownloader extends ArchiveDownloader { protected $process; - public function __construct(IOInterface $io, Config $config, Cache $cache = null, ProcessExecutor $process = null) + public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) { - $this->process = $process ?: new ProcessExecutor; - parent::__construct($io, $config, $cache); + $this->process = $process ?: new ProcessExecutor($io); + parent::__construct($io, $config, $eventDispatcher, $cache); } protected function extract($file, $path) { + $processError = null; + + // try to use unzip on *nix + if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + $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(); + } + } + if (!class_exists('ZipArchive')) { // php.ini path is added to the error message to help users find the correct file $iniPath = php_ini_loaded_file(); @@ -43,19 +60,11 @@ class ZipDownloader extends ArchiveDownloader $iniMessage = 'A php.ini file does not exist. You will have to create one.'; } - $error = "You need the zip extension enabled to use the ZipDownloader.\n". - $iniMessage; + $error = "Could not decompress the archive, enable the PHP zip extension or install unzip.\n" + . $iniMessage . "\n" . $processError; - // try to use unzip on *nix if (!defined('PHP_WINDOWS_VERSION_BUILD')) { - $command = 'unzip '.escapeshellarg($file).' -d '.escapeshellarg($path); - if (0 === $this->process->execute($command, $ignoredOutput)) { - return; - } - - $error = "Could not decompress the archive, enable the PHP zip extension or install unzip.\n". - $iniMessage . "\n" . - 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); + $error = "Could not decompress the archive, enable the PHP zip extension.\n" . $iniMessage; } throw new \RuntimeException($error); @@ -64,7 +73,7 @@ class ZipDownloader extends ArchiveDownloader $zipArchive = new ZipArchive(); if (true !== ($retval = $zipArchive->open($file))) { - throw new \UnexpectedValueException($this->getErrorMessage($retval, $file)); + throw new \UnexpectedValueException($this->getErrorMessage($retval, $file), $retval); } if (true !== $zipArchive->extractTo($path)) { diff --git a/src/Composer/EventDispatcher/Event.php b/src/Composer/EventDispatcher/Event.php new file mode 100644 index 000000000..b9ebeb029 --- /dev/null +++ b/src/Composer/EventDispatcher/Event.php @@ -0,0 +1,103 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\EventDispatcher; + +/** + * The base event class + * + * @author Nils Adermann + */ +class Event +{ + /** + * @var string This event's name + */ + 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 + */ + private $propagationStopped = false; + + /** + * Constructor. + * + * @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, array $args = array(), array $flags = array()) + { + $this->name = $name; + $this->args = $args; + $this->flags = $flags; + } + + /** + * Returns the event's name. + * + * @return string The event name + */ + public function getName() + { + 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 + * + * @return boolean Whether propagation has been stopped + */ + public function isPropagationStopped() + { + return $this->propagationStopped; + } + + /** + * Prevents the event from being passed to further listeners + */ + public function stopPropagation() + { + $this->propagationStopped = true; + } +} diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php new file mode 100644 index 000000000..3026d3c9c --- /dev/null +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -0,0 +1,314 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +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; +use Composer\Util\ProcessExecutor; + +/** + * The Event Dispatcher. + * + * Example in command: + * $dispatcher = new EventDispatcher($this->getComposer(), $this->getApplication()->getIO()); + * // ... + * $dispatcher->dispatch(ScriptEvents::POST_INSTALL_CMD); + * // ... + * + * @author François Pluchino + * @author Jordi Boggiano + * @author Nils Adermann + */ +class EventDispatcher +{ + protected $composer; + protected $io; + protected $loader; + protected $process; + + /** + * Constructor. + * + * @param Composer $composer The composer instance + * @param IOInterface $io The IOInterface instance + * @param ProcessExecutor $process + */ + public function __construct(Composer $composer, IOInterface $io, ProcessExecutor $process = null) + { + $this->composer = $composer; + $this->io = $io; + $this->process = $process ?: new ProcessExecutor($io); + } + + /** + * Dispatch an 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) + { + if (null == $event) { + $event = new Event($eventName); + } + + return $this->doDispatch($event); + } + + /** + * Dispatch a script 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, $devMode = false, $additionalArgs = array(), $flags = array()) + { + 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 + * @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) + { + 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 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 dispatchInstallerEvent($eventName, PolicyInterface $policy, Pool $pool, CompositeRepository $installedRepo, Request $request, array $operations = array()) + { + 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 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 + */ + protected function doDispatch(Event $event) + { + $listeners = $this->getListeners($event); + + $return = 0; + foreach ($listeners as $callable) { + if (!is_string($callable) && is_callable($callable)) { + $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); + + if (!class_exists($className)) { + $this->io->write('Class '.$className.' is not autoloadable, can not call '.$event->getName().' script'); + continue; + } + if (!is_callable($callable)) { + $this->io->write('Method '.$callable.' is not callable, can not call '.$event->getName().' script'); + continue; + } + + try { + $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 { + $args = implode(' ', array_map(array('Composer\Util\ProcessExecutor','escape'), $event->getArguments())); + if (0 !== ($exitCode = $this->process->execute($callable . ($args === '' ? '' : ' '.$args)))) { + $event->getIO()->write(sprintf('Script %s handling the %s event returned with an error', $callable, $event->getName())); + + throw new \RuntimeException('Error Output: '.$this->process->getErrorOutput(), $exitCode); + } + } + + if ($event->isPropagationStopped()) { + break; + } + } + + return $return; + } + + /** + * @param string $className + * @param string $methodName + * @param Event $event Event invoking the PHP callable + */ + protected function executeEventPhpScript($className, $methodName, Event $event) + { + return $className::$methodName($event); + } + + /** + * Add a listener for a particular event + * + * @param string $eventName The event name - typically a constant + * @param Callable $listener A callable expecting an event argument + * @param integer $priority A higher value represents a higher priority + */ + protected function addListener($eventName, $listener, $priority = 0) + { + $this->listeners[$eventName][$priority][] = $listener; + } + + /** + * Adds object methods as listeners for the events in getSubscribedEvents + * + * @see EventSubscriberInterface + * + * @param EventSubscriberInterface $subscriber + */ + public function addSubscriber(EventSubscriberInterface $subscriber) + { + foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { + if (is_string($params)) { + $this->addListener($eventName, array($subscriber, $params)); + } elseif (is_string($params[0])) { + $this->addListener($eventName, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0); + } else { + foreach ($params as $listener) { + $this->addListener($eventName, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0); + } + } + } + } + + /** + * Retrieves all listeners for a given event + * + * @param Event $event + * @return array All listeners: callables and scripts + */ + protected function getListeners(Event $event) + { + $scriptListeners = $this->getScriptListeners($event); + + if (!isset($this->listeners[$event->getName()][0])) { + $this->listeners[$event->getName()][0] = array(); + } + krsort($this->listeners[$event->getName()]); + + $listeners = $this->listeners; + $listeners[$event->getName()][0] = array_merge($listeners[$event->getName()][0], $scriptListeners); + + 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 + * + * @param Event $event Event object + * @return array Listeners + */ + protected function getScriptListeners(Event $event) + { + $package = $this->composer->getPackage(); + $scripts = $package->getScripts(); + + if (empty($scripts[$event->getName()])) { + return array(); + } + + if ($this->loader) { + $this->loader->unregister(); + } + + $generator = $this->composer->getAutoloadGenerator(); + $packages = $this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages(); + $packageMap = $generator->buildPackageMap($this->composer->getInstallationManager(), $package, $packages); + $map = $generator->parseAutoloads($packageMap, $package); + $this->loader = $generator->createLoader($map); + $this->loader->register(); + + return $scripts[$event->getName()]; + } + + /** + * Checks if string given references a class path and method + * + * @param string $callable + * @return boolean + */ + protected function isPhpScript($callable) + { + return false === strpos($callable, ' ') && false !== strpos($callable, '::'); + } +} diff --git a/src/Composer/EventDispatcher/EventSubscriberInterface.php b/src/Composer/EventDispatcher/EventSubscriberInterface.php new file mode 100644 index 000000000..6b0c4ca06 --- /dev/null +++ b/src/Composer/EventDispatcher/EventSubscriberInterface.php @@ -0,0 +1,48 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\EventDispatcher; + +/** + * An EventSubscriber knows which events it is interested in. + * + * If an EventSubscriber is added to an EventDispatcher, the manager invokes + * {@link getSubscribedEvents} and registers the subscriber as a listener for all + * returned events. + * + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Bernhard Schussek + */ +interface EventSubscriberInterface +{ + /** + * Returns an array of event names this subscriber wants to listen to. + * + * The array keys are event names and the value can be: + * + * * The method name to call (priority defaults to 0) + * * An array composed of the method name to call and the priority + * * An array of arrays composed of the method names to call and respective + * priorities, or 0 if unset + * + * For instance: + * + * * array('eventName' => 'methodName') + * * array('eventName' => array('methodName', $priority)) + * * array('eventName' => array(array('methodName1', $priority), array('methodName2')) + * + * @return array The event names to listen to + */ + public static function getSubscribedEvents(); +} diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 2dbf52dfa..10a3c3cab 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -15,11 +15,16 @@ namespace Composer; use Composer\Config\JsonConfigSource; use Composer\Json\JsonFile; use Composer\IO\IOInterface; -use Composer\Repository\ComposerRepository; +use Composer\Package\Archiver; use Composer\Repository\RepositoryManager; +use Composer\Repository\RepositoryInterface; use Composer\Util\ProcessExecutor; use Composer\Util\RemoteFilesystem; +use Composer\Util\Filesystem; use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Composer\EventDispatcher\EventDispatcher; +use Composer\Autoload\AutoloadGenerator; +use Composer\Package\Version\VersionParser; /** * Creates a configured instance of composer. @@ -27,21 +32,29 @@ use Symfony\Component\Console\Formatter\OutputFormatterStyle; * @author Ryan Weaver * @author Jordi Boggiano * @author Igor Wiedler + * @author Nils Adermann */ class Factory { /** - * @return Config + * @return string + * @throws \RuntimeException */ - public static function createConfig() + protected static function getHomeDir() { - // determine home and cache dirs $home = getenv('COMPOSER_HOME'); $cacheDir = getenv('COMPOSER_CACHE_DIR'); + if (!getenv('HOME')) { + throw new \RuntimeException('The HOME or COMPOSER_HOME environment variable must be set for composer to run correctly'); + } $userDir = rtrim(getenv('HOME'), '/'); $followXDG = false; + if (!$home) { if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + if (!getenv('APPDATA')) { + throw new \RuntimeException('The APPDATA or COMPOSER_HOME environment variable must be set for composer to run correctly'); + } $home = getenv('APPDATA') . '/Composer'; } elseif (getenv('XDG_CONFIG_DIRS')) { // XDG Base Directory Specifications @@ -55,13 +68,27 @@ class Factory $home = $userDir . '/.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')) { $cacheDir .= '/Composer'; } else { - $cacheDir = getenv('APPDATA') . '/Composer/cache'; + $cacheDir = $home . '/cache'; } + + $cacheDir = strtr($cacheDir, '\\', '/'); } elseif (getenv('XDG_CONFIG_DIRS')) { $followXDG = true; $xdgCache = getenv('XDG_CACHE_HOME'); @@ -69,13 +96,24 @@ class Factory $xdgCache = $userDir . '/.cache'; } $cacheDir = $xdgCache . '/composer'; - - } else { $cacheDir = $home . '/.cache'; } } - + + return $cacheDir; + } + + /** + * @param IOInterface|null $io + * @return Config + */ + public static function createConfig(IOInterface $io = null) + { + // 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 @@ -106,48 +144,32 @@ class Factory // 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)); - } - } - @unlink($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; } public static function getComposerFile() { - return getenv('COMPOSER') ?: 'composer.json'; + return trim(getenv('COMPOSER')) ?: './composer.json'; } public static function createAdditionalStyles() @@ -163,7 +185,7 @@ class Factory $repos = array(); if (!$config) { - $config = static::createConfig(); + $config = static::createConfig($io); } if (!$rm) { if (!$io) { @@ -193,13 +215,15 @@ 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 * @throws \InvalidArgumentException + * @throws \UnexpectedValueException * @return Composer */ - public function createComposer(IOInterface $io, $localConfig = null) + public function createComposer(IOInterface $io, $localConfig = null, $disablePlugins = false) { // load Composer configuration if (null === $localConfig) { @@ -211,7 +235,7 @@ class Factory $file = new JsonFile($localConfig, new RemoteFilesystem($io)); if (!$file->exists()) { - if ($localConfig === 'composer.json') { + if ($localConfig === './composer.json' || $localConfig === 'composer.json') { $message = 'Composer could not find a composer.json file in '.getcwd(); } else { $message = 'Composer could not find the config file: '.$localConfig; @@ -224,53 +248,79 @@ class Factory $localConfig = $file->read(); } - // Configuration defaults - $config = static::createConfig(); + // Load config and override with local config/auth config + $config = static::createConfig($io); $config->merge($localConfig); - - // reload oauth token from config if available - if ($tokens = $config->get('github-oauth')) { - foreach ($tokens as $domain => $token) { - if (!preg_match('{^[a-z0-9]+$}', $token)) { - throw new \UnexpectedValueException('Your github oauth token for '.$domain.' contains invalid characters: "'.$token.'"'); + 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()); } - $io->setAuthentication($domain, $token, 'x-oauth-basic'); + $config->merge(array('config' => $localAuthFile->read())); + $config->setAuthConfigSource(new JsonConfigSource($localAuthFile, true)); } } + // load auth configs into the IO instance + $io->loadConfiguration($config); + $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); + + // initialize event dispatcher + $dispatcher = new EventDispatcher($composer, $io); + // initialize repository manager - $rm = $this->createRepositoryManager($io, $config); + $rm = $this->createRepositoryManager($io, $config, $dispatcher); // load local repository $this->addLocalRepository($rm, $vendorDir); // load package - $loader = new Package\Loader\RootPackageLoader($rm, $config); + $parser = new VersionParser; + $loader = new Package\Loader\RootPackageLoader($rm, $config, $parser, new ProcessExecutor($io)); $package = $loader->load($localConfig); - // initialize download manager - $dm = $this->createDownloadManager($io, $config); - // initialize installation manager $im = $this->createInstallationManager(); - // initialize composer - $composer = new Composer(); - $composer->setConfig($config); + // Composer composition $composer->setPackage($package); $composer->setRepositoryManager($rm); - $composer->setDownloadManager($dm); $composer->setInstallationManager($im); + // initialize download manager + $dm = $this->createDownloadManager($io, $config, $dispatcher); + + $composer->setDownloadManager($dm); + $composer->setEventDispatcher($dispatcher); + + // initialize autoload generator + $generator = new AutoloadGenerator($dispatcher, $io); + $composer->setAutoloadGenerator($generator); + // add installers to the manager $this->createDefaultInstallers($im, $composer, $io); + $globalRepository = $this->createGlobalRepository($config, $vendorDir); + $pm = $this->createPluginManager($composer, $io, $globalRepository); + $composer->setPluginManager($pm); + + if (!$disablePlugins) { + $pm->loadInstalledPlugins(); + } + // purge packages if they have been deleted on the filesystem $this->purgePackages($rm, $im); @@ -279,7 +329,7 @@ class Factory $lockFile = "json" === pathinfo($composerFile, PATHINFO_EXTENSION) ? substr($composerFile, 0, -4).'lock' : $composerFile . '.lock'; - $locker = new Package\Locker(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); } @@ -289,18 +339,21 @@ class Factory /** * @param IOInterface $io * @param Config $config + * @param EventDispatcher $eventDispatcher * @return Repository\RepositoryManager */ - protected function createRepositoryManager(IOInterface $io, Config $config) + protected function createRepositoryManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null) { - $rm = new RepositoryManager($io, $config); + $rm = new RepositoryManager($io, $config, $eventDispatcher); $rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository'); $rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository'); $rm->setRepositoryClass('pear', 'Composer\Repository\PearRepository'); $rm->setRepositoryClass('git', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('svn', 'Composer\Repository\VcsRepository'); + $rm->setRepositoryClass('perforce', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('hg', 'Composer\Repository\VcsRepository'); + $rm->setRepositoryClass('artifact', 'Composer\Repository\ArtifactRepository'); return $rm; } @@ -312,33 +365,99 @@ class Factory protected function addLocalRepository(RepositoryManager $rm, $vendorDir) { $rm->setLocalRepository(new Repository\InstalledFilesystemRepository(new JsonFile($vendorDir.'/composer/installed.json'))); - $rm->setLocalDevRepository(new Repository\InstalledFilesystemRepository(new JsonFile($vendorDir.'/composer/installed_dev.json'))); + } + + /** + * @param Config $config + * @param string $vendorDir + * @return Repository\InstalledFilesystemRepository|null + */ + protected function createGlobalRepository(Config $config, $vendorDir) + { + if ($config->get('home') == $vendorDir) { + return null; + } + + $path = $config->get('home').'/vendor/composer/installed.json'; + if (!file_exists($path)) { + return null; + } + + return new Repository\InstalledFilesystemRepository(new JsonFile($path)); } /** - * @param IO\IOInterface $io - * @param Config $config + * @param IO\IOInterface $io + * @param Config $config + * @param EventDispatcher $eventDispatcher * @return Downloader\DownloadManager */ - public function createDownloadManager(IOInterface $io, Config $config) + public function createDownloadManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null) { $cache = null; if ($config->get('cache-files-ttl') > 0) { $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); + break; + case 'source': + $dm->setPreferSource(true); + break; + case 'auto': + default: + // noop + break; + } + $dm->setDownloader('git', new Downloader\GitDownloader($io, $config)); $dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config)); $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config)); - $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $cache)); - $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $cache)); - $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $cache)); - $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $cache)); + $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config)); + $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)); return $dm; } + /** + * @param Config $config The configuration + * @param Downloader\DownloadManager $dm Manager use to download sources + * + * @return Archiver\ArchiveManager + */ + public function createArchiveManager(Config $config, Downloader\DownloadManager $dm = null) + { + if (null === $dm) { + $io = new IO\NullIO(); + $io->loadConfiguration($config); + $dm = $this->createDownloadManager($io, $config); + } + + $am = new Archiver\ArchiveManager($dm); + $am->addArchiver(new Archiver\PharArchiver); + + return $am; + } + + /** + * @param Composer $composer + * @param IOInterface $io + * @param RepositoryInterface $globalRepository + * @return Plugin\PluginManager + */ + protected function createPluginManager(Composer $composer, IOInterface $io, RepositoryInterface $globalRepository = null) + { + return new Plugin\PluginManager($composer, $io, $globalRepository); + } + /** * @return Installer\InstallationManager */ @@ -356,7 +475,7 @@ class Factory { $im->addInstaller(new Installer\LibraryInstaller($io, $composer, null)); $im->addInstaller(new Installer\PearInstaller($io, $composer, 'pear-library')); - $im->addInstaller(new Installer\InstallerInstaller($io, $composer)); + $im->addInstaller(new Installer\PluginInstaller($io, $composer)); $im->addInstaller(new Installer\MetapackageInstaller($io)); } @@ -366,26 +485,25 @@ class Factory */ protected function purgePackages(Repository\RepositoryManager $rm, Installer\InstallationManager $im) { - foreach ($rm->getLocalRepositories() as $repo) { - /* @var $repo Repository\WritableRepositoryInterface */ - foreach ($repo->getPackages() as $package) { - if (!$im->isPackageInstalled($repo, $package)) { - $repo->removePackage($package); - } + $repo = $rm->getLocalRepository(); + foreach ($repo->getPackages() as $package) { + if (!$im->isPackageInstalled($repo, $package)) { + $repo->removePackage($package); } } } /** - * @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 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) + public static function create(IOInterface $io, $config = null, $disablePlugins = false) { $factory = new static(); - return $factory->createComposer($io, $config); + return $factory->createComposer($io, $config, $disablePlugins); } } diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php new file mode 100644 index 000000000..8d684833e --- /dev/null +++ b/src/Composer/IO/BaseIO.php @@ -0,0 +1,79 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\IO; + +use Composer\Config; + +abstract class BaseIO implements IOInterface +{ + protected $authentications = array(); + + /** + * {@inheritDoc} + */ + public function getAuthentications() + { + return $this->authentications; + } + + /** + * {@inheritDoc} + */ + public function hasAuthentication($repositoryName) + { + return isset($this->authentications[$repositoryName]); + } + + /** + * {@inheritDoc} + */ + public function getAuthentication($repositoryName) + { + if (isset($this->authentications[$repositoryName])) { + return $this->authentications[$repositoryName]; + } + + return array('username' => null, 'password' => null); + } + + /** + * {@inheritDoc} + */ + public function setAuthentication($repositoryName, $username, $password = null) + { + $this->authentications[$repositoryName] = array('username' => $username, 'password' => $password); + } + + /** + * {@inheritDoc} + */ + public function loadConfiguration(Config $config) + { + // reload oauth token from config if available + if ($tokens = $config->get('github-oauth')) { + foreach ($tokens as $domain => $token) { + if (!preg_match('{^[a-z0-9]+$}', $token)) { + throw new \UnexpectedValueException('Your github oauth token for '.$domain.' contains invalid characters: "'.$token.'"'); + } + $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/BufferIO.php b/src/Composer/IO/BufferIO.php index 8e1818a97..fc675ce70 100644 --- a/src/Composer/IO/BufferIO.php +++ b/src/Composer/IO/BufferIO.php @@ -23,8 +23,9 @@ use Symfony\Component\Console\Helper\HelperSet; class BufferIO extends ConsoleIO { /** - * @param string $input - * @param int $verbosity + * @param string $input + * @param int $verbosity + * @param OutputFormatterInterface $formatter */ public function __construct($input = '', $verbosity = null, OutputFormatterInterface $formatter = null) { diff --git a/src/Composer/IO/ConsoleIO.php b/src/Composer/IO/ConsoleIO.php index c2dcc86ab..07e529666 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. @@ -22,13 +23,13 @@ use Symfony\Component\Console\Helper\HelperSet; * @author François Pluchino * @author Jordi Boggiano */ -class ConsoleIO implements IOInterface +class ConsoleIO extends BaseIO { protected $input; protected $output; protected $helperSet; - protected $authentications = array(); protected $lastMessage; + private $startTime; /** * Constructor. @@ -44,6 +45,11 @@ class ConsoleIO implements IOInterface $this->helperSet = $helperSet; } + public function enableDebugging($startTime) + { + $this->startTime = $startTime; + } + /** * {@inheritDoc} */ @@ -65,7 +71,23 @@ class ConsoleIO implements IOInterface */ public function isVerbose() { - return $this->output->getVerbosity() === OutputInterface::VERBOSITY_VERBOSE; + return $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE; + } + + /** + * {@inheritDoc} + */ + public function isVeryVerbose() + { + return $this->output->getVerbosity() >= 3; // OutputInterface::VERSOBITY_VERY_VERBOSE + } + + /** + * {@inheritDoc} + */ + public function isDebug() + { + return $this->output->getVerbosity() >= 4; // OutputInterface::VERBOSITY_DEBUG } /** @@ -73,6 +95,15 @@ class ConsoleIO implements IOInterface */ 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] + ); + } $this->output->write($messages, $newline); $this->lastMessage = join($newline ? "\n" : '', (array) $messages); } @@ -141,12 +172,33 @@ class ConsoleIO implements IOInterface { // 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 if ('phar:' === substr(__FILE__, 0, 5)) { $tmpExe = sys_get_temp_dir().'/hiddeninput.exe'; - copy($exe, $tmpExe); + + // use stream_copy_to_stream instead of copy + // to work around https://bugs.php.net/bug.php?id=64634 + $source = fopen(__DIR__.'\\hiddeninput.exe', 'r'); + $target = fopen($tmpExe, 'w+'); + stream_copy_to_stream($source, $target); + fclose($source); + fclose($target); + unset($source, $target); + $exe = $tmpExe; } @@ -185,40 +237,4 @@ class ConsoleIO implements IOInterface // not able to hide the answer, proceed with normal question handling return $this->ask($question); } - - /** - * {@inheritDoc} - */ - public function getAuthentications() - { - return $this->authentications; - } - - /** - * {@inheritDoc} - */ - public function hasAuthentication($repositoryName) - { - $auths = $this->getAuthentications(); - - return isset($auths[$repositoryName]); - } - - /** - * {@inheritDoc} - */ - public function getAuthentication($repositoryName) - { - $auths = $this->getAuthentications(); - - return isset($auths[$repositoryName]) ? $auths[$repositoryName] : array('username' => null, 'password' => null); - } - - /** - * {@inheritDoc} - */ - public function setAuthentication($repositoryName, $username, $password = null) - { - $this->authentications[$repositoryName] = array('username' => $username, 'password' => $password); - } } diff --git a/src/Composer/IO/IOInterface.php b/src/Composer/IO/IOInterface.php index 1cb896c5f..f117ce974 100644 --- a/src/Composer/IO/IOInterface.php +++ b/src/Composer/IO/IOInterface.php @@ -12,6 +12,8 @@ namespace Composer\IO; +use Composer\Config; + /** * The Input/Output helper interface. * @@ -27,12 +29,26 @@ interface IOInterface public function isInteractive(); /** - * Is this input verbose? + * Is this output verbose? * * @return bool */ public function isVerbose(); + /** + * Is the output very verbose? + * + * @return bool + */ + public function isVeryVerbose(); + + /** + * Is the output in debug verbosity? + * + * @return bool + */ + public function isDebug(); + /** * Is this output decorated? * @@ -55,7 +71,7 @@ interface IOInterface * @param bool $newline Whether to add a newline or not * @param integer $size The size of line */ - public function overwrite($messages, $newline = true, $size = 80); + public function overwrite($messages, $newline = true, $size = null); /** * Asks a question to the user. @@ -90,7 +106,7 @@ interface IOInterface * * @param string|array $question The question to ask * @param callback $validator A PHP callback - * @param integer $attempts Max number of times to ask before giving up (false by default, which means infinite) + * @param bool|integer $attempts Max number of times to ask before giving up (false by default, which means infinite) * @param string $default The default answer if none is given by the user * * @return mixed @@ -141,4 +157,11 @@ interface IOInterface * @param string $password The password */ public function setAuthentication($repositoryName, $username, $password = null); + + /** + * Loads authentications from a config instance + * + * @param Config $config + */ + public function loadConfiguration(Config $config); } diff --git a/src/Composer/IO/NullIO.php b/src/Composer/IO/NullIO.php index 670edd4b5..f3ecde0cb 100644 --- a/src/Composer/IO/NullIO.php +++ b/src/Composer/IO/NullIO.php @@ -17,7 +17,7 @@ namespace Composer\IO; * * @author Christophe Coevoet */ -class NullIO implements IOInterface +class NullIO extends BaseIO { /** * {@inheritDoc} @@ -38,7 +38,7 @@ class NullIO implements IOInterface /** * {@inheritDoc} */ - public function isDecorated() + public function isVeryVerbose() { return false; } @@ -46,37 +46,37 @@ class NullIO implements IOInterface /** * {@inheritDoc} */ - public function write($messages, $newline = true) + public function isDebug() { + return false; } /** * {@inheritDoc} */ - public function overwrite($messages, $newline = true, $size = 80) + public function isDecorated() { + return false; } /** * {@inheritDoc} */ - public function ask($question, $default = null) + public function write($messages, $newline = true) { - return $default; } /** * {@inheritDoc} */ - public function askConfirmation($question, $default = true) + public function overwrite($messages, $newline = true, $size = 80) { - return $default; } /** * {@inheritDoc} */ - public function askAndValidate($question, $validator, $attempts = false, $default = null) + public function ask($question, $default = null) { return $default; } @@ -84,39 +84,24 @@ class NullIO implements IOInterface /** * {@inheritDoc} */ - public function askAndHideAnswer($question) - { - return null; - } - - /** - * {@inheritDoc} - */ - public function getAuthentications() - { - return array(); - } - - /** - * {@inheritDoc} - */ - public function hasAuthentication($repositoryName) + public function askConfirmation($question, $default = true) { - return false; + return $default; } /** * {@inheritDoc} */ - public function getAuthentication($repositoryName) + public function askAndValidate($question, $validator, $attempts = false, $default = null) { - return array('username' => null, 'password' => null); + return $default; } /** * {@inheritDoc} */ - public function setAuthentication($repositoryName, $username, $password = null) + public function askAndHideAnswer($question) { + return null; } } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index c6c0ebfa5..2899e2bdb 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -15,15 +15,21 @@ namespace Composer; use Composer\Autoload\AutoloadGenerator; use Composer\DependencyResolver\DefaultPolicy; use Composer\DependencyResolver\Operation\UpdateOperation; +use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Operation\UninstallOperation; +use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; +use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\Solver; 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\Link; use Composer\Package\LinkConstraint\VersionConstraint; @@ -32,16 +38,17 @@ use Composer\Package\PackageInterface; use Composer\Package\RootPackageInterface; use Composer\Repository\CompositeRepository; use Composer\Repository\InstalledArrayRepository; +use Composer\Repository\InstalledFilesystemRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositoryManager; -use Composer\Script\EventDispatcher; use Composer\Script\ScriptEvents; /** * @author Jordi Boggiano * @author Beau Simensen * @author Konstantin Kudryashov + * @author Nils Adermann */ class Installer { @@ -98,7 +105,13 @@ class Installer protected $verbose = false; protected $update = false; protected $runScripts = true; + /** + * Array of package names/globs flagged for update + * + * @var array|null + */ protected $updateWhitelist = null; + protected $whitelistDependencies = false; /** * @var array @@ -138,6 +151,10 @@ class Installer /** * Run installation (or update) + * + * @return int 0 on success or a positive error code on failure + * + * @throws \Exception */ public function run() { @@ -148,25 +165,44 @@ class Installer $this->mockLocalRepositories($this->repositoryManager); } - if ($this->preferSource) { - $this->downloadManager->setPreferSource(true); + // TODO remove this BC feature at some point + // purge old require-dev packages to avoid conflicts with the new way of handling dev requirements + $devRepo = new InstalledFilesystemRepository(new JsonFile($this->config->get('vendor-dir').'/composer/installed_dev.json')); + if ($devRepo->getPackages()) { + $this->io->write('BC Notice: Removing old dev packages to migrate to the new require-dev handling.'); + foreach ($devRepo->getPackages() as $package) { + if ($this->installationManager->isPackageInstalled($devRepo, $package)) { + $this->installationManager->uninstall($devRepo, new UninstallOperation($package)); + } + } + unlink($this->config->get('vendor-dir').'/composer/installed_dev.json'); } - if ($this->preferDist) { - $this->downloadManager->setPreferDist(true); + unset($devRepo, $package); + // end BC + + if ($this->runScripts) { + // dispatch pre event + $eventName = $this->update ? ScriptEvents::PRE_UPDATE_CMD : ScriptEvents::PRE_INSTALL_CMD; + $this->eventDispatcher->dispatchCommandEvent($eventName, $this->devMode); } - // create installed repo, this contains all local packages + platform packages (php & extensions) + $this->downloadManager->setPreferSource($this->preferSource); + $this->downloadManager->setPreferDist($this->preferDist); + + // clone root package to have one in the installed repo that does not require anything + // we don't want it to be uninstallable, but its requirements should not conflict + // with the lock file for example $installedRootPackage = clone $this->package; $installedRootPackage->setRequires(array()); $installedRootPackage->setDevRequires(array()); + // create installed repo, this contains all local packages + platform packages (php & extensions) + $localRepo = $this->repositoryManager->getLocalRepository(); $platformRepo = new PlatformRepository(); - $repos = array_merge( - $this->repositoryManager->getLocalRepositories(), - array( - new InstalledArrayRepository(array($installedRootPackage)), - $platformRepo, - ) + $repos = array( + $localRepo, + new InstalledArrayRepository(array($installedRootPackage)), + $platformRepo, ); $installedRepo = new CompositeRepository($repos); if ($this->additionalInstalledRepository) { @@ -176,21 +212,11 @@ class Installer $aliases = $this->getRootAliases(); $this->aliasPlatformPackages($platformRepo, $aliases); - if ($this->runScripts) { - // dispatch pre event - $eventName = $this->update ? ScriptEvents::PRE_UPDATE_CMD : ScriptEvents::PRE_INSTALL_CMD; - $this->eventDispatcher->dispatchCommandEvent($eventName, $this->devMode); - } - try { $this->suggestedPackages = array(); - if (!$this->doInstall($this->repositoryManager->getLocalRepository(), $installedRepo, $aliases)) { - return false; - } - if ($this->devMode) { - if (!$this->doInstall($this->repositoryManager->getLocalDevRepository(), $installedRepo, $aliases, true)) { - return false; - } + $res = $this->doInstall($localRepo, $installedRepo, $platformRepo, $aliases, $this->devMode); + if ($res !== 0) { + return $res; } } catch (\Exception $e) { $this->installationManager->notifyInstalls(); @@ -199,14 +225,16 @@ class Installer } $this->installationManager->notifyInstalls(); - // output suggestions - foreach ($this->suggestedPackages as $suggestion) { - $target = $suggestion['target']; - if ($installedRepo->filterPackages(function (PackageInterface $package) use ($target) { - if (in_array($target, $package->getNames())) { - return false; + // 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'].')'); } } @@ -214,12 +242,48 @@ class Installer if (!$this->dryRun) { // write lock if ($this->update || !$this->locker->isLocked()) { + $localRepo->reload(); + + // if this is not run in dev mode and the root has dev requires, the lock must + // contain null to prevent dev installs from a non-dev lock + $devPackages = ($this->devMode || !$this->package->getDevRequires()) ? array() : null; + + // split dev and non-dev requirements by checking what would be removed if we update without the dev requirements + if ($this->devMode && $this->package->getDevRequires()) { + $policy = $this->createPolicy(); + $pool = $this->createPool(true); + $pool->addRepository($installedRepo, $aliases); + + // creating requirements request + $request = $this->createRequest($pool, $this->package, $platformRepo); + $request->updateAll(); + foreach ($this->package->getRequires() as $link) { + $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); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $policy, $pool, $installedRepo, $request, $ops); + foreach ($ops as $op) { + if ($op->getJobType() === 'uninstall') { + $devPackages[] = $op->getPackage(); + } + } + } + + $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); + $platformDevReqs = $this->devMode ? $this->extractPlatformRequirements($this->package->getDevRequires()) : array(); + $updatedLock = $this->locker->setLockData( - $this->repositoryManager->getLocalRepository()->getPackages(), - $this->devMode ? $this->repositoryManager->getLocalDevRepository()->getPackages() : null, + array_diff($localRepo->getCanonicalPackages(), (array) $devPackages), + $devPackages, + $platformReqs, + $platformDevReqs, $aliases, $this->package->getMinimumStability(), - $this->package->getStabilityFlags() + $this->package->getStabilityFlags(), + $this->package->getPreferStable() ); if ($updatedLock) { $this->io->write('Writing lock file'); @@ -227,41 +291,55 @@ class Installer } // write autoloader - $this->io->write('Generating autoload files'); - $localRepos = new CompositeRepository($this->repositoryManager->getLocalRepositories()); - $this->autoloadGenerator->dump($this->config, $localRepos, $this->package, $this->installationManager, 'composer', $this->optimizeAutoloader); + if ($this->optimizeAutoloader) { + $this->io->write('Generating optimized autoload files'); + } else { + $this->io->write('Generating autoload files'); + } + + $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 true; + return 0; } - protected function doInstall($localRepo, $installedRepo, $aliases, $devMode = false) + protected function doInstall($localRepo, $installedRepo, $platformRepo, $aliases, $withDevReqs) { - $minimumStability = $this->package->getMinimumStability(); - $stabilityFlags = $this->package->getStabilityFlags(); - // init vars $lockedRepository = null; $repositories = null; // initialize locker to create aliased packages $installFromLock = false; - if (!$this->update && $this->locker->isLocked($devMode)) { + if (!$this->update && $this->locker->isLocked()) { $installFromLock = true; - $lockedRepository = $this->locker->getLockedRepository($devMode); - $minimumStability = $this->locker->getMinimumStability(); - $stabilityFlags = $this->locker->getStabilityFlags(); + try { + $lockedRepository = $this->locker->getLockedRepository($withDevReqs); + } catch (\RuntimeException $e) { + // if there are dev requires, then we really can not install + if ($this->package->getDevRequires()) { + throw $e; + } + // no require-dev in composer.json and the lock file was created with no dev info, so skip them + $lockedRepository = $this->locker->getLockedRepository(); + } } $this->whitelistUpdateDependencies( $localRepo, - $devMode, + $withDevReqs, $this->package->getRequires(), $this->package->getDevRequires() ); @@ -269,14 +347,14 @@ class Installer $this->io->write('Loading composer repositories with package information'); // creating repository pool - $policy = new DefaultPolicy(); - $pool = new Pool($minimumStability, $stabilityFlags); + $policy = $this->createPolicy(); + $pool = $this->createPool($withDevReqs); $pool->addRepository($installedRepo, $aliases); if ($installFromLock) { $pool->addRepository($lockedRepository, $aliases); } - if (!$installFromLock || !$this->locker->isCompleteFormat($devMode)) { + if (!$installFromLock) { $repositories = $this->repositoryManager->getRepositories(); foreach ($repositories as $repository) { $pool->addRepository($repository, $aliases); @@ -284,31 +362,78 @@ class Installer } // creating requirements request - $request = new Request($pool); + $request = $this->createRequest($pool, $this->package, $platformRepo); - $constraint = new VersionConstraint('=', $this->package->getVersion()); - $constraint->setPrettyString($this->package->getPrettyVersion()); - $request->install($this->package->getName(), $constraint); + if (!$installFromLock) { + // remove unstable packages from the localRepo if they don't match the current stability settings + $removedUnstablePackages = array(); + foreach ($localRepo->getPackages() as $package) { + if ( + !$pool->isPackageAcceptable($package->getNames(), $package->getStability()) + && $this->installationManager->isPackageInstalled($localRepo, $package) + ) { + $removedUnstablePackages[$package->getName()] = true; + $request->remove($package->getName(), new VersionConstraint('=', $package->getVersion())); + } + } + } if ($this->update) { - $this->io->write('Updating '.($devMode ? 'dev ': '').'dependencies'); + $this->io->write('Updating dependencies'.($withDevReqs?' (including require-dev)':'').''); $request->updateAll(); - $links = $devMode ? $this->package->getDevRequires() : $this->package->getRequires(); + if ($withDevReqs) { + $links = array_merge($this->package->getRequires(), $this->package->getDevRequires()); + } else { + $links = $this->package->getRequires(); + } foreach ($links as $link) { $request->install($link->getTarget(), $link->getConstraint()); } - } elseif ($installFromLock) { - $this->io->write('Installing '.($devMode ? 'dev ': '').'dependencies from lock file'); - if (!$this->locker->isCompleteFormat($devMode)) { - $this->io->write('Warning: Your lock file is in a deprecated format. It will most likely take a *long* time for composer to install dependencies, and may cause dependency solving issues.'); + // if the updateWhitelist is enabled, packages not in it are also fixed + // to the version specified in the lock, or their currently installed version + if ($this->updateWhitelist) { + if ($this->locker->isLocked()) { + try { + $currentPackages = $this->locker->getLockedRepository($withDevReqs)->getPackages(); + } catch (\RuntimeException $e) { + // fetch only non-dev packages from lock if doing a dev update fails due to a previously incomplete lock file + $currentPackages = $this->locker->getLockedRepository()->getPackages(); + } + } else { + $currentPackages = $installedRepo->getPackages(); + } + + // collect packages to fixate from root requirements as well as installed packages + $candidates = array(); + foreach ($links as $link) { + $candidates[$link->getTarget()] = true; + } + foreach ($localRepo->getPackages() as $package) { + $candidates[$package->getName()] = true; + } + + // fix them to the version in lock (or currently installed) if they are not updateable + foreach ($candidates as $candidate => $dummy) { + foreach ($currentPackages as $curPackage) { + if ($curPackage->getName() === $candidate) { + if (!$this->isUpdateable($curPackage) && !isset($removedUnstablePackages[$curPackage->getName()])) { + $constraint = new VersionConstraint('=', $curPackage->getVersion()); + $request->install($curPackage->getName(), $constraint); + } + break; + } + } + } } + } elseif ($installFromLock) { + $this->io->write('Installing dependencies'.($withDevReqs?' (including require-dev)':'').' from lock file'); - if (!$this->locker->isFresh() && !$devMode) { - $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.'); + 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.'); } foreach ($lockedRepository->getPackages() as $package) { @@ -320,66 +445,21 @@ class Installer $constraint->setPrettyString($package->getPrettyVersion()); $request->install($package->getName(), $constraint); } - } else { - $this->io->write('Installing '.($devMode ? 'dev ': '').'dependencies'); - - $links = $devMode ? $this->package->getDevRequires() : $this->package->getRequires(); - foreach ($links as $link) { + foreach ($this->locker->getPlatformRequirements($withDevReqs) as $link) { $request->install($link->getTarget(), $link->getConstraint()); } - } - - // fix the version of all installed packages (+ platform) that are not - // in the current local repo to prevent rogue updates (e.g. non-dev - // updating when in dev) - foreach ($installedRepo->getPackages() as $package) { - if ($package->getRepository() === $localRepo) { - continue; - } - - $constraint = new VersionConstraint('=', $package->getVersion()); - $constraint->setPrettyString($package->getPrettyVersion()); - - if (!($package->getRepository() instanceof PlatformRepository) - || !($provided = $this->package->getProvides()) - || !isset($provided[$package->getName()]) - || !$provided[$package->getName()]->getConstraint()->matches($constraint) - ) { - $request->install($package->getName(), $constraint); - } - } + } else { + $this->io->write('Installing dependencies'.($withDevReqs?' (including require-dev)':'').''); - // if the updateWhitelist is enabled, packages not in it are also fixed - // to the version specified in the lock, or their currently installed version - if ($this->update && $this->updateWhitelist) { - if ($this->locker->isLocked($devMode)) { - $currentPackages = $this->locker->getLockedRepository($devMode)->getPackages(); + if ($withDevReqs) { + $links = array_merge($this->package->getRequires(), $this->package->getDevRequires()); } else { - $currentPackages = $installedRepo->getPackages(); + $links = $this->package->getRequires(); } - // collect links from composer as well as installed packages - $candidates = array(); foreach ($links as $link) { - $candidates[$link->getTarget()] = true; - } - foreach ($localRepo->getPackages() as $package) { - $candidates[$package->getName()] = true; - } - - // fix them to the version in lock (or currently installed) if they are not updateable - foreach ($candidates as $candidate => $dummy) { - foreach ($currentPackages as $curPackage) { - if ($curPackage->getName() === $candidate) { - if ($this->isUpdateable($curPackage)) { - break; - } - - $constraint = new VersionConstraint('=', $curPackage->getVersion()); - $request->install($curPackage->getName(), $constraint); - } - } + $request->install($link->getTarget(), $link->getConstraint()); } } @@ -387,27 +467,16 @@ 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); + $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()); - return false; - } - - if ($devMode) { - // remove bogus operations that the solver creates for stuff that was force-updated in the non-dev pass - // TODO this should not be necessary ideally, but it seems to work around the problem quite well - foreach ($operations as $index => $op) { - if ('update' === $op->getJobType() && $op->getInitialPackage()->getUniqueName() === $op->getTargetPackage()->getUniqueName() - && $op->getInitialPackage()->getSourceReference() === $op->getTargetPackage()->getSourceReference() - && $op->getInitialPackage()->getDistReference() === $op->getTargetPackage()->getDistReference() - ) { - unset($operations[$index]); - } - } + return max(1, $e->getCode()); } // force dev packages to be updated if we update or install from a (potentially new) lock @@ -418,6 +487,9 @@ class Installer $this->io->write('Nothing to install or update'); } + $operations = $this->movePluginsToFront($operations); + $operations = $this->moveUninstallsToFront($operations); + foreach ($operations as $operation) { // collect suggestions if ('install' === $operation->getJobType()) { @@ -430,11 +502,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; @@ -450,15 +517,53 @@ 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; + } } - // output alias operations in verbose mode, or all ops in dry run - if ($this->dryRun || ($this->verbose && false !== strpos($operation->getJobType(), 'Alias'))) { + $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 + if ($this->dryRun && false === strpos($operation->getJobType(), 'Alias')) { + $this->io->write(' - ' . $operation); + $this->io->write(''); + } elseif ($this->io->isDebug() && false !== strpos($operation->getJobType(), 'Alias')) { $this->io->write(' - ' . $operation); + $this->io->write(''); } $this->installationManager->execute($localRepo, $operation); + // output reasons why the operation was ran, only for install/update operations + if ($this->verbose && $this->io->isVeryVerbose() && in_array($operation->getJobType(), array('install', 'update'))) { + $reason = $operation->getReason(); + if ($reason instanceof Rule) { + switch ($reason->getReason()) { + case Rule::RULE_JOB_INSTALL: + $this->io->write(' REASON: Required by root: '.$reason->getPrettyString()); + $this->io->write(''); + break; + case Rule::RULE_PACKAGE_REQUIRES: + $this->io->write(' REASON: '.$reason->getPrettyString()); + $this->io->write(''); + break; + } + } + } + $event = 'Composer\Script\ScriptEvents::POST_PACKAGE_'.strtoupper($operation->getJobType()); if (defined($event) && $this->runScripts) { $this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $operation); @@ -469,7 +574,141 @@ class Installer } } - return true; + return 0; + } + + /** + * Workaround: if your packages depend on plugins, we must be sure + * that those are installed / updated first; else it would lead to packages + * being installed multiple times in different folders, when running Composer + * twice. + * + * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147, + * it at least fixes the symptoms and makes usage of composer possible (again) + * in such scenarios. + * + * @param OperationInterface[] $operations + * @return OperationInterface[] reordered operation list + */ + private function movePluginsToFront(array $operations) + { + $installerOps = array(); + foreach ($operations as $idx => $op) { + if ($op instanceof InstallOperation) { + $package = $op->getPackage(); + } elseif ($op instanceof UpdateOperation) { + $package = $op->getTargetPackage(); + } else { + continue; + } + + 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(); + $stabilityFlags = $this->package->getStabilityFlags(); + + if (!$this->update && $this->locker->isLocked()) { + $minimumStability = $this->locker->getMinimumStability(); + $stabilityFlags = $this->locker->getStabilityFlags(); + } + + $requires = $this->package->getRequires(); + if ($withDevReqs) { + $requires = array_merge($requires, $this->package->getDevRequires()); + } + $rootConstraints = array(); + foreach ($requires as $req => $constraint) { + $rootConstraints[$req] = $constraint->getConstraint(); + } + + return new Pool($minimumStability, $stabilityFlags, $rootConstraints); + } + + private function createPolicy() + { + $preferStable = null; + if (!$this->update && $this->locker->isLocked()) { + $preferStable = $this->locker->getPreferStable(); + } + // old lock file without prefer stable will return null + // so in this case we use the composer.json info + if (null === $preferStable) { + $preferStable = $this->package->getPreferStable(); + } + + return new DefaultPolicy($preferStable); + } + + private function createRequest(Pool $pool, RootPackageInterface $rootPackage, PlatformRepository $platformRepo) + { + $request = new Request($pool); + + $constraint = new VersionConstraint('=', $rootPackage->getVersion()); + $constraint->setPrettyString($rootPackage->getPrettyVersion()); + $request->install($rootPackage->getName(), $constraint); + + $fixedPackages = $platformRepo->getPackages(); + if ($this->additionalInstalledRepository) { + $additionalFixedPackages = $this->additionalInstalledRepository->getPackages(); + $fixedPackages = array_merge($fixedPackages, $additionalFixedPackages); + } + + // fix the version of all platform packages + additionally installed packages + // to prevent the solver trying to remove or update those + $provided = $rootPackage->getProvides(); + foreach ($fixedPackages as $package) { + $constraint = new VersionConstraint('=', $package->getVersion()); + $constraint->setPrettyString($package->getPrettyVersion()); + + // skip platform packages that are provided by the root package + if ($package->getRepository() !== $platformRepo + || !isset($provided[$package->getName()]) + || !$provided[$package->getName()]->getConstraint()->matches($constraint) + ) { + $request->install($package->getName(), $constraint); + } + } + + return $request; } private function processDevPackages($localRepo, $pool, $policy, $repositories, $lockedRepository, $installFromLock, $task, array $operations = null) @@ -481,16 +720,12 @@ class Installer $operations = array(); } - foreach ($localRepo->getPackages() as $package) { + foreach ($localRepo->getCanonicalPackages() as $package) { // skip non-dev packages if (!$package->isDev()) { continue; } - if ($package instanceof AliasPackage) { - continue; - } - // skip packages that will be updated/uninstalled foreach ($operations as $operation) { if (('update' === $operation->getJobType() && $operation->getInitialPackage()->equals($package)) @@ -608,8 +843,6 @@ class Installer foreach ($versions as $version => $alias) { $packages = $platformRepo->findPackages($package, $version); foreach ($packages as $package) { - $package->setAlias($alias['alias_normalized']); - $package->setPrettyAlias($alias['alias']); $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); $aliasPackage->setRootPackageAlias(true); $platformRepo->addPackage($aliasPackage); @@ -625,9 +858,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; } } @@ -635,6 +867,31 @@ 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(); + foreach ($links as $link) { + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $link->getTarget())) { + $platformReqs[$link->getTarget()] = $link->getPrettyConstraint(); + } + } + + return $platformReqs; + } + /** * Adds all dependencies of the update whitelist to the whitelist, too. * @@ -653,6 +910,11 @@ class Installer return; } + $requiredPackageNames = array(); + foreach (array_merge($rootRequires, $rootDevRequires) as $require) { + $requiredPackageNames[] = $require->getTarget(); + } + if ($devMode) { $rootRequires = array_merge($rootRequires, $rootDevRequires); } @@ -667,10 +929,31 @@ class Installer $seen = array(); + $rootRequiredPackageNames = array_keys($rootRequires); + foreach ($this->updateWhitelist as $packageName => $void) { $packageQueue = new \SplQueue; - foreach ($pool->whatProvides($packageName) as $depPackage) { + $depPackages = $pool->whatProvides($packageName); + + $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.'); + } + + foreach ($depPackages as $depPackage) { $packageQueue->enqueue($depPackage); } @@ -683,11 +966,12 @@ class Installer $seen[$package->getId()] = true; $this->updateWhitelist[$package->getName()] = true; - $requires = $package->getRequires(); - if ($devMode) { - $requires = array_merge($requires, $package->getDevRequires()); + if (!$this->whitelistDependencies) { + continue; } + $requires = $package->getRequires(); + foreach ($requires as $require) { $requirePackages = $pool->whatProvides($require->getTarget()); @@ -711,27 +995,17 @@ class Installer */ private function mockLocalRepositories(RepositoryManager $rm) { - $packages = array_map(function ($p) { - return clone $p; - }, $rm->getLocalRepository()->getPackages()); - foreach ($packages as $key => $package) { - if ($package instanceof AliasPackage) { - unset($packages[$key]); - } + $packages = array(); + foreach ($rm->getLocalRepository()->getPackages() as $package) { + $packages[(string) $package] = clone $package; } - $rm->setLocalRepository( - new InstalledArrayRepository($packages) - ); - - $packages = array_map(function ($p) { - return clone $p; - }, $rm->getLocalDevRepository()->getPackages()); foreach ($packages as $key => $package) { if ($package instanceof AliasPackage) { - unset($packages[$key]); + $alias = (string) $package->getAliasOf(); + $packages[$key] = new AliasPackage($packages[$alias], $package->getVersion(), $package->getPrettyVersion()); } } - $rm->setLocalDevRepository( + $rm->setLocalRepository( new InstalledArrayRepository($packages) ); } @@ -739,17 +1013,12 @@ class Installer /** * Create Installer * - * @param IOInterface $io - * @param Composer $composer - * @param EventDispatcher $eventDispatcher - * @param AutoloadGenerator $autoloadGenerator + * @param IOInterface $io + * @param Composer $composer * @return Installer */ - public static function create(IOInterface $io, Composer $composer, EventDispatcher $eventDispatcher = null, AutoloadGenerator $autoloadGenerator = null) + public static function create(IOInterface $io, Composer $composer) { - $eventDispatcher = $eventDispatcher ?: new EventDispatcher($composer, $io); - $autoloadGenerator = $autoloadGenerator ?: new AutoloadGenerator; - return new static( $io, $composer->getConfig(), @@ -758,8 +1027,8 @@ class Installer $composer->getRepositoryManager(), $composer->getLocker(), $composer->getInstallationManager(), - $eventDispatcher, - $autoloadGenerator + $composer->getEventDispatcher(), + $composer->getAutoloadGenerator() ); } @@ -812,7 +1081,7 @@ class Installer /** * Whether or not generated autoloader are optimized * - * @param bool $optimizeAutoloader + * @param bool $optimizeAutoloader * @return Installer */ public function setOptimizeAutoloader($optimizeAutoloader = false) @@ -902,7 +1171,20 @@ class Installer } /** - * Disables custom installers. + * Should dependencies of whitelisted packages be updated recursively? + * + * @param boolean $updateDependencies + * @return Installer + */ + public function setWhitelistDependencies($updateDependencies = true) + { + $this->whitelistDependencies = (boolean) $updateDependencies; + + return $this; + } + + /** + * Disables plugins. * * Call this if you want to ensure that third-party code never gets * executed. The default is to automatically install, and execute @@ -910,9 +1192,9 @@ class Installer * * @return Installer */ - public function disableCustomInstallers() + public function disablePlugins() { - $this->installationManager->disableCustomInstallers(); + $this->installationManager->disablePlugins(); return $this; } diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 55e1eef4b..a43acbbda 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -29,6 +29,7 @@ use Composer\Util\StreamContextFactory; * * @author Konstantin Kudryashov * @author Jordi Boggiano + * @author Nils Adermann */ class InstallationManager { @@ -66,16 +67,16 @@ class InstallationManager } /** - * Disables custom installers. + * Disables plugins. * - * We prevent any custom installers from being instantiated by simply + * We prevent any plugins from being instantiated by simply * deactivating the installer for them. This ensure that no third-party * code is ever executed. */ - public function disableCustomInstallers() + public function disablePlugins() { foreach ($this->installers as $i => $installer) { - if (!$installer instanceof InstallerInstaller) { + if (!$installer instanceof PluginInstaller) { continue; } @@ -90,7 +91,7 @@ class InstallationManager * * @return InstallerInterface * - * @throws InvalidArgumentException if installer for provided type is not registered + * @throws \InvalidArgumentException if installer for provided type is not registered */ public function getInstaller($type) { @@ -251,7 +252,7 @@ class InstallationManager ) ); - $context = StreamContextFactory::getContext($opts); + $context = StreamContextFactory::getContext($url, $opts); @file_get_contents($url, false, $context); } @@ -275,7 +276,7 @@ class InstallationManager ) ); - $context = StreamContextFactory::getContext($opts); + $context = StreamContextFactory::getContext($repoUrl, $opts); @file_get_contents($repoUrl, false, $context); } 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/InstallerInstaller.php b/src/Composer/Installer/InstallerInstaller.php deleted file mode 100644 index cde0030cb..000000000 --- a/src/Composer/Installer/InstallerInstaller.php +++ /dev/null @@ -1,106 +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\Installer; - -use Composer\Composer; -use Composer\Package\Package; -use Composer\IO\IOInterface; -use Composer\Autoload\AutoloadGenerator; -use Composer\Repository\InstalledRepositoryInterface; -use Composer\Package\PackageInterface; - -/** - * Installer installation manager. - * - * @author Jordi Boggiano - */ -class InstallerInstaller extends LibraryInstaller -{ - private $installationManager; - private static $classCounter = 0; - - /** - * Initializes Installer installer. - * - * @param IOInterface $io - * @param Composer $composer - * @param string $type - */ - public function __construct(IOInterface $io, Composer $composer, $type = 'library') - { - parent::__construct($io, $composer, 'composer-installer'); - $this->installationManager = $composer->getInstallationManager(); - - foreach ($composer->getRepositoryManager()->getLocalRepositories() as $repo) { - foreach ($repo->getPackages() as $package) { - if ('composer-installer' === $package->getType()) { - $this->registerInstaller($package); - } - } - } - } - - /** - * {@inheritDoc} - */ - public function install(InstalledRepositoryInterface $repo, PackageInterface $package) - { - $extra = $package->getExtra(); - if (empty($extra['class'])) { - throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-installer packages should have a class defined in their extra key to be usable.'); - } - - parent::install($repo, $package); - $this->registerInstaller($package); - } - - /** - * {@inheritDoc} - */ - public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) - { - $extra = $target->getExtra(); - if (empty($extra['class'])) { - throw new \UnexpectedValueException('Error while installing '.$target->getPrettyName().', composer-installer packages should have a class defined in their extra key to be usable.'); - } - - parent::update($repo, $initial, $target); - $this->registerInstaller($target); - } - - private function registerInstaller(PackageInterface $package) - { - $downloadPath = $this->getInstallPath($package); - - $extra = $package->getExtra(); - $classes = is_array($extra['class']) ? $extra['class'] : array($extra['class']); - - $generator = new AutoloadGenerator; - $map = $generator->parseAutoloads(array(array($package, $downloadPath)), new Package('dummy', '1.0.0.0', '1.0.0')); - $classLoader = $generator->createLoader($map); - $classLoader->register(); - - foreach ($classes as $class) { - if (class_exists($class, false)) { - $code = file_get_contents($classLoader->findFile($class)); - $code = preg_replace('{^class\s+(\S+)}mi', 'class $1_composer_tmp'.self::$classCounter, $code); - eval('?>'.$code); - $class .= '_composer_tmp'.self::$classCounter; - self::$classCounter++; - } - - $installer = new $class($this->io, $this->composer); - $this->installationManager->addInstaller($installer); - } - } -} diff --git a/src/Composer/Installer/InstallerInterface.php b/src/Composer/Installer/InstallerInterface.php index 144cbcdf6..469b91ed4 100644 --- a/src/Composer/Installer/InstallerInterface.php +++ b/src/Composer/Installer/InstallerInterface.php @@ -56,7 +56,7 @@ interface InstallerInterface * @param PackageInterface $initial already installed package version * @param PackageInterface $target updated version * - * @throws InvalidArgumentException if $from package is not installed + * @throws InvalidArgumentException if $initial package is not installed */ public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target); diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index 5bbba4815..05cd420d7 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -14,10 +14,10 @@ namespace Composer\Installer; use Composer\Composer; use Composer\IO\IOInterface; -use Composer\Downloader\DownloadManager; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; use Composer\Util\Filesystem; +use Composer\Util\ProcessExecutor; /** * Package installation manager. @@ -41,15 +41,16 @@ class LibraryInstaller implements InstallerInterface * @param IOInterface $io * @param Composer $composer * @param string $type + * @param Filesystem $filesystem */ - public function __construct(IOInterface $io, Composer $composer, $type = 'library') + public function __construct(IOInterface $io, Composer $composer, $type = 'library', Filesystem $filesystem = null) { $this->composer = $composer; $this->downloadManager = $composer->getDownloadManager(); $this->io = $io; $this->type = $type; - $this->filesystem = new Filesystem(); + $this->filesystem = $filesystem ?: new Filesystem(); $this->vendorDir = rtrim($composer->getConfig()->get('vendor-dir'), '/'); $this->binDir = rtrim($composer->getConfig()->get('bin-dir'), '/'); } @@ -116,20 +117,17 @@ class LibraryInstaller implements InstallerInterface public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) { if (!$repo->hasPackage($package)) { - // TODO throw exception again here, when update is fixed and we don't have to remove+install (see #125) - return; throw new \InvalidArgumentException('Package is not installed: '.$package); } - $downloadPath = $this->getInstallPath($package); - $this->removeCode($package); $this->removeBinaries($package); $repo->removePackage($package); + $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); } } @@ -140,10 +138,16 @@ class LibraryInstaller implements InstallerInterface */ public function getInstallPath(PackageInterface $package) { - $this->initializeVendorDir(); $targetDir = $package->getTargetDir(); - return ($this->vendorDir ? $this->vendorDir.'/' : '') . $package->getPrettyName() . ($targetDir ? '/'.$targetDir : ''); + return $this->getPackageBasePath($package) . ($targetDir ? '/'.$targetDir : ''); + } + + protected function getPackageBasePath(PackageInterface $package) + { + $this->initializeVendorDir(); + + return ($this->vendorDir ? $this->vendorDir.'/' : '') . $package->getPrettyName(); } protected function installCode(PackageInterface $package) @@ -154,13 +158,28 @@ class LibraryInstaller implements InstallerInterface protected function updateCode(PackageInterface $initial, PackageInterface $target) { - $downloadPath = $this->getInstallPath($initial); - $this->downloadManager->update($initial, $target, $downloadPath); + $initialDownloadPath = $this->getInstallPath($initial); + $targetDownloadPath = $this->getInstallPath($target); + if ($targetDownloadPath !== $initialDownloadPath) { + // if the target and initial dirs intersect, we force a remove + install + // to avoid the rename wiping the target dir as part of the initial dir cleanup + if (substr($initialDownloadPath, 0, strlen($targetDownloadPath)) === $targetDownloadPath + || substr($targetDownloadPath, 0, strlen($initialDownloadPath)) === $initialDownloadPath + ) { + $this->removeCode($initial); + $this->installCode($target); + + return; + } + + $this->filesystem->rename($initialDownloadPath, $targetDownloadPath); + } + $this->downloadManager->update($initial, $target, $targetDownloadPath); } protected function removeCode(PackageInterface $package) { - $downloadPath = $this->getInstallPath($package); + $downloadPath = $this->getPackageBasePath($package); $this->downloadManager->remove($package, $downloadPath); } @@ -178,10 +197,16 @@ 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; } + // in case a custom installer returned a relative path for the + // $package, we can now safely turn it into a absolute path (as we + // already checked the binary's existence). The following helpers + // will require absolute paths to work properly. + $binPath = realpath($binPath); + $this->initializeBinDir(); $link = $this->binDir.'/'.basename($bin); if (file_exists($link)) { @@ -189,19 +214,24 @@ class LibraryInstaller implements InstallerInterface // likely leftover from a previous install, make sure // that the target is still executable in case this // is a fresh install of the vendor. - chmod($link, 0777 & ~umask()); + @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')) { // add unixy support for cygwin and similar environments if ('.bat' !== substr($binPath, -4)) { file_put_contents($link, $this->generateUnixyProxyCode($binPath, $link)); - chmod($link, 0777 & ~umask()); + @chmod($link, 0777 & ~umask()); $link .= '.bat'; + if (file_exists($link)) { + $this->io->write(' Skipped installation of bin '.$bin.'.bat proxy for package '.$package->getName().': a .bat proxy was already installed'); + } + } + if (!file_exists($link)) { + file_put_contents($link, $this->generateWindowsProxyCode($binPath, $link)); } - file_put_contents($link, $this->generateWindowsProxyCode($binPath, $link)); } else { $cwd = getcwd(); try { @@ -209,13 +239,15 @@ class LibraryInstaller implements InstallerInterface // when using it in smbfs mounted folder $relativeBin = $this->filesystem->findShortestPath($link, $binPath); chdir(dirname($link)); - symlink($relativeBin, $link); + if (false === symlink($relativeBin, $link)) { + throw new \ErrorException(); + } } catch (\ErrorException $e) { file_put_contents($link, $this->generateUnixyProxyCode($binPath, $link)); } chdir($cwd); } - chmod($link, 0777 & ~umask()); + @chmod($link, 0777 & ~umask()); } } @@ -227,11 +259,11 @@ class LibraryInstaller implements InstallerInterface } foreach ($binaries as $bin) { $link = $this->binDir.'/'.basename($bin); - if (file_exists($link)) { - unlink($link); + if (is_link($link) || file_exists($link)) { + $this->filesystem->unlink($link); } if (file_exists($link.'.bat')) { - unlink($link.'.bat'); + $this->filesystem->unlink($link.'.bat'); } } } @@ -265,7 +297,7 @@ class LibraryInstaller implements InstallerInterface } return "@ECHO OFF\r\n". - "SET BIN_TARGET=%~dp0\\".escapeshellarg(dirname($binPath)).'\\'.basename($binPath)."\r\n". + "SET BIN_TARGET=%~dp0/".trim(ProcessExecutor::escape($binPath), '"')."\r\n". "{$caller} \"%BIN_TARGET%\" %*\r\n"; } @@ -276,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/MetapackageInstaller.php b/src/Composer/Installer/MetapackageInstaller.php index e0d19ab6e..3f99ec03c 100644 --- a/src/Composer/Installer/MetapackageInstaller.php +++ b/src/Composer/Installer/MetapackageInstaller.php @@ -65,8 +65,6 @@ class MetapackageInstaller implements InstallerInterface public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) { if (!$repo->hasPackage($package)) { - // TODO throw exception again here, when update is fixed and we don't have to remove+install (see #125) - return; throw new \InvalidArgumentException('Package is not installed: '.$package); } diff --git a/src/Composer/Installer/NoopInstaller.php b/src/Composer/Installer/NoopInstaller.php index 1f006ee82..72cf17d22 100644 --- a/src/Composer/Installer/NoopInstaller.php +++ b/src/Composer/Installer/NoopInstaller.php @@ -71,8 +71,6 @@ class NoopInstaller implements InstallerInterface public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) { if (!$repo->hasPackage($package)) { - // TODO throw exception again here, when update is fixed and we don't have to remove+install (see #125) - return; throw new \InvalidArgumentException('Package is not installed: '.$package); } $repo->removePackage($package); diff --git a/src/Composer/Installer/PearInstaller.php b/src/Composer/Installer/PearInstaller.php index 1a8f674af..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) @@ -99,9 +100,9 @@ class PearInstaller extends LibraryInstaller { parent::initializeBinDir(); file_put_contents($this->binDir.'/composer-php', $this->generateUnixyPhpProxyCode()); - chmod($this->binDir.'/composer-php', 0777); + @chmod($this->binDir.'/composer-php', 0777); file_put_contents($this->binDir.'/composer-php.bat', $this->generateWindowsPhpProxyCode()); - chmod($this->binDir.'/composer-php.bat', 0777); + @chmod($this->binDir.'/composer-php.bat', 0777); } protected function generateWindowsProxyCode($bin, $link) @@ -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 new file mode 100644 index 000000000..61c5a2823 --- /dev/null +++ b/src/Composer/Installer/PluginInstaller.php @@ -0,0 +1,81 @@ + + * 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\Package\Package; +use Composer\IO\IOInterface; +use Composer\Repository\InstalledRepositoryInterface; +use Composer\Package\PackageInterface; + +/** + * Installer for plugin packages + * + * @author Jordi Boggiano + * @author Nils Adermann + */ +class PluginInstaller extends LibraryInstaller +{ + private $installationManager; + private static $classCounter = 0; + + /** + * Initializes Plugin installer. + * + * @param IOInterface $io + * @param Composer $composer + * @param string $type + */ + public function __construct(IOInterface $io, Composer $composer, $type = 'library') + { + parent::__construct($io, $composer, 'composer-plugin'); + $this->installationManager = $composer->getInstallationManager(); + + } + + /** + * {@inheritDoc} + */ + public function supports($packageType) + { + return $packageType === 'composer-plugin' || $packageType === 'composer-installer'; + } + + /** + * {@inheritDoc} + */ + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { + $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.'); + } + + parent::install($repo, $package); + $this->composer->getPluginManager()->registerPackage($package); + } + + /** + * {@inheritDoc} + */ + public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) + { + $extra = $target->getExtra(); + if (empty($extra['class'])) { + throw new \UnexpectedValueException('Error while installing '.$target->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.'); + } + + parent::update($repo, $initial, $target); + $this->composer->getPluginManager()->registerPackage($target); + } +} diff --git a/src/Composer/Installer/ProjectInstaller.php b/src/Composer/Installer/ProjectInstaller.php index cbc0ebfaf..c79238b36 100644 --- a/src/Composer/Installer/ProjectInstaller.php +++ b/src/Composer/Installer/ProjectInstaller.php @@ -15,6 +15,7 @@ namespace Composer\Installer; use Composer\Package\PackageInterface; use Composer\Downloader\DownloadManager; use Composer\Repository\InstalledRepositoryInterface; +use Composer\Util\Filesystem; /** * Project Installer is used to install a single package into a directory as @@ -26,11 +27,13 @@ class ProjectInstaller implements InstallerInterface { private $installPath; private $downloadManager; + private $filesystem; public function __construct($installPath, DownloadManager $dm) { - $this->installPath = $installPath; + $this->installPath = rtrim(strtr($installPath, '\\', '/'), '/').'/'; $this->downloadManager = $dm; + $this->filesystem = new Filesystem; } /** @@ -58,13 +61,12 @@ class ProjectInstaller implements InstallerInterface public function install(InstalledRepositoryInterface $repo, PackageInterface $package) { $installPath = $this->installPath; - if (file_exists($installPath)) { - throw new \InvalidArgumentException("Project directory $installPath already exists."); + if (file_exists($installPath) && !$this->filesystem->isDirEmpty($installPath)) { + throw new \InvalidArgumentException("Project directory $installPath is not empty."); } - if (!file_exists(dirname($installPath))) { - throw new \InvalidArgumentException("Project root " . dirname($installPath) . " does not exist."); + if (!is_dir($installPath)) { + mkdir($installPath, 0777, true); } - mkdir($installPath, 0777); $this->downloadManager->download($package, $installPath); } diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php old mode 100755 new mode 100644 index 00bded4d0..80718348c --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -12,7 +12,6 @@ namespace Composer\Json; -use Composer\Composer; use JsonSchema\Validator; use Seld\JsonLint\JsonParser; use Seld\JsonLint\ParsingException; @@ -40,8 +39,9 @@ class JsonFile /** * Initializes json file reader/parser. * - * @param string $path path to a lockfile - * @param RemoteFilesystem $rfs required for loading http/https json files + * @param string $path path to a lockfile + * @param RemoteFilesystem $rfs required for loading http/https json files + * @throws \InvalidArgumentException */ public function __construct($path, RemoteFilesystem $rfs = null) { @@ -74,7 +74,8 @@ class JsonFile /** * Reads json file. * - * @return array + * @throws \RuntimeException + * @return mixed */ public function read() { @@ -96,8 +97,9 @@ class JsonFile /** * Writes json file. * - * @param array $hash writes hash into json file - * @param int $options json_encode options (defaults to JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + * @param array $hash writes hash into json file + * @param int $options json_encode options (defaults to JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + * @throws \UnexpectedValueException */ public function write(array $hash, $options = 448) { @@ -108,21 +110,35 @@ class JsonFile $dir.' exists and is not a directory.' ); } - if (!mkdir($dir, 0777, true)) { + if (!@mkdir($dir, 0777, true)) { throw new \UnexpectedValueException( $dir.' does not exist and could not be created.' ); } } - file_put_contents($this->path, static::encode($hash, $options). ($options & self::JSON_PRETTY_PRINT ? "\n" : '')); + + $retries = 3; + while ($retries--) { + try { + file_put_contents($this->path, static::encode($hash, $options). ($options & self::JSON_PRETTY_PRINT ? "\n" : '')); + break; + } catch (\Exception $e) { + if ($retries) { + usleep(500000); + continue; + } + + throw $e; + } + } } /** * Validates the schema of the current json file according to composer-schema.json rules * - * @param int $schema a JsonFile::*_SCHEMA constant - * @return bool true on success - * @throws \UnexpectedValueException + * @param int $schema a JsonFile::*_SCHEMA constant + * @return bool true on success + * @throws JsonValidationException */ public function validateSchema($schema = self::STRICT_SCHEMA) { @@ -161,11 +177,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 @@ -173,7 +184,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)) { + $json = preg_replace('/\[\s+\]/', '[]', $json); + $json = preg_replace('/\{\s+\}/', '{}', $json); + } + + return $json; } $json = json_encode($data); @@ -186,81 +205,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; } @@ -291,6 +236,7 @@ class JsonFile * @return bool true on success * @throws \UnexpectedValueException * @throws JsonValidationException + * @throws ParsingException */ protected static function validateSyntax($json, $file = null) { 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 fd37f681d..6dccc62dc 100644 --- a/src/Composer/Json/JsonManipulator.php +++ b/src/Composer/Json/JsonManipulator.php @@ -17,7 +17,10 @@ namespace Composer\Json; */ class JsonManipulator { - private static $RECURSE_BLOCKS = '(?:[^{}]*|\{(?:[^{}]*|\{(?:[^{}]*|\{(?:[^{}]*|\{[^{}]*\})*\})*\})*\})*'; + private static $RECURSE_BLOCKS; + private static $RECURSE_ARRAYS; + private static $JSON_VALUE; + private static $JSON_STRING; private $contents; private $newline; @@ -25,12 +28,22 @@ class JsonManipulator public function __construct($contents) { + if (!self::$RECURSE_BLOCKS) { + self::$RECURSE_BLOCKS = '(?:[^{}]*|\{(?:[^{}]*|\{(?:[^{}]*|\{(?:[^{}]*|\{[^{}]*\})*\})*\})*\})*'; + self::$RECURSE_ARRAYS = '(?:[^\]]*|\[(?:[^\]]*|\[(?:[^\]]*|\[(?:[^\]]*|\[[^\]]*\])*\])*\])*\]|'.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_ARRAYS.'\]|\{'.self::$RECURSE_BLOCKS.'\})'; + } + $contents = trim($contents); - if (!preg_match('#^\{(.*)\}$#s', $contents)) { + if ($contents === '') { + $contents = '{}'; + } + if (!$this->pregMatch('#^\{(.*)\}$#s', $contents)) { throw new \InvalidArgumentException('The json file must be an object ({})'); } - $this->newline = false !== strpos("\r\n", $contents) ? "\r\n": "\n"; - $this->contents = $contents; + $this->newline = false !== strpos($contents, "\r\n") ? "\r\n": "\n"; + $this->contents = $contents === '{}' ? '{' . $this->newline . '}' : $contents; $this->detectIndenting(); } @@ -41,37 +54,43 @@ class JsonManipulator public function addLink($type, $package, $constraint) { - // no link of that type yet - if (!preg_match('#"'.$type.'":\s*\{#', $this->contents)) { - $this->addMainKey($type, $this->format(array($package => $constraint))); + $decoded = JsonFile::parseJson($this->contents); - return true; + // no link of that type yet + if (!isset($decoded[$type])) { + return $this->addMainKey($type, array($package => $constraint)); } - $linksRegex = '#("'.$type.'":\s*\{)([^}]+)(\})#s'; - if (!preg_match($linksRegex, $this->contents, $match)) { + $regex = '{^(\s*\{\s*(?:'.self::$JSON_STRING.'\s*:\s*'.self::$JSON_VALUE.'\s*,\s*)*?)'. + '('.preg_quote(JsonFile::encode($type)).'\s*:\s*)('.self::$JSON_VALUE.')(.*)}s'; + if (!$this->pregMatch($regex, $this->contents, $matches)) { return false; } - $links = $match[2]; - $packageRegex = str_replace('/', '\\\\?/', preg_quote($package)); + $links = $matches[3]; - // link exists already - if (preg_match('{"'.$packageRegex.'"\s*:}i', $links)) { - $links = preg_replace('{"'.$packageRegex.'"(\s*:\s*)"[^"]+"}i', JsonFile::encode($package).'${1}"'.$constraint.'"', $links); - } elseif (preg_match('#[^\s](\s*)$#', $links, $match)) { - // link missing but non empty links - $links = preg_replace( - '#'.$match[1].'$#', - ',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $match[1], - $links - ); + if (isset($decoded[$type][$package])) { + // update existing link + $packageRegex = str_replace('/', '\\\\?/', preg_quote($package)); + // addcslashes is used to double up backslashes since preg_replace resolves them as back references otherwise, see #1588 + $links = preg_replace('{"'.$packageRegex.'"(\s*:\s*)'.self::$JSON_STRING.'}i', addcslashes(JsonFile::encode($package).'${1}"'.$constraint.'"', '\\'), $links); } else { - // links empty - $links = $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $links; + if ($this->pregMatch('#^\s*\{\s*\S+.*?(\s*\}\s*)$#s', $links, $match)) { + // link missing but non empty links + $links = preg_replace( + '{'.preg_quote($match[1]).'$}', + addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $match[1], '\\'), + $links + ); + } else { + // links empty + $links = '{' . $this->newline . + $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $this->newline . + $this->indent . '}'; + } } - $this->contents = preg_replace($linksRegex, '${1}'.$links.'$3', $this->contents); + $this->contents = $matches[1] . $matches[2] . $links . $matches[4]; return true; } @@ -98,118 +117,194 @@ class JsonManipulator public function addSubNode($mainNode, $name, $value) { + $decoded = JsonFile::parseJson($this->contents); + // no main node yet - if (!preg_match('#"'.$mainNode.'":\s*\{#', $this->contents)) { - $this->addMainKey(''.$mainNode.'', $this->format(array($name => $value))); + if (!isset($decoded[$mainNode])) { + $this->addMainKey($mainNode, array($name => $value)); return true; } + $subName = null; + 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 (!preg_match($nodeRegex, $this->contents, $match)) { + if (!$this->pregMatch($nodeRegex, $this->contents, $match)) { return false; } $children = $match[2]; // invalid match due to un-regexable content, abort - if (!json_decode('{'.$children.'}')) { + if (!@json_decode('{'.$children.'}')) { return false; } + $that = $this; + // child exists - if (preg_match('{("'.preg_quote($name).'"\s*:\s*)([0-9.]+|null|true|false|"[^"]+"|\[[^\]]*\]|\{'.self::$RECURSE_BLOCKS.'\})(,?)}', $children, $matches)) { - $children = preg_replace('{("'.preg_quote($name).'"\s*:\s*)([0-9.]+|null|true|false|"[^"]+"|\[[^\]]*\]|\{'.self::$RECURSE_BLOCKS.'\})(,?)}', '${1}'.$this->format($value, 1).'$3', $children); - } elseif (preg_match('#[^\s](\s*)$#', $children, $match)) { + if ($this->pregMatch('{("'.preg_quote($name).'"\s*:\s*)('.self::$JSON_VALUE.')(,?)}', $children, $matches)) { + $children = preg_replace_callback('{("'.preg_quote($name).'"\s*:\s*)('.self::$JSON_VALUE.')(,?)}', function ($matches) use ($name, $subName, $value, $that) { + if ($subName !== null) { + $curVal = json_decode($matches[2], true); + $curVal[$subName] = $value; + $value = $curVal; + } + + return $matches[1] . $that->format($value, 1) . $matches[3]; + }, $children); + } elseif ($this->pregMatch('#[^\s](\s*)$#', $children, $match)) { + if ($subName !== null) { + $value = array($subName => $value); + } + // child missing but non empty children $children = preg_replace( '#'.$match[1].'$#', - ',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $match[1], + addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $match[1], '\\'), $children ); } else { + if ($subName !== null) { + $value = array($subName => $value); + } + // children present but empty $children = $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $children; } - $this->contents = preg_replace($nodeRegex, '${1}'.$children.'$3', $this->contents); + $this->contents = preg_replace($nodeRegex, addcslashes('${1}'.$children.'$3', '\\'), $this->contents); return true; } public function removeSubNode($mainNode, $name) { - // no node - if (!preg_match('#"'.$mainNode.'":\s*\{#', $this->contents)) { - return true; - } + $decoded = JsonFile::parseJson($this->contents); - // empty node - if (preg_match('#"'.$mainNode.'":\s*\{\s*\}#s', $this->contents)) { + // no node or empty node + if (empty($decoded[$mainNode])) { return true; } // no node content match-able $nodeRegex = '#("'.$mainNode.'":\s*\{)('.self::$RECURSE_BLOCKS.')(\})#s'; - if (!preg_match($nodeRegex, $this->contents, $match)) { + if (!$this->pregMatch($nodeRegex, $this->contents, $match)) { return false; } $children = $match[2]; // invalid match due to un-regexable content, abort - if (!json_decode('{'.$children.'}')) { + if (!@json_decode('{'.$children.'}')) { return false; } - if (preg_match('{"'.preg_quote($name).'"\s*:}i', $children)) { - if (preg_match_all('{"'.preg_quote($name).'"\s*:\s*(?:[0-9.]+|null|true|false|"[^"]+"|\[[^\]]*\]|\{'.self::$RECURSE_BLOCKS.'\})}', $children, $matches)) { + $subName = null; + 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" + if (preg_match_all('{"'.preg_quote($name).'"\s*:\s*(?:'.self::$JSON_VALUE.')}', $children, $matches)) { $bestMatch = ''; foreach ($matches[0] as $match) { if (strlen($bestMatch) < strlen($match)) { $bestMatch = $match; } } - $children = preg_replace('{,\s*'.preg_quote($bestMatch).'}i', '', $children, -1, $count); + $childrenClean = preg_replace('{,\s*'.preg_quote($bestMatch).'}i', '', $children, -1, $count); if (1 !== $count) { - $children = preg_replace('{'.preg_quote($bestMatch).'\s*,?\s*}i', '', $children, -1, $count); + $childrenClean = preg_replace('{'.preg_quote($bestMatch).'\s*,?\s*}i', '', $childrenClean, -1, $count); if (1 !== $count) { return false; } } } + } else { + $childrenClean = $children; } - if (!trim($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); + // we have a subname, so we restore the rest of $name + if ($subName !== null) { + $curVal = json_decode('{'.$children.'}', true); + unset($curVal[$name][$subName]); + $this->addSubNode($mainNode, $name, $curVal[$name]); + } + return true; } - $this->contents = preg_replace($nodeRegex, '${1}'.$children.'$3', $this->contents); + $that = $this; + $this->contents = preg_replace_callback($nodeRegex, function ($matches) use ($that, $name, $subName, $childrenClean) { + if ($subName !== null) { + $curVal = json_decode('{'.$matches[2].'}', true); + unset($curVal[$name][$subName]); + $childrenClean = substr($that->format($curVal, 0), 1, -1); + } + + return $matches[1] . $childrenClean . $matches[3]; + }, $this->contents); return true; } public function addMainKey($key, $content) { - if (preg_match('#[^{\s](\s*)\}$#', $this->contents, $match)) { + $decoded = JsonFile::parseJson($this->contents); + $content = $this->format($content); + + // key exists already + $regex = '{^(\s*\{\s*(?:'.self::$JSON_STRING.'\s*:\s*'.self::$JSON_VALUE.'\s*,\s*)*?)'. + '('.preg_quote(JsonFile::encode($key)).'\s*:\s*'.self::$JSON_VALUE.')(.*)}s'; + if (isset($decoded[$key]) && $this->pregMatch($regex, $this->contents, $matches)) { + // invalid match due to un-regexable content, abort + if (!@json_decode('{'.$matches[2].'}')) { + return false; + } + + $this->contents = $matches[1] . JsonFile::encode($key).': '.$content . $matches[3]; + + return true; + } + + // append at the end of the file and keep whitespace + if ($this->pregMatch('#[^{\s](\s*)\}$#', $this->contents, $match)) { $this->contents = preg_replace( '#'.$match[1].'\}$#', - ',' . $this->newline . $this->indent . JsonFile::encode($key). ': '. $content . $this->newline . '}', - $this->contents - ); - } else { - $this->contents = preg_replace( - '#\}$#', - $this->indent . JsonFile::encode($key). ': '.$content . $this->newline . '}', + addcslashes(',' . $this->newline . $this->indent . JsonFile::encode($key). ': '. $content . $this->newline . '}', '\\'), $this->contents ); + + return true; } + + // append at the end of the file + $this->contents = preg_replace( + '#\}$#', + addcslashes($this->indent . JsonFile::encode($key). ': '.$content . $this->newline . '}', '\\'), + $this->contents + ); + + return true; } - protected function format($data, $depth = 0) + public function format($data, $depth = 0) { if (is_array($data)) { reset($data); @@ -223,6 +318,7 @@ class JsonManipulator } $out = '{' . $this->newline; + $elems = array(); foreach ($data as $key => $val) { $elems[] = str_repeat($this->indent, $depth + 2) . JsonFile::encode($key). ': '.$this->format($val, $depth + 1); } @@ -235,10 +331,36 @@ class JsonManipulator protected function detectIndenting() { - if (preg_match('{^(\s+)"}', $this->contents, $match)) { + if ($this->pregMatch('{^(\s+)"}m', $this->contents, $match)) { $this->indent = $match[1]; } else { $this->indent = ' '; } } + + protected function pregMatch($re, $str, &$matches = array()) + { + $count = preg_match($re, $str, $matches); + + if ($count === false) { + switch (preg_last_error()) { + case PREG_NO_ERROR: + throw new \RuntimeException('Failed to execute regex: PREG_NO_ERROR'); + case PREG_INTERNAL_ERROR: + throw new \RuntimeException('Failed to execute regex: PREG_INTERNAL_ERROR'); + case PREG_BACKTRACK_LIMIT_ERROR: + throw new \RuntimeException('Failed to execute regex: PREG_BACKTRACK_LIMIT_ERROR'); + case PREG_RECURSION_LIMIT_ERROR: + throw new \RuntimeException('Failed to execute regex: PREG_RECURSION_LIMIT_ERROR'); + case PREG_BAD_UTF8_ERROR: + throw new \RuntimeException('Failed to execute regex: PREG_BAD_UTF8_ERROR'); + case PREG_BAD_UTF8_OFFSET_ERROR: + throw new \RuntimeException('Failed to execute regex: PREG_BAD_UTF8_OFFSET_ERROR'); + default: + throw new \RuntimeException('Failed to execute regex: Unknown error'); + } + } + + return $count; + } } diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php index e2f748092..183f2c740 100644 --- a/src/Composer/Package/AliasPackage.php +++ b/src/Composer/Package/AliasPackage.php @@ -160,6 +160,8 @@ class AliasPackage extends BasePackage implements CompletePackageInterface * Use by the policy for sorting manually aliased packages first, see #576 * * @param bool $value + * + * @return mixed */ public function setRootPackageAlias($value) { @@ -175,22 +177,6 @@ class AliasPackage extends BasePackage implements CompletePackageInterface return $this->rootPackageAlias; } - /** - * {@inheritDoc} - */ - public function getAlias() - { - return ''; - } - - /** - * {@inheritDoc} - */ - public function getPrettyAlias() - { - return ''; - } - /*************************************** * Wrappers around the aliased package * ***************************************/ @@ -223,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(); @@ -231,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(); @@ -239,25 +237,41 @@ 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 getScripts() + public function setTransportOptions(array $options) { - return $this->aliasOf->getScripts(); + return $this->aliasOf->setTransportOptions($options); + } + public function getTransportOptions() + { + return $this->aliasOf->getTransportOptions(); } - public function setAliases(array $aliases) + public function setDistMirrors($mirrors) { - return $this->aliasOf->setAliases($aliases); + return $this->aliasOf->setDistMirrors($mirrors); } - public function getAliases() + public function getDistMirrors() { - return $this->aliasOf->getAliases(); + return $this->aliasOf->getDistMirrors(); + } + public function getScripts() + { + return $this->aliasOf->getScripts(); } public function getLicense() { @@ -267,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(); @@ -311,6 +329,10 @@ class AliasPackage extends BasePackage implements CompletePackageInterface { return $this->aliasOf->getNotificationUrl(); } + public function getArchiveExcludes() + { + return $this->aliasOf->getArchiveExcludes(); + } 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 new file mode 100644 index 000000000..44c682616 --- /dev/null +++ b/src/Composer/Package/Archiver/ArchivableFilesFinder.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\Package\Archiver; + +use Composer\Util\Filesystem; + +use Symfony\Component\Finder; + +/** + * A Symfony Finder wrapper which locates files that should go into archives + * + * Handles .gitignore, .gitattributes and .hgignore files as well as composer's + * own exclude rules from composer.json + * + * @author Nils Adermann + */ +class ArchivableFilesFinder extends \FilterIterator +{ + /** + * @var Symfony\Component\Finder\Finder + */ + protected $finder; + + /** + * Initializes the internal Symfony Finder with appropriate filters + * + * @param string $sources Path to source files to be archived + * @param array $excludes Composer's own exclude rules from composer.json + */ + public function __construct($sources, array $excludes) + { + $fs = new Filesystem(); + + $sources = $fs->normalizePath($sources); + + $filters = array( + new HgExcludeFilter($sources), + new GitExcludeFilter($sources), + new ComposerExcludeFilter($sources, $excludes), + ); + + $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, '#').'#', + '', + $fs->normalizePath($file->getRealPath()) + ); + + $exclude = false; + foreach ($filters as $filter) { + $exclude = $filter->filter($relativePath, $exclude); + } + + return !$exclude; + }; + + if (method_exists($filter, 'bindTo')) { + $filter = $filter->bindTo(null); + } + + $this->finder + ->in($sources) + ->filter($filter) + ->ignoreVCS(true) + ->ignoreDotFiles(false); + + parent::__construct($this->finder->getIterator()); + } + + public function accept() + { + return !$this->getInnerIterator()->current()->isDir(); + } +} diff --git a/src/Composer/Package/Archiver/ArchiveManager.php b/src/Composer/Package/Archiver/ArchiveManager.php new file mode 100644 index 000000000..55d87c1a3 --- /dev/null +++ b/src/Composer/Package/Archiver/ArchiveManager.php @@ -0,0 +1,170 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package\Archiver; + +use Composer\Downloader\DownloadManager; +use Composer\Package\PackageInterface; +use Composer\Package\RootPackageInterface; +use Composer\Util\Filesystem; +use Composer\Json\JsonFile; + +/** + * @author Matthieu Moquet + * @author Till Klampaeckel + */ +class ArchiveManager +{ + protected $downloadManager; + + protected $archivers = array(); + + /** + * @var bool + */ + protected $overwriteFiles = true; + + /** + * @param DownloadManager $downloadManager A manager used to download package sources + */ + public function __construct(DownloadManager $downloadManager) + { + $this->downloadManager = $downloadManager; + } + + /** + * @param ArchiverInterface $archiver + */ + public function addArchiver(ArchiverInterface $archiver) + { + $this->archivers[] = $archiver; + } + + /** + * Set whether existing archives should be overwritten + * + * @param bool $overwriteFiles New setting + * + * @return $this + */ + public function setOverwriteFiles($overwriteFiles) + { + $this->overwriteFiles = $overwriteFiles; + + return $this; + } + + /** + * Generate a distinct filename for a particular version of a package. + * + * @param PackageInterface $package The package to get a name for + * + * @return string A filename without an extension + */ + public function getPackageFilename(PackageInterface $package) + { + $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())); + } else { + $nameParts = array_merge($nameParts, array($package->getPrettyVersion(), $package->getDistReference())); + } + + if ($package->getSourceReference()) { + $nameParts[] = substr(sha1($package->getSourceReference()), 0, 6); + } + + $name = implode('-', array_filter($nameParts, function ($p) { + return !empty($p); + })); + + return str_replace('/', '-', $name); + } + + /** + * Create an archive of the specified package. + * + * @param PackageInterface $package The package to archive + * @param string $format The format of the archive (zip, tar, ...) + * @param string $targetDir The diretory where to build the archive + * @throws \InvalidArgumentException + * @throws \RuntimeException + * @return string The path of the created archive + */ + public function archive(PackageInterface $package, $format, $targetDir) + { + if (empty($format)) { + throw new \InvalidArgumentException('Format must be specified'); + } + + // Search for the most appropriate archiver + $usableArchiver = null; + foreach ($this->archivers as $archiver) { + if ($archiver->supports($format, $package->getSourceType())) { + $usableArchiver = $archiver; + break; + } + } + + // Checks the format/source type are supported before downloading the package + if (null === $usableArchiver) { + throw new \RuntimeException(sprintf('No archiver found to support %s format', $format)); + } + + $filesystem = new Filesystem(); + $packageName = $this->getPackageFilename($package); + + // Archive filename + $filesystem->ensureDirectoryExists($targetDir); + $target = realpath($targetDir).'/'.$packageName.'.'.$format; + $filesystem->ensureDirectoryExists(dirname($target)); + + if (!$this->overwriteFiles && file_exists($target)) { + return $target; + } + + if ($package instanceof RootPackageInterface) { + $sourcePath = realpath('.'); + } else { + // Directory used to download the sources + $sourcePath = sys_get_temp_dir().'/composer_archiver/arch'.uniqid(); + $filesystem->ensureDirectoryExists($sourcePath); + + // Download sources + $this->downloadManager->download($package, $sourcePath); + + // Check exclude from downloaded composer.json + if (file_exists($composerJsonPath = $sourcePath.'/composer.json')) { + $jsonFile = new JsonFile($composerJsonPath); + $jsonData = $jsonFile->read(); + if (!empty($jsonData['archive']['exclude'])) { + $package->setArchiveExcludes($jsonData['archive']['exclude']); + } + } + } + + // Create the archive + $tempTarget = sys_get_temp_dir().'/composer_archiver/arch'.uniqid().'.'.$format; + $filesystem->ensureDirectoryExists(dirname($tempTarget)); + + $archivePath = $usableArchiver->archive($sourcePath, $tempTarget, $format, $package->getArchiveExcludes()); + rename($archivePath, $target); + + // cleanup temporary download + if (!$package instanceof RootPackageInterface) { + $filesystem->removeDirectory($sourcePath); + } + + return $target; + } +} diff --git a/src/Composer/Package/Archiver/ArchiverInterface.php b/src/Composer/Package/Archiver/ArchiverInterface.php new file mode 100644 index 000000000..72a06c1c9 --- /dev/null +++ b/src/Composer/Package/Archiver/ArchiverInterface.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\Package\Archiver; + +/** + * @author Till Klampaeckel + * @author Matthieu Moquet + * @author Nils Adermann + */ +interface ArchiverInterface +{ + /** + * Create an archive from the sources. + * + * @param string $sources The sources directory + * @param string $target The target file + * @param string $format The format used for archive + * @param array $excludes A list of patterns for files to exclude + * + * @return string The path to the written archive file + */ + public function archive($sources, $target, $format, array $excludes = array()); + + /** + * Format supported by the archiver. + * + * @param string $format The archive format + * @param string $sourceType The source type (git, svn, hg, etc.) + * + * @return boolean true if the format is supported by the archiver + */ + public function supports($format, $sourceType); +} diff --git a/src/Composer/Package/Archiver/BaseExcludeFilter.php b/src/Composer/Package/Archiver/BaseExcludeFilter.php new file mode 100644 index 000000000..d724f31e4 --- /dev/null +++ b/src/Composer/Package/Archiver/BaseExcludeFilter.php @@ -0,0 +1,148 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package\Archiver; + +use Symfony\Component\Finder; + +/** + * @author Nils Adermann + */ +abstract class BaseExcludeFilter +{ + /** + * @var string + */ + protected $sourcePath; + + /** + * @var array + */ + protected $excludePatterns; + + /** + * @param string $sourcePath Directory containing sources to be filtered + */ + public function __construct($sourcePath) + { + $this->sourcePath = $sourcePath; + $this->excludePatterns = array(); + } + + /** + * Checks the given path against all exclude patterns in this filter + * + * Negated patterns overwrite exclude decisions of previous filters. + * + * @param string $relativePath The file's path relative to the sourcePath + * @param bool $exclude Whether a previous filter wants to exclude this file + * + * @return bool Whether the file should be excluded + */ + public function filter($relativePath, $exclude) + { + foreach ($this->excludePatterns as $patternData) { + list($pattern, $negate, $stripLeadingSlash) = $patternData; + + if ($stripLeadingSlash) { + $path = substr($relativePath, 1); + } else { + $path = $relativePath; + } + + if (preg_match($pattern, $path)) { + $exclude = !$negate; + } + } + + return $exclude; + } + + /** + * Processes a file containing exclude rules of different formats per line + * + * @param array $lines A set of lines to be parsed + * @param callback $lineParser The parser to be used on each line + * + * @return array Exclude patterns to be used in filter() + */ + protected function parseLines(array $lines, $lineParser) + { + return array_filter( + array_map( + function ($line) use ($lineParser) { + $line = trim($line); + + if (!$line || 0 === strpos($line, '#')) { + return; + } + + return call_user_func($lineParser, $line); + }, + $lines + ), + function ($pattern) { + return $pattern !== null; + } + ); + } + + /** + * Generates a set of exclude patterns for filter() from gitignore rules + * + * @param array $rules A list of exclude rules in gitignore syntax + * + * @return array Exclude patterns + */ + protected function generatePatterns($rules) + { + $patterns = array(); + foreach ($rules as $rule) { + $patterns[] = $this->generatePattern($rule); + } + + return $patterns; + } + + /** + * Generates an exclude pattern for filter() from a gitignore rule + * + * @param string $rule An exclude rule in gitignore syntax + * + * @return array An exclude pattern + */ + protected function generatePattern($rule) + { + $negate = false; + $pattern = '#'; + + if (strlen($rule) && $rule[0] === '!') { + $negate = true; + $rule = substr($rule, 1); + } + + if (strlen($rule) && $rule[0] === '/') { + $pattern .= '^/'; + $rule = substr($rule, 1); + } elseif (strlen($rule) - 1 === strpos($rule, '/')) { + $pattern .= '/'; + $rule = substr($rule, 0, -1); + } elseif (false === strpos($rule, '/')) { + $pattern .= '/'; + } + + // 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/Archiver/ComposerExcludeFilter.php b/src/Composer/Package/Archiver/ComposerExcludeFilter.php new file mode 100644 index 000000000..9e663869c --- /dev/null +++ b/src/Composer/Package/Archiver/ComposerExcludeFilter.php @@ -0,0 +1,31 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package\Archiver; + +/** + * An exclude filter which processes composer's own exclude rules + * + * @author Nils Adermann + */ +class ComposerExcludeFilter extends BaseExcludeFilter +{ + /** + * @param string $sourcePath Directory containing sources to be filtered + * @param array $excludeRules An array of exclude rules from composer.json + */ + public function __construct($sourcePath, array $excludeRules) + { + parent::__construct($sourcePath); + $this->excludePatterns = $this->generatePatterns($excludeRules); + } +} diff --git a/src/Composer/Package/Archiver/GitExcludeFilter.php b/src/Composer/Package/Archiver/GitExcludeFilter.php new file mode 100644 index 000000000..926bb4d69 --- /dev/null +++ b/src/Composer/Package/Archiver/GitExcludeFilter.php @@ -0,0 +1,80 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package\Archiver; + +/** + * An exclude filter that processes gitignore and gitattributes + * + * It respects export-ignore git attributes + * + * @author Nils Adermann + */ +class GitExcludeFilter extends BaseExcludeFilter +{ + /** + * Parses .gitignore and .gitattributes files if they exist + * + * @param string $sourcePath + */ + public function __construct($sourcePath) + { + parent::__construct($sourcePath); + + if (file_exists($sourcePath.'/.gitignore')) { + $this->excludePatterns = $this->parseLines( + file($sourcePath.'/.gitignore'), + array($this, 'parseGitIgnoreLine') + ); + } + if (file_exists($sourcePath.'/.gitattributes')) { + $this->excludePatterns = array_merge( + $this->excludePatterns, + $this->parseLines( + file($sourcePath.'/.gitattributes'), + array($this, 'parseGitAttributesLine') + )); + } + } + + /** + * Callback line parser which process gitignore lines + * + * @param string $line A line from .gitignore + * + * @return array An exclude pattern for filter() + */ + public function parseGitIgnoreLine($line) + { + return $this->generatePattern($line); + } + + /** + * Callback parser which finds export-ignore rules in git attribute lines + * + * @param string $line A line from .gitattributes + * + * @return array An exclude pattern for filter() + */ + public function parseGitAttributesLine($line) + { + $parts = preg_split('#\s+#', $line); + + if (count($parts) != 2) { + return null; + } + + if ($parts[1] === 'export-ignore') { + return $this->generatePattern($parts[0]); + } + } +} diff --git a/src/Composer/Package/Archiver/HgExcludeFilter.php b/src/Composer/Package/Archiver/HgExcludeFilter.php new file mode 100644 index 000000000..fa2327c5f --- /dev/null +++ b/src/Composer/Package/Archiver/HgExcludeFilter.php @@ -0,0 +1,107 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package\Archiver; + +use Symfony\Component\Finder; + +/** + * An exclude filter that processes hgignore files + * + * @author Nils Adermann + */ +class HgExcludeFilter extends BaseExcludeFilter +{ + const HG_IGNORE_REGEX = 1; + const HG_IGNORE_GLOB = 2; + + /** + * Either HG_IGNORE_REGEX or HG_IGNORE_GLOB + * @var integer + */ + protected $patternMode; + + /** + * Parses .hgignore file if it exist + * + * @param string $sourcePath + */ + public function __construct($sourcePath) + { + parent::__construct($sourcePath); + + $this->patternMode = self::HG_IGNORE_REGEX; + + if (file_exists($sourcePath.'/.hgignore')) { + $this->excludePatterns = $this->parseLines( + file($sourcePath.'/.hgignore'), + array($this, 'parseHgIgnoreLine') + ); + } + } + + /** + * Callback line parser which process hgignore lines + * + * @param string $line A line from .hgignore + * + * @return array An exclude pattern for filter() + */ + public function parseHgIgnoreLine($line) + { + if (preg_match('#^syntax\s*:\s*(glob|regexp)$#', $line, $matches)) { + if ($matches[1] === 'glob') { + $this->patternMode = self::HG_IGNORE_GLOB; + } else { + $this->patternMode = self::HG_IGNORE_REGEX; + } + + return null; + } + + if ($this->patternMode == self::HG_IGNORE_GLOB) { + return $this->patternFromGlob($line); + } else { + return $this->patternFromRegex($line); + } + } + + /** + * Generates an exclude pattern for filter() from a hg glob expression + * + * @param string $line A line from .hgignore in glob mode + * + * @return array An exclude pattern for filter() + */ + protected function patternFromGlob($line) + { + $pattern = '#'.substr(Finder\Glob::toRegex($line), 2, -1).'#'; + $pattern = str_replace('[^/]*', '.*', $pattern); + + return array($pattern, false, true); + } + + /** + * Generates an exclude pattern for filter() from a hg regexp expression + * + * @param string $line A line from .hgignore in regexp mode + * + * @return array An exclude pattern for filter() + */ + public function patternFromRegex($line) + { + // WTF need to escape the delimiter safely + $pattern = '#'.preg_replace('/((?:\\\\\\\\)*)(\\\\?)#/', '\1\2\2\\#', $line).'#'; + + return array($pattern, false, true); + } +} diff --git a/src/Composer/Package/Archiver/PharArchiver.php b/src/Composer/Package/Archiver/PharArchiver.php new file mode 100644 index 000000000..75b9843f3 --- /dev/null +++ b/src/Composer/Package/Archiver/PharArchiver.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\Package\Archiver; + +/** + * @author Till Klampaeckel + * @author Nils Adermann + * @author Matthieu Moquet + */ +class PharArchiver implements ArchiverInterface +{ + protected static $formats = array( + 'zip' => \Phar::ZIP, + 'tar' => \Phar::TAR, + ); + + /** + * {@inheritdoc} + */ + public function archive($sources, $target, $format, array $excludes = array()) + { + $sources = realpath($sources); + + // Phar would otherwise load the file which we don't want + if (file_exists($target)) { + unlink($target); + } + + try { + $phar = new \PharData($target, null, null, static::$formats[$format]); + $files = new ArchivableFilesFinder($sources, $excludes); + $phar->buildFromIterator($files, $sources); + + return $target; + } catch (\UnexpectedValueException $e) { + $message = sprintf("Could not create archive '%s' from '%s': %s", + $target, + $sources, + $e->getMessage() + ); + + throw new \RuntimeException($message, $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + */ + public function supports($format, $sourceType) + { + return isset(static::$formats[$format]); + } +} diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index b28de157d..d756196ef 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -49,6 +49,7 @@ abstract class BasePackage implements PackageInterface protected $repository; protected $id; + protected $transportOptions; /** * All descendants' constructors should call this parent constructor @@ -60,6 +61,7 @@ abstract class BasePackage implements PackageInterface $this->prettyName = $name; $this->name = strtolower($name); $this->id = -1; + $this->transportOptions = array(); } /** @@ -114,17 +116,41 @@ abstract class BasePackage implements PackageInterface return $this->id; } + /** + * {@inheritDoc} + */ + public function setRepository(RepositoryInterface $repository) + { + if ($this->repository && $repository !== $this->repository) { + throw new \LogicException('A package can only be added to one repository'); + } + $this->repository = $repository; + } + + /** + * {@inheritDoc} + */ public function getRepository() { return $this->repository; } - public function setRepository(RepositoryInterface $repository) + /** + * {@inheritDoc} + */ + public function getTransportOptions() { - if ($this->repository) { - throw new \LogicException('A package can only be added to one repository'); - } - $this->repository = $repository; + return $this->transportOptions; + } + + /** + * Configures the list of options to download package dist files + * + * @param array $options + */ + public function setTransportOptions(array $options) + { + $this->transportOptions = $options; } /** diff --git a/src/Composer/Package/CompletePackage.php b/src/Composer/Package/CompletePackage.php index 418c87d9e..a884174af 100644 --- a/src/Composer/Package/CompletePackage.php +++ b/src/Composer/Package/CompletePackage.php @@ -47,7 +47,7 @@ class CompletePackage extends Package implements CompletePackageInterface /** * Set the repositories * - * @param string $repositories + * @param array $repositories */ public function setRepositories($repositories) { diff --git a/src/Composer/Package/Dumper/ArrayDumper.php b/src/Composer/Package/Dumper/ArrayDumper.php index 47167dd23..67318c04a 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,13 @@ 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()) { + $data['archive']['exclude'] = $package->getArchiveExcludes(); } foreach (BasePackage::$supportedLinkTypes as $type => $opts) { @@ -62,10 +73,12 @@ class ArrayDumper foreach ($links as $link) { $data[$type][$link->getTarget()] = $link->getPrettyConstraint(); } + ksort($data[$type]); } } if ($packages = $package->getSuggests()) { + ksort($packages); $data['suggest'] = $packages; } @@ -88,6 +101,10 @@ class ArrayDumper ); $data = $this->dumpValues($package, $keys, $data); + + if (isset($data['keywords']) && is_array($data['keywords'])) { + sort($data['keywords']); + } } if ($package instanceof RootPackageInterface) { @@ -97,6 +114,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/EmptyConstraint.php b/src/Composer/Package/LinkConstraint/EmptyConstraint.php new file mode 100644 index 000000000..ca1c75ff5 --- /dev/null +++ b/src/Composer/Package/LinkConstraint/EmptyConstraint.php @@ -0,0 +1,47 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package\LinkConstraint; + +/** + * Defines an absence of constraints + * + * @author Jordi Boggiano + */ +class EmptyConstraint implements LinkConstraintInterface +{ + protected $prettyString; + + public function matches(LinkConstraintInterface $provider) + { + return true; + } + + public function setPrettyString($prettyString) + { + $this->prettyString = $prettyString; + } + + public function getPrettyString() + { + if ($this->prettyString) { + return $this->prettyString; + } + + return $this->__toString(); + } + + public function __toString() + { + return '[]'; + } +} diff --git a/src/Composer/Package/LinkConstraint/MultiConstraint.php b/src/Composer/Package/LinkConstraint/MultiConstraint.php index 836d565a0..f2eff93ec 100644 --- a/src/Composer/Package/LinkConstraint/MultiConstraint.php +++ b/src/Composer/Package/LinkConstraint/MultiConstraint.php @@ -13,27 +13,41 @@ namespace Composer\Package\LinkConstraint; /** - * Defines a conjunctive set of constraints on the target of a package link + * Defines a conjunctive or disjunctive set of constraints on the target of a package link * * @author Nils Adermann + * @author Jordi Boggiano */ class MultiConstraint implements LinkConstraintInterface { protected $constraints; protected $prettyString; + protected $conjunctive; /** * Sets operator and version to compare a package with * - * @param array $constraints A conjunctive set of constraints + * @param array $constraints A set of constraints + * @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive */ - public function __construct(array $constraints) + public function __construct(array $constraints, $conjunctive = true) { $this->constraints = $constraints; + $this->conjunctive = $conjunctive; } public function matches(LinkConstraintInterface $provider) { + if (false === $this->conjunctive) { + foreach ($this->constraints as $constraint) { + if ($constraint->matches($provider)) { + return true; + } + } + + return false; + } + foreach ($this->constraints as $constraint) { if (!$constraint->matches($provider)) { return false; @@ -64,6 +78,6 @@ class MultiConstraint implements LinkConstraintInterface $constraints[] = $constraint->__toString(); } - return '['.implode(', ', $constraints).']'; + return '['.implode($this->conjunctive ? ', ' : ' | ', $constraints).']'; } } diff --git a/src/Composer/Package/LinkConstraint/VersionConstraint.php b/src/Composer/Package/LinkConstraint/VersionConstraint.php index 8eb69dfd3..cd2336227 100644 --- a/src/Composer/Package/LinkConstraint/VersionConstraint.php +++ b/src/Composer/Package/LinkConstraint/VersionConstraint.php @@ -44,20 +44,44 @@ class VersionConstraint extends SpecificConstraint $this->version = $version; } - public function versionCompare($a, $b, $operator) + public function versionCompare($a, $b, $operator, $compareBranches = false) { - if ('dev-' === substr($a, 0, 4) && 'dev-' === substr($b, 0, 4)) { + $aIsBranch = 'dev-' === substr($a, 0, 4); + $bIsBranch = 'dev-' === substr($b, 0, 4); + if ($aIsBranch && $bIsBranch) { return $operator == '==' && $a === $b; } + // when branches are not comparable, we make sure dev branches never match anything + if (!$compareBranches && ($aIsBranch || $bIsBranch)) { + return false; + } + return version_compare($a, $b, $operator); } /** - * - * @param VersionConstraint $provider + * @param VersionConstraint $provider + * @param bool $compareBranches + * @return bool + */ + public function matchSpecific(VersionConstraint $provider, $compareBranches = false) + { + static $cache = array(); + if (isset($cache[$this->operator][$this->version][$provider->operator][$provider->version][$compareBranches])) { + return $cache[$this->operator][$this->version][$provider->operator][$provider->version][$compareBranches]; + } + + return $cache[$this->operator][$this->version][$provider->operator][$provider->version][$compareBranches] = + $this->doMatchSpecific($provider, $compareBranches); + } + + /** + * @param VersionConstraint $provider + * @param bool $compareBranches + * @return bool */ - public function matchSpecific(VersionConstraint $provider) + private function doMatchSpecific(VersionConstraint $provider, $compareBranches = false) { $noEqualOp = str_replace('=', '', $this->operator); $providerNoEqualOp = str_replace('=', '', $provider->operator); @@ -71,7 +95,7 @@ class VersionConstraint extends SpecificConstraint // these kinds of comparisons always have a solution if ($isNonEqualOp || $isProviderNonEqualOp) { return !$isEqualOp && !$isProviderEqualOp - || $this->versionCompare($provider->version, $this->version, '!='); + || $this->versionCompare($provider->version, $this->version, '!=', $compareBranches); } // an example for the condition is <= 2.0 & < 1.0 @@ -80,7 +104,7 @@ class VersionConstraint extends SpecificConstraint return true; } - if ($this->versionCompare($provider->version, $this->version, $this->operator)) { + if ($this->versionCompare($provider->version, $this->version, $this->operator, $compareBranches)) { // special case, e.g. require >= 1.0 and provide < 1.0 // 1.0 >= 1.0 but 1.0 is outside of the provided interval if ($provider->version == $this->version && $provider->operator == $providerNoEqualOp && $this->operator != $noEqualOp) { diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index 661013691..142b15c72 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -13,6 +13,9 @@ namespace Composer\Package\Loader; use Composer\Package; +use Composer\Package\AliasPackage; +use Composer\Package\RootAliasPackage; +use Composer\Package\RootPackageInterface; use Composer\Package\Version\VersionParser; /** @@ -22,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') @@ -72,23 +77,28 @@ class ArrayLoader implements LoaderInterface } if (isset($config['source'])) { - if (!isset($config['source']['type']) || !isset($config['source']['url'])) { + if (!isset($config['source']['type']) || !isset($config['source']['url']) || !isset($config['source']['reference'])) { throw new \UnexpectedValueException(sprintf( - "package source should be specified as {\"type\": ..., \"url\": ...},\n%s given", + "Package %s's source key should be specified as {\"type\": ..., \"url\": ..., \"reference\": ...},\n%s given.", + $config['name'], json_encode($config['source']) )); } $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'])) { if (!isset($config['dist']['type']) || !isset($config['dist']['url'])) { throw new \UnexpectedValueException(sprintf( - "package dist should be specified as ". - "{\"type\": ..., \"url\": ..., \"reference\": ..., \"shasum\": ...},\n%s given", + "Package %s's dist key should be specified as ". + "{\"type\": ..., \"url\": ..., \"reference\": ..., \"shasum\": ...},\n%s given.", + $config['name'], json_encode($config['dist']) )); } @@ -96,11 +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 ($aliasNormalized = $this->getBranchAlias($config)) { - $package->setAlias($aliasNormalized); - $package->setPrettyAlias(preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); + if (isset($config['dist']['mirrors'])) { + $package->setDistMirrors($config['dist']['mirrors']); + } } foreach (Package\BasePackage::$supportedLinkTypes as $type => $opts) { @@ -130,13 +138,19 @@ 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']); } if (!empty($config['time'])) { + $time = ctype_digit($config['time']) ? '@'.$config['time'] : $config['time']; + try { - $date = new \DateTime($config['time'], new \DateTimeZone('UTC')); + $date = new \DateTime($time, new \DateTimeZone('UTC')); $package->setReleaseDate($date); } catch (\Exception $e) { } @@ -146,10 +160,14 @@ class ArrayLoader implements LoaderInterface $package->setNotificationUrl($config['notification-url']); } + if (!empty($config['archive']['exclude'])) { + $package->setArchiveExcludes($config['archive']['exclude']); + } + if ($package instanceof Package\CompletePackageInterface) { if (isset($config['scripts']) && is_array($config['scripts'])) { foreach ($config['scripts'] as $event => $listeners) { - $config['scripts'][$event]= (array) $listeners; + $config['scripts'][$event] = (array) $listeners; } $package->setScripts($config['scripts']); } @@ -179,6 +197,18 @@ class ArrayLoader implements LoaderInterface } } + if ($aliasNormalized = $this->getBranchAlias($config)) { + if ($package instanceof RootPackageInterface) { + $package = new RootAliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); + } else { + $package = new AliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized)); + } + } + + if ($this->loadOptions && isset($config['transport-options'])) { + $package->setTransportOptions($config['transport-options']); + } + return $package; } diff --git a/src/Composer/Package/Loader/RootPackageLoader.php b/src/Composer/Package/Loader/RootPackageLoader.php index 22275d988..64880d727 100644 --- a/src/Composer/Package/Loader/RootPackageLoader.php +++ b/src/Composer/Package/Loader/RootPackageLoader.php @@ -13,11 +13,16 @@ namespace Composer\Package\Loader; use Composer\Package\BasePackage; +use Composer\Package\AliasPackage; use Composer\Config; use Composer\Factory; use Composer\Package\Version\VersionParser; use Composer\Repository\RepositoryManager; +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 @@ -58,11 +63,17 @@ class RootPackageLoader extends ArrayLoader } $config['version'] = $version; - } else { - $version = $config['version']; } - $package = parent::load($config, $class); + $realPackage = $package = parent::load($config, $class); + + if ($realPackage instanceof AliasPackage) { + $realPackage = $package->getAliasOf(); + } + + if (isset($config['minimum-stability'])) { + $realPackage->setMinimumStability(VersionParser::normalizeStability($config['minimum-stability'])); + } $aliases = array(); $stabilityFlags = array(); @@ -72,28 +83,28 @@ class RootPackageLoader extends ArrayLoader $linkInfo = BasePackage::$supportedLinkTypes[$linkType]; $method = 'get'.ucfirst($linkInfo['method']); $links = array(); - foreach ($package->$method() as $link) { + foreach ($realPackage->$method() as $link) { $links[$link->getTarget()] = $link->getConstraint()->getPrettyString(); } $aliases = $this->extractAliases($links, $aliases); - $stabilityFlags = $this->extractStabilityFlags($links, $stabilityFlags); + $stabilityFlags = $this->extractStabilityFlags($links, $stabilityFlags, $realPackage->getMinimumStability()); $references = $this->extractReferences($links, $references); } } - $package->setAliases($aliases); - $package->setStabilityFlags($stabilityFlags); - $package->setReferences($references); + $realPackage->setAliases($aliases); + $realPackage->setStabilityFlags($stabilityFlags); + $realPackage->setReferences($references); - if (isset($config['minimum-stability'])) { - $package->setMinimumStability(VersionParser::normalizeStability($config['minimum-stability'])); + if (isset($config['prefer-stable'])) { + $realPackage->setPreferStable((bool) $config['prefer-stable']); } $repos = Factory::createDefaultRepositories(null, $this->config, $this->manager); foreach ($repos as $repo) { $this->manager->addRepository($repo); } - $package->setRepositories($this->config->getRepositories()); + $realPackage->setRepositories($this->config->getRepositories()); return $package; } @@ -101,7 +112,7 @@ class RootPackageLoader extends ArrayLoader private function extractAliases(array $requires, array $aliases) { foreach ($requires as $reqName => $reqVersion) { - if (preg_match('{^([^,\s]+) +as +([^,\s]+)$}', $reqVersion, $match)) { + if (preg_match('{^([^,\s#]+)(?:#[^ ]+)? +as +([^,\s]+)$}', $reqVersion, $match)) { $aliases[] = array( 'package' => strtolower($reqName), 'version' => $this->versionParser->normalize($match[1], $reqVersion), @@ -114,11 +125,12 @@ class RootPackageLoader extends ArrayLoader return $aliases; } - private function extractStabilityFlags(array $requires, array $stabilityFlags) + private function extractStabilityFlags(array $requires, array $stabilityFlags, $minimumStability) { $stabilities = BasePackage::$stabilities; + $minimumStability = $stabilities[$minimumStability]; foreach ($requires as $reqName => $reqVersion) { - // parse explicit stability flags + // parse explicit stability flags to the most unstable if (preg_match('{^[^,\s]*?@('.implode('|', array_keys($stabilities)).')$}i', $reqVersion, $match)) { $name = strtolower($reqName); $stability = $stabilities[VersionParser::normalizeStability($match[1])]; @@ -131,12 +143,13 @@ class RootPackageLoader extends ArrayLoader continue; } - // infer flags for requirements that have an explicit -dev or -beta version specified for example + // infer flags for requirements that have an explicit -dev or -beta version specified but only + // for those that are more unstable than the minimumStability or existing flags $reqVersion = preg_replace('{^([^,\s@]+) as .+$}', '$1', $reqVersion); if (preg_match('{^[^,\s@]+$}', $reqVersion) && 'stable' !== ($stabilityName = VersionParser::parseStability($reqVersion))) { $name = strtolower($reqName); $stability = $stabilities[$stabilityName]; - if (isset($stabilityFlags[$name]) && $stabilityFlags[$name] > $stability) { + if ((isset($stabilityFlags[$name]) && $stabilityFlags[$name] > $stability) || ($minimumStability > $stability)) { continue; } $stabilityFlags[$name] = $stability; @@ -161,16 +174,43 @@ class RootPackageLoader extends ArrayLoader private function guessVersion(array $config) { + if (function_exists('proc_open')) { + $version = $this->guessGitVersion($config); + if (null !== $version) { + return $version; + } + + $version = $this->guessHgVersion($config); + if (null !== $version) { + return $version; + } + + return $this->guessSvnVersion($config); + } + } + + private function guessGitVersion(array $config) + { + GitUtil::cleanEnv(); + + // try to fetch current version from git tags + if (0 === $this->process->execute('git describe --exact-match --tags', $output)) { + try { + return $this->versionParser->normalize(trim($output)); + } catch (\Exception $e) { + } + } + // try to fetch current version from git branch - if (function_exists('proc_open') && 0 === $this->process->execute('git branch --no-color --no-abbrev -v', $output)) { + if (0 === $this->process->execute('git branch --no-color --no-abbrev -v', $output)) { $branches = array(); $isFeatureBranch = false; $version = null; // find current branch and collect all branch names foreach ($this->process->splitLines($output) as $branch) { - if ($branch && preg_match('{^(?:\* ) *(?:[^/ ]+?/)?(\S+|\(no branch\)) *([a-f0-9]+) .*$}', $branch, $match)) { - if ($match[1] === '(no branch)') { + 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; } else { @@ -183,7 +223,7 @@ class RootPackageLoader extends ArrayLoader } if ($branch && !preg_match('{^ *[^/]+/HEAD }', $branch)) { - if (preg_match('{^(?:\* )? *(?:[^/ ]+?/)?(\S+) *([a-f0-9]+) .*$}', $branch, $match)) { + if (preg_match('{^(?:\* )? *(\S+) *([a-f0-9]+) .*$}', $branch, $match)) { $branches[] = $match[1]; } } @@ -193,32 +233,99 @@ class RootPackageLoader extends ArrayLoader return $version; } - // ignore feature branches if they have no branch-alias or self.version is used - // and find the branch they came from to use as a version instead - if ((isset($config['extra']['branch-alias']) && !isset($config['extra']['branch-alias'][$version])) - || strpos(json_encode($config), '"self.version"') - ) { - $branch = preg_replace('{^dev-}', '', $version); - $length = PHP_INT_MAX; - foreach ($branches as $candidate) { - // do not compare against other feature branches - if ($candidate === $branch || !preg_match('{^(master|trunk|default|develop|\d+\..+)$}', $candidate, $match)) { - continue; - } - if (0 !== $this->process->execute('git rev-list '.$candidate.'..'.$branch, $output)) { - continue; - } - if (strlen($output) < $length) { - $length = strlen($output); - $version = $this->versionParser->normalizeBranch($candidate); - if ('9999999-dev' === $version) { - $version = 'dev-'.$match[1]; - } + // try to find the best (nearest) version branch to assume this feature's version + $version = $this->guessFeatureVersion($config, $version, $branches, 'git rev-list %candidate%..%branch%'); + + return $version; + } + } + + private function guessHgVersion(array $config) + { + // try to fetch current version from hg branch + if (0 === $this->process->execute('hg branch', $output)) { + $branch = trim($output); + $version = $this->versionParser->normalizeBranch($branch); + $isFeatureBranch = 0 === strpos($version, 'dev-'); + + if ('9999999-dev' === $version) { + $version = 'dev-'.$branch; + } + + if (!$isFeatureBranch) { + return $version; + } + + // re-use the HgDriver to fetch branches (this properly includes bookmarks) + $config = array('url' => getcwd()); + $driver = new HgDriver($config, new NullIO(), $this->config, $this->process); + $branches = array_keys($driver->getBranches()); + + // try to find the best (nearest) version branch to assume this feature's version + $version = $this->guessFeatureVersion($config, $version, $branches, 'hg log -r "not ancestors(\'%candidate%\') and ancestors(\'%branch%\')" --template "{node}\\n"'); + + return $version; + } + } + + private function guessFeatureVersion(array $config, $version, array $branches, $scmCmdline) + { + // ignore feature branches if they have no branch-alias or self.version is used + // and find the branch they came from to use as a version instead + if ((isset($config['extra']['branch-alias']) && !isset($config['extra']['branch-alias'][$version])) + || strpos(json_encode($config), '"self.version"') + ) { + $branch = preg_replace('{^dev-}', '', $version); + $length = PHP_INT_MAX; + foreach ($branches as $candidate) { + // do not compare against other feature branches + if ($candidate === $branch || !preg_match('{^(master|trunk|default|develop|\d+\..+)$}', $candidate, $match)) { + continue; + } + + $cmdLine = str_replace(array('%candidate%', '%branch%'), array($candidate, $branch), $scmCmdline); + if (0 !== $this->process->execute($cmdLine, $output)) { + continue; + } + + if (strlen($output) < $length) { + $length = strlen($output); + $version = $this->versionParser->normalizeBranch($candidate); + if ('9999999-dev' === $version) { + $version = 'dev-'.$match[1]; } } } + } - return $version; + 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 87ce0036d..3493d3d5b 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'; } } } @@ -179,7 +199,29 @@ class ValidatingArrayLoader implements LoaderInterface } } - // TODO validate autoload + if ($this->validateArray('autoload') && !empty($this->config['autoload'])) { + $types = array('psr-0', 'psr-4', 'classmap', 'files'); + foreach ($this->config['autoload'] as $type => $typeConfig) { + if (!in_array($type, $types)) { + $this->errors[] = 'autoload : invalid value ('.$type.'), must be one of '.implode(', ', $types); + unset($this->config['autoload'][$type]); + } + if ($type === 'psr-4') { + foreach ($typeConfig as $namespace => $dirs) { + if ($namespace !== '' && '\\' !== substr($namespace, -1)) { + $this->errors[] = 'autoload.psr-4 : invalid value ('.$namespace.'), namespaces must end with a namespace separator, should be '.$namespace.'\\'; + } + } + } + } + } + + if (!empty($this->config['autoload']['psr-4']) && !empty($this->config['target-dir'])) { + $this->errors[] = 'target-dir : this can not be used together with the autoload.psr-4 setting, remove target-dir to upgrade to psr-4'; + // Unset the psr-4 setting, since unsetting target-dir might + // interfere with other settings. + unset($this->config['autoload']['psr-4']); + } // TODO validate dist // TODO validate source @@ -188,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'])) { diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index a78c2ccde..4eb836706 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -16,10 +16,12 @@ 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; +use Composer\Package\Version\VersionParser; +use Composer\Util\Git as GitUtil; +use Composer\IO\IOInterface; /** * Reads/writes project lockfile (composer.lock). @@ -35,42 +37,41 @@ class Locker private $hash; private $loader; private $dumper; + private $process; private $lockDataCache; /** * Initializes packages locker. * + * @param IOInterface $io * @param JsonFile $lockFile lockfile loader * @param RepositoryManager $repositoryManager repository manager instance * @param InstallationManager $installationManager installation manager instance * @param string $hash unique hash of the current composer configuration */ - public function __construct(JsonFile $lockFile, RepositoryManager $repositoryManager, InstallationManager $installationManager, $hash) + public function __construct(IOInterface $io, JsonFile $lockFile, RepositoryManager $repositoryManager, InstallationManager $installationManager, $hash) { - $this->lockFile = $lockFile; + $this->lockFile = $lockFile; $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); } /** * Checks whether locker were been locked (lockfile found). * - * @param bool $dev true to check if dev packages are locked * @return bool */ - public function isLocked($dev = false) + public function isLocked() { if (!$this->lockFile->exists()) { return false; } $data = $this->getLockData(); - if ($dev) { - return isset($data['packages-dev']); - } return isset($data['packages']); } @@ -87,36 +88,26 @@ class Locker return $this->hash === $lock['hash']; } - /** - * Checks whether the lock file is in the new complete format or not - * - * @param bool $dev true to check in dev mode - * @return bool - */ - public function isCompleteFormat($dev) - { - $lockData = $this->getLockData(); - $lockedPackages = $dev ? $lockData['packages-dev'] : $lockData['packages']; - - if (empty($lockedPackages) || isset($lockedPackages[0]['name'])) { - return true; - } - - return false; - } - /** * Searches and returns an array of locked packages, retrieved from registered repositories. * - * @param bool $dev true to retrieve the locked dev packages + * @param bool $withDevReqs true to retrieve the locked dev packages + * @throws \RuntimeException * @return \Composer\Repository\RepositoryInterface */ - public function getLockedRepository($dev = false) + public function getLockedRepository($withDevReqs = false) { $lockData = $this->getLockData(); $packages = new ArrayRepository(); - $lockedPackages = $dev ? $lockData['packages-dev'] : $lockData['packages']; + $lockedPackages = $lockData['packages']; + if ($withDevReqs) { + if (isset($lockData['packages-dev'])) { + $lockedPackages = array_merge($lockedPackages, $lockData['packages-dev']); + } else { + throw new \RuntimeException('The lock file does not contain require-dev information, run install with the --no-dev option or run update to install those packages.'); + } + } if (empty($lockedPackages)) { return $packages; @@ -130,50 +121,42 @@ class Locker return $packages; } - // legacy lock file support - $repo = $dev ? $this->repositoryManager->getLocalDevRepository() : $this->repositoryManager->getLocalRepository(); - foreach ($lockedPackages as $info) { - $resolvedVersion = !empty($info['alias-version']) ? $info['alias-version'] : $info['version']; - - // try to find the package in the local repo (best match) - $package = $repo->findPackage($info['package'], $resolvedVersion); - - // try to find the package in any repo - if (!$package) { - $package = $this->repositoryManager->findPackage($info['package'], $resolvedVersion); - } - - // try to find the package in any repo (second pass without alias + rebuild alias since it disappeared) - if (!$package && !empty($info['alias-version'])) { - $package = $this->repositoryManager->findPackage($info['package'], $info['version']); - if ($package) { - $package->setAlias($info['alias-version']); - $package->setPrettyAlias($info['alias-pretty-version']); - } - } + throw new \RuntimeException('Your composer.lock was created before 2012-09-15, and is not supported anymore. Run "composer update" to generate a new one.'); + } - if (!$package) { - throw new \LogicException(sprintf( - 'Can not find "%s-%s" package in registered repositories', - $info['package'], $info['version'] - )); - } + /** + * Returns the platform requirements stored in the lock file + * + * @param bool $withDevReqs if true, the platform requirements from the require-dev block are also returned + * @return \Composer\Package\Link[] + */ + public function getPlatformRequirements($withDevReqs = false) + { + $lockData = $this->getLockData(); + $versionParser = new VersionParser(); + $requirements = array(); + + if (!empty($lockData['platform'])) { + $requirements = $versionParser->parseLinks( + '__ROOT__', + '1.0.0', + 'requires', + isset($lockData['platform']) ? $lockData['platform'] : array() + ); + } - $package = clone $package; - if (!empty($info['time'])) { - $package->setReleaseDate($info['time']); - } - if (!empty($info['source-reference'])) { - $package->setSourceReference($info['source-reference']); - if (is_callable($package, 'setDistReference')) { - $package->setDistReference($info['source-reference']); - } - } + if ($withDevReqs && !empty($lockData['platform-dev'])) { + $devRequirements = $versionParser->parseLinks( + '__ROOT__', + '1.0.0', + 'requires', + isset($lockData['platform-dev']) ? $lockData['platform-dev'] : array() + ); - $packages->addPackage($package); + $requirements = array_merge($requirements, $devRequirements); } - return $packages; + return $requirements; } public function getMinimumStability() @@ -190,6 +173,15 @@ 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 getAliases() { $lockData = $this->getLockData(); @@ -215,21 +207,28 @@ class Locker * * @param array $packages array of packages * @param mixed $devPackages array of dev packages or null if installed without --dev + * @param array $platformReqs array of package name => constraint for required platform packages + * @param mixed $platformDevReqs array of package name => constraint for dev-required platform packages * @param array $aliases array of aliases * @param string $minimumStability * @param array $stabilityFlags + * @param bool $preferStable * * @return bool */ - public function setLockData(array $packages, $devPackages, array $aliases, $minimumStability, array $stabilityFlags) + public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable) { $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', + '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, ); foreach ($aliases as $package => $versions) { @@ -256,6 +255,9 @@ class Locker return false; } + $lock['platform'] = $platformReqs; + $lock['platform-dev'] = $platformDevReqs; + if (!$this->isLocked() || $lock !== $this->getLockData()) { $this->lockFile->write($lock); $this->lockDataCache = null; @@ -287,17 +289,19 @@ class Locker $spec = $this->dumper->dump($package); unset($spec['version_normalized']); - if ($package->isDev()) { - if (function_exists('proc_open') && 'git' === $package->getSourceType() && ($path = $this->installationManager->getInstallPath($package))) { - $sourceRef = $package->getSourceReference() ?: $package->getDistReference(); - $process = new ProcessExecutor(); - if (0 === $process->execute('git log -n1 --pretty=%ct '.escapeshellarg($sourceRef), $output, $path) && preg_match('{^\s*\d+\s*$}', $output)) { - $datetime = new \DateTime('@'.trim($output), new \DateTimeZone('UTC')); - $spec['time'] = $datetime->format('Y-m-d H:i:s'); - } - } + // always move time to the end of the package definition + $time = isset($spec['time']) ? $spec['time'] : null; + unset($spec['time']); + if ($package->isDev() && $package->getInstallationSource() === 'source') { + // use the exact commit time of the current reference if it's a dev package + $time = $this->getPackageTime($package) ?: $time; + } + if (null !== $time) { + $spec['time'] = $time; } + unset($spec['installation-source']); + $locked[] = $spec; } @@ -314,4 +318,42 @@ class Locker return $locked; } + + /** + * Returns the packages's datetime for its source reference. + * + * @param PackageInterface $package The package to scan. + * @return string|null The formatted datetime or null if none was found. + */ + private function getPackageTime(PackageInterface $package) + { + if (!function_exists('proc_open')) { + return null; + } + + $path = realpath($this->installationManager->getInstallPath($package)); + $sourceType = $package->getSourceType(); + $datetime = null; + + if ($path && in_array($sourceType, array('git', 'hg'))) { + $sourceRef = $package->getSourceReference() ?: $package->getDistReference(); + switch ($sourceType) { + case 'git': + GitUtil::cleanEnv(); + + 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 '.ProcessExecutor::escape($sourceRef), $output, $path) && preg_match('{^\s*(\d+)\s*}', $output, $match)) { + $datetime = new \DateTime('@'.$match[1], new \DateTimeZone('UTC')); + } + break; + } + } + + return $datetime ? $datetime->format('Y-m-d H:i:s') : null; + } } diff --git a/src/Composer/Package/Package.php b/src/Composer/Package/Package.php index ddd4b4ec5..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,18 +28,17 @@ 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; protected $extra = array(); protected $binaries = array(); - protected $aliases = array(); - protected $alias; - protected $prettyAlias; protected $dev; protected $stability; protected $notificationUrl; @@ -50,7 +50,9 @@ class Package extends BasePackage protected $devRequires = array(); protected $suggests = array(); protected $autoload = array(); + protected $devAutoload = array(); protected $includePaths = array(); + protected $archiveExcludes = array(); /** * Creates a new in memory package. @@ -154,54 +156,6 @@ class Package extends BasePackage return $this->binaries; } - /** - * @param array $aliases - */ - public function setAliases(array $aliases) - { - $this->aliases = $aliases; - } - - /** - * {@inheritDoc} - */ - public function getAliases() - { - return $this->aliases; - } - - /** - * @param string $alias - */ - public function setAlias($alias) - { - $this->alias = $alias; - } - - /** - * {@inheritDoc} - */ - public function getAlias() - { - return $this->alias; - } - - /** - * @param string $prettyAlias - */ - public function setPrettyAlias($prettyAlias) - { - $this->prettyAlias = $prettyAlias; - } - - /** - * {@inheritDoc} - */ - public function getPrettyAlias() - { - return $this->prettyAlias; - } - /** * {@inheritDoc} */ @@ -266,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 */ @@ -330,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} */ @@ -349,7 +351,7 @@ class Package extends BasePackage /** * Set the releaseDate * - * @param DateTime $releaseDate + * @param \DateTime $releaseDate */ public function setReleaseDate(\DateTime $releaseDate) { @@ -490,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. * @@ -525,4 +545,63 @@ class Package extends BasePackage { return $this->notificationUrl; } + + /** + * Sets a list of patterns to be excluded from archives + * + * @param array $excludes + */ + public function setArchiveExcludes(array $excludes) + { + $this->archiveExcludes = $excludes; + } + + /** + * {@inheritDoc} + */ + public function getArchiveExcludes() + { + 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 6c2b48b4f..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. @@ -267,20 +307,6 @@ interface PackageInterface */ public function getBinaries(); - /** - * Returns a version this package should be aliased to - * - * @return string - */ - public function getAlias(); - - /** - * Returns a non-normalized version this package should be aliased to - * - * @return string - */ - public function getPrettyAlias(); - /** * Returns package unique name, constructed from name and version. * @@ -308,4 +334,18 @@ interface PackageInterface * @return string */ public function getPrettyString(); + + /** + * Returns a list of patterns to exclude from package archives + * + * @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/RootAliasPackage.php b/src/Composer/Package/RootAliasPackage.php new file mode 100644 index 000000000..a16e0e914 --- /dev/null +++ b/src/Composer/Package/RootAliasPackage.php @@ -0,0 +1,86 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package; + +/** + * @author Jordi Boggiano + */ +class RootAliasPackage extends AliasPackage implements RootPackageInterface +{ + public function __construct(RootPackageInterface $aliasOf, $version, $prettyVersion) + { + parent::__construct($aliasOf, $version, $prettyVersion); + } + + /** + * {@inheritDoc} + */ + public function getAliases() + { + return $this->aliasOf->getAliases(); + } + + /** + * {@inheritDoc} + */ + public function getMinimumStability() + { + return $this->aliasOf->getMinimumStability(); + } + + /** + * {@inheritDoc} + */ + public function getStabilityFlags() + { + return $this->aliasOf->getStabilityFlags(); + } + + /** + * {@inheritDoc} + */ + public function getReferences() + { + return $this->aliasOf->getReferences(); + } + + /** + * {@inheritDoc} + */ + public function getPreferStable() + { + return $this->aliasOf->getPreferStable(); + } + + /** + * {@inheritDoc} + */ + public function setRequires(array $require) + { + return $this->aliasOf->setRequires($require); + } + + /** + * {@inheritDoc} + */ + public function setDevRequires(array $devRequire) + { + return $this->aliasOf->setDevRequires($devRequire); + } + + public function __clone() + { + parent::__clone(); + $this->aliasOf = clone $this->aliasOf; + } +} diff --git a/src/Composer/Package/RootPackage.php b/src/Composer/Package/RootPackage.php index 798f045b7..b2e812990 100644 --- a/src/Composer/Package/RootPackage.php +++ b/src/Composer/Package/RootPackage.php @@ -20,8 +20,10 @@ namespace Composer\Package; class RootPackage extends CompletePackage implements RootPackageInterface { protected $minimumStability = 'stable'; + protected $preferStable = false; protected $stabilityFlags = array(); protected $references = array(); + protected $aliases = array(); /** * Set the minimumStability @@ -59,6 +61,24 @@ class RootPackage extends CompletePackage implements RootPackageInterface return $this->stabilityFlags; } + /** + * Set the preferStable + * + * @param bool $preferStable + */ + public function setPreferStable($preferStable) + { + $this->preferStable = $preferStable; + } + + /** + * {@inheritDoc} + */ + public function getPreferStable() + { + return $this->preferStable; + } + /** * Set the references * @@ -76,4 +96,22 @@ class RootPackage extends CompletePackage implements RootPackageInterface { return $this->references; } + + /** + * Set the aliases + * + * @param array $aliases + */ + public function setAliases(array $aliases) + { + $this->aliases = $aliases; + } + + /** + * {@inheritDoc} + */ + public function getAliases() + { + return $this->aliases; + } } diff --git a/src/Composer/Package/RootPackageInterface.php b/src/Composer/Package/RootPackageInterface.php index 501376f66..7bee86324 100644 --- a/src/Composer/Package/RootPackageInterface.php +++ b/src/Composer/Package/RootPackageInterface.php @@ -19,6 +19,13 @@ namespace Composer\Package; */ interface RootPackageInterface extends CompletePackageInterface { + /** + * Returns a set of package names and theirs aliases + * + * @return array + */ + public function getAliases(); + /** * Returns the minimum stability of the package * @@ -43,4 +50,25 @@ interface RootPackageInterface extends CompletePackageInterface * @return array */ public function getReferences(); + + /** + * Returns true if the root package prefers picking stable packages over unstable ones + * + * @return bool + */ + public function getPreferStable(); + + /** + * Set the required packages + * + * @param array $requires A set of package links + */ + public function setRequires(array $requires); + + /** + * Set the recommended packages + * + * @param array $devRequires A set of package links + */ + public function setDevRequires(array $devRequires); } diff --git a/src/Composer/Package/Version/VersionParser.php b/src/Composer/Package/Version/VersionParser.php index 43f2b87e2..3825426a8 100644 --- a/src/Composer/Package/Version/VersionParser.php +++ b/src/Composer/Package/Version/VersionParser.php @@ -15,6 +15,7 @@ namespace Composer\Package\Version; use Composer\Package\BasePackage; use Composer\Package\PackageInterface; use Composer\Package\Link; +use Composer\Package\LinkConstraint\EmptyConstraint; use Composer\Package\LinkConstraint\MultiConstraint; use Composer\Package\LinkConstraint\VersionConstraint; @@ -35,7 +36,7 @@ class VersionParser */ public static function parseStability($version) { - $version = preg_replace('{#[a-f0-9]+$}i', '', $version); + $version = preg_replace('{#.+$}i', '', $version); if ('dev-' === substr($version, 0, 4) || '-dev' === substr($version, -4)) { return 'dev'; @@ -85,9 +86,10 @@ class VersionParser /** * Normalizes a version string to be able to perform comparisons on it * - * @param string $version - * @param string $fullVersion optional complete version string to give more context - * @return array + * @param string $version + * @param string $fullVersion optional complete version string to give more context + * @throws \UnexpectedValueException + * @return string */ public function normalize($version, $fullVersion = null) { @@ -120,6 +122,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 @@ -128,10 +136,7 @@ class VersionParser if ('stable' === $matches[$index]) { return $version; } - $mod = array('{^pl?$}i', '{^rc$}i'); - $modNormalized = array('patch', 'RC'); - $version .= '-'.preg_replace($mod, $modNormalized, strtolower($matches[$index])) - . (!empty($matches[$index+1]) ? $matches[$index+1] : ''); + $version .= '-' . $this->expandStability($matches[$index]) . (!empty($matches[$index+1]) ? $matches[$index+1] : ''); } if (!empty($matches[$index+2])) { @@ -162,7 +167,7 @@ class VersionParser * Normalizes a branch name to be able to perform comparisons on it * * @param string $name - * @return array + * @return string */ public function normalizeBranch($name) { @@ -220,25 +225,37 @@ class VersionParser $constraints = empty($match[1]) ? '*' : $match[1]; } - if (preg_match('{^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#[a-f0-9]+$}i', $constraints, $match)) { + if (preg_match('{^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$}i', $constraints, $match)) { $constraints = $match[1]; } - $constraints = 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); - if (count($constraints) > 1) { - $constraintObjects = array(); - foreach ($constraints as $constraint) { - $constraintObjects = array_merge($constraintObjects, $this->parseConstraint($constraint)); + if (count($andConstraints) > 1) { + $constraintObjects = array(); + foreach ($andConstraints as $constraint) { + $constraintObjects = array_merge($constraintObjects, $this->parseConstraint($constraint)); + } + } else { + $constraintObjects = $this->parseConstraint($andConstraints[0]); } - } else { - $constraintObjects = $this->parseConstraint($constraints[0]); + + if (1 === count($constraintObjects)) { + $constraint = $constraintObjects[0]; + } else { + $constraint = new MultiConstraint($constraintObjects); + } + + $orGroups[] = $constraint; } - if (1 === count($constraintObjects)) { - $constraint = $constraintObjects[0]; + if (1 === count($orGroups)) { + $constraint = $orGroups[0]; } else { - $constraint = new MultiConstraint($constraintObjects); + $constraint = new MultiConstraint($orGroups, false); } $constraint->setPrettyString($prettyConstraint); @@ -256,58 +273,80 @@ class VersionParser } if (preg_match('{^[x*](\.[x*])*$}i', $constraint)) { - return array(); + return array(new EmptyConstraint); } - if (preg_match('{^~(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?$}', $constraint, $matches)) { - if (isset($matches[4])) { - $highVersion = $matches[1] . '.' . $matches[2] . '.' . ($matches[3] + 1) . '.0-dev'; - $lowVersion = $matches[1] . '.' . $matches[2] . '.' . $matches[3]. '.' . $matches[4]; - } elseif (isset($matches[3])) { - $highVersion = $matches[1] . '.' . ($matches[2] + 1) . '.0.0-dev'; - $lowVersion = $matches[1] . '.' . $matches[2] . '.' . $matches[3]. '.0'; + // 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 (substr($constraint, 0, 2) === '~>') { + throw new \UnexpectedValueException( + 'Could not parse version constraint '.$constraint.': '. + 'Invalid operator "~>", you probably meant to use the "~" operator' + ); + } + + // Work out which position in the version we are operating at + if (isset($matches[4]) && '' !== $matches[4]) { + $position = 4; + } elseif (isset($matches[3]) && '' !== $matches[3]) { + $position = 3; + } elseif (isset($matches[2]) && '' !== $matches[2]) { + $position = 2; } else { - $highVersion = ($matches[1] + 1) . '.0.0.0-dev'; - if (isset($matches[2])) { - $lowVersion = $matches[1] . '.' . $matches[2] . '.0.0'; - } else { - $lowVersion = $matches[1] . '.0.0.0'; - } + $position = 1; + } + + // Calculate the stability suffix + $stabilitySuffix = ''; + if (!empty($matches[5])) { + $stabilitySuffix .= '-' . $this->expandStability($matches[5]) . (!empty($matches[6]) ? $matches[6] : ''); + } + + if (!empty($matches[7])) { + $stabilitySuffix .= '-dev'; + } + + if (!$stabilitySuffix) { + $stabilitySuffix = "-dev"; } + $lowVersion = $this->manipulateVersionString($matches, $position, 0) . $stabilitySuffix; + $lowerBound = new VersionConstraint('>=', $lowVersion); + + // For upper bound, we increment the position of one more significance, + // but highPosition = 0 would be illegal + $highPosition = max(1, $position - 1); + $highVersion = $this->manipulateVersionString($matches, $highPosition, 1) . '-dev'; + $upperBound = new VersionConstraint('<', $highVersion); return array( - new VersionConstraint('>=', $lowVersion), - new VersionConstraint('<', $highVersion), + $lowerBound, + $upperBound ); } // match wildcard constraints if (preg_match('{^(\d+)(?:\.(\d+))?(?:\.(\d+))?\.[x*]$}', $constraint, $matches)) { - if (isset($matches[3])) { - $highVersion = $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.9999999'; - if ($matches[3] === '0') { - $lowVersion = $matches[1] . '.' . ($matches[2] - 1) . '.9999999.9999999'; - } else { - $lowVersion = $matches[1] . '.' . $matches[2] . '.' . ($matches[3] - 1). '.9999999'; - } - } elseif (isset($matches[2])) { - $highVersion = $matches[1] . '.' . $matches[2] . '.9999999.9999999'; - if ($matches[2] === '0') { - $lowVersion = ($matches[1] - 1) . '.9999999.9999999.9999999'; - } else { - $lowVersion = $matches[1] . '.' . ($matches[2] - 1) . '.9999999.9999999'; - } + if (isset($matches[3]) && '' !== $matches[3]) { + $position = 3; + } elseif (isset($matches[2]) && '' !== $matches[2]) { + $position = 2; } else { - $highVersion = $matches[1] . '.9999999.9999999.9999999'; - if ($matches[1] === '0') { - return array(new VersionConstraint('<', $highVersion)); - } else { - $lowVersion = ($matches[1] - 1) . '.9999999.9999999.9999999'; - } + $position = 1; + } + + $lowVersion = $this->manipulateVersionString($matches, $position) . "-dev"; + $highVersion = $this->manipulateVersionString($matches, $position, 1) . "-dev"; + + if ($lowVersion === "0.0.0.0-dev") { + return array(new VersionConstraint('<', $highVersion)); } return array( - new VersionConstraint('>', $lowVersion), + new VersionConstraint('>=', $lowVersion), new VersionConstraint('<', $highVersion), ); } @@ -331,16 +370,69 @@ class VersionParser $message = 'Could not parse version constraint '.$constraint; if (isset($e)) { - $message .= ': '.$e->getMessage(); + $message .= ': '. $e->getMessage(); } throw new \UnexpectedValueException($message); } + /** + * Increment, decrement, or simply pad a version number. + * + * Support function for {@link parseConstraint()} + * + * @param array $matches Array with version parts in array indexes 1,2,3,4 + * @param int $position 1,2,3,4 - which segment of the version to decrement + * @param int $increment + * @param string $pad The string to pad version parts after $position + * @return string The new version + */ + private function manipulateVersionString($matches, $position, $increment = 0, $pad = '0') + { + for ($i = 4; $i > 0; $i--) { + if ($i > $position) { + $matches[$i] = $pad; + } elseif ($i == $position && $increment) { + $matches[$i] += $increment; + // If $matches[$i] was 0, carry the decrement + if ($matches[$i] < 0) { + $matches[$i] = $pad; + $position--; + + // Return null on a carry overflow + if ($i == 1) { + return; + } + } + } + } + + return $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4]; + } + + private function expandStability($stability) + { + $stability = strtolower($stability); + + switch ($stability) { + case 'a': + return 'alpha'; + case 'b': + return 'beta'; + case 'p': + case 'pl': + return 'patch'; + case 'rc': + return 'RC'; + default: + return $stability; + } + } + /** * Parses a name/version pairs and returns an array of pairs + the * - * @param array $pairs a set of package/version pairs separated by ":", "=" or " " + * @param array $pairs a set of package/version pairs separated by ":", "=" or " " * @return array[] array of arrays containing a name and (if provided) a version */ public function parseNameVersionPairs(array $pairs) @@ -348,7 +440,7 @@ class VersionParser $pairs = array_values($pairs); $result = array(); - for ($i = 0; $i < count($pairs); $i++) { + for ($i = 0, $count = count($pairs); $i < $count; $i++) { $pair = preg_replace('{^([^=: ]+)[=: ](.*)$}', '$1 $2', trim($pairs[$i])); if (false === strpos($pair, ' ') && isset($pairs[$i+1]) && false === strpos($pairs[$i+1], '/')) { $pair .= ' '.$pairs[$i+1]; diff --git a/src/Composer/Package/Version/VersionSelector.php b/src/Composer/Package/Version/VersionSelector.php new file mode 100644 index 000000000..29358027f --- /dev/null +++ b/src/Composer/Package/Version/VersionSelector.php @@ -0,0 +1,131 @@ + + * 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); + // 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) + 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 '~'.$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 new file mode 100644 index 000000000..0697df97a --- /dev/null +++ b/src/Composer/Plugin/CommandEvent.php @@ -0,0 +1,88 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Plugin; + +use Composer\EventDispatcher\Event; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * An event for all commands. + * + * @author Nils Adermann + */ +class CommandEvent extends Event +{ + /** + * @var string + */ + private $commandName; + + /** + * @var InputInterface + */ + private $input; + + /** + * @var OutputInterface + */ + private $output; + + /** + * Constructor. + * + * @param string $name The event name + * @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, array $args = array(), array $flags = array()) + { + parent::__construct($name, $args, $flags); + $this->commandName = $commandName; + $this->input = $input; + $this->output = $output; + } + + /** + * Returns the command input interface + * + * @return InputInterface + */ + public function getInput() + { + return $this->input; + } + + /** + * Retrieves the command output interface + * + * @return OutputInterface + */ + public function getOutput() + { + return $this->output; + } + + /** + * Retrieves the name of the command being run + * + * @return string + */ + public function getCommandName() + { + return $this->commandName; + } +} diff --git a/src/Composer/Plugin/PluginEvents.php b/src/Composer/Plugin/PluginEvents.php new file mode 100644 index 000000000..ce9efdef2 --- /dev/null +++ b/src/Composer/Plugin/PluginEvents.php @@ -0,0 +1,41 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Plugin; + +/** + * The Plugin Events. + * + * @author Nils Adermann + */ +class PluginEvents +{ + /** + * The COMMAND event occurs as a command begins + * + * The event listener method receives a + * Composer\Plugin\CommandEvent instance. + * + * @var string + */ + const COMMAND = 'command'; + + /** + * The PRE_FILE_DOWNLOAD event occurs before downloading a file + * + * The event listener method receives a + * Composer\Plugin\PreFileDownloadEvent instance. + * + * @var string + */ + const PRE_FILE_DOWNLOAD = 'pre-file-download'; +} diff --git a/src/Composer/Plugin/PluginInterface.php b/src/Composer/Plugin/PluginInterface.php new file mode 100644 index 000000000..dea5828c1 --- /dev/null +++ b/src/Composer/Plugin/PluginInterface.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\Plugin; + +use Composer\Composer; +use Composer\IO\IOInterface; + +/** + * Plugin interface + * + * @author Nils Adermann + */ +interface PluginInterface +{ + /** + * Version number of the fake composer-plugin-api package + * + * @var string + */ + const PLUGIN_API_VERSION = '1.0.0'; + + /** + * Apply plugin modifications to composer + * + * @param Composer $composer + * @param IOInterface $io + */ + public function activate(Composer $composer, IOInterface $io); +} diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php new file mode 100644 index 000000000..fdd952ae3 --- /dev/null +++ b/src/Composer/Plugin/PluginManager.php @@ -0,0 +1,269 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Plugin; + +use Composer\Composer; +use Composer\EventDispatcher\EventSubscriberInterface; +use Composer\IO\IOInterface; +use Composer\Package\Package; +use Composer\Package\Version\VersionParser; +use Composer\Repository\RepositoryInterface; +use Composer\Package\AliasPackage; +use Composer\Package\PackageInterface; +use Composer\Package\Link; +use Composer\Package\LinkConstraint\VersionConstraint; +use Composer\DependencyResolver\Pool; + +/** + * Plugin manager + * + * @author Nils Adermann + */ +class PluginManager +{ + protected $composer; + protected $io; + protected $globalRepository; + protected $versionParser; + + protected $plugins = array(); + protected $registeredPlugins = array(); + + private static $classCounter = 0; + + /** + * Initializes plugin manager + * + * @param Composer $composer + * @param IOInterface $io + * @param RepositoryInterface $globalRepository + */ + public function __construct(Composer $composer, IOInterface $io, RepositoryInterface $globalRepository = null) + { + $this->composer = $composer; + $this->io = $io; + $this->globalRepository = $globalRepository; + $this->versionParser = new VersionParser(); + } + + /** + * Loads all plugins from currently installed plugin packages + */ + public function loadInstalledPlugins() + { + $repo = $this->composer->getRepositoryManager()->getLocalRepository(); + + if ($repo) { + $this->loadRepository($repo); + } + if ($this->globalRepository) { + $this->loadRepository($this->globalRepository); + } + } + + /** + * Adds a plugin, activates it and registers it with the event dispatcher + * + * @param PluginInterface $plugin plugin instance + */ + public function addPlugin(PluginInterface $plugin) + { + $this->plugins[] = $plugin; + $plugin->activate($this->composer, $this->io); + + if ($plugin instanceof EventSubscriberInterface) { + $this->composer->getEventDispatcher()->addSubscriber($plugin); + } + } + + /** + * Gets all currently active plugin instances + * + * @return array plugins + */ + public function getPlugins() + { + return $this->plugins; + } + + /** + * Load all plugins and installers from a repository + * + * Note that plugins in the specified repository that rely on events that + * have fired prior to loading will be missed. This means you likely want to + * call this method as early as possible. + * + * @param RepositoryInterface $repo Repository to scan for plugins to install + * + * @throws \RuntimeException + */ + public function loadRepository(RepositoryInterface $repo) + { + foreach ($repo->getPackages() as $package) { + if ($package instanceof AliasPackage) { + continue; + } + if ('composer-plugin' === $package->getType()) { + $requiresComposer = null; + foreach ($package->getRequires() as $link) { + if ($link->getTarget() == 'composer-plugin-api') { + $requiresComposer = $link->getConstraint(); + } + } + + if (!$requiresComposer) { + throw new \RuntimeException("Plugin ".$package->getName()." is missing a require statement for a version of the composer-plugin-api package."); + } + + if (!$requiresComposer->matches(new VersionConstraint('==', $this->versionParser->normalize(PluginInterface::PLUGIN_API_VERSION)))) { + $this->io->write("The plugin ".$package->getName()." requires a version of composer-plugin-api that does not match your composer installation. You may need to run composer update with the '--no-plugins' option."); + } + + $this->registerPackage($package); + } + // Backward compatibility + if ('composer-installer' === $package->getType()) { + $this->registerPackage($package); + } + } + } + + /** + * Recursively generates a map of package names to packages for all deps + * + * @param Pool $pool Package pool of installed packages + * @param array $collected Current state of the map for recursion + * @param PackageInterface $package The package to analyze + * + * @return array Map of package names to packages + */ + protected function collectDependencies(Pool $pool, array $collected, PackageInterface $package) + { + $requires = array_merge( + $package->getRequires(), + $package->getDevRequires() + ); + + foreach ($requires as $requireLink) { + $requiredPackage = $this->lookupInstalledPackage($pool, $requireLink); + if ($requiredPackage && !isset($collected[$requiredPackage->getName()])) { + $collected[$requiredPackage->getName()] = $requiredPackage; + $collected = $this->collectDependencies($pool, $collected, $requiredPackage); + } + } + + return $collected; + } + + /** + * Resolves a package link to a package in the installed pool + * + * Since dependencies are already installed this should always find one. + * + * @param Pool $pool Pool of installed packages only + * @param Link $link Package link to look up + * + * @return PackageInterface|null The found package + */ + protected function lookupInstalledPackage(Pool $pool, Link $link) + { + $packages = $pool->whatProvides($link->getTarget(), $link->getConstraint()); + + return (!empty($packages)) ? $packages[0] : null; + } + + /** + * Register a plugin package, activate it etc. + * + * If it's of type composer-installer it is registered as an installer + * instead for BC + * + * @param PackageInterface $package + * + * @throws \UnexpectedValueException + */ + public function registerPackage(PackageInterface $package) + { + $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(); + $pool->addRepository($localRepo); + if ($this->globalRepository) { + $pool->addRepository($this->globalRepository); + } + + $autoloadPackages = array($package->getName() => $package); + $autoloadPackages = $this->collectDependencies($pool, $autoloadPackages, $package); + + $generator = $this->composer->getAutoloadGenerator(); + $autoloads = array(); + foreach ($autoloadPackages as $autoloadPackage) { + $downloadPath = $this->getInstallPath($autoloadPackage, ($this->globalRepository && $this->globalRepository->hasPackage($autoloadPackage))); + $autoloads[] = array($autoloadPackage, $downloadPath); + } + + $map = $generator->parseAutoloads($autoloads, new Package('dummy', '1.0.0.0', '1.0.0')); + $classLoader = $generator->createLoader($map); + $classLoader->register(); + + foreach ($classes as $class) { + if (class_exists($class, false)) { + $code = file_get_contents($classLoader->findFile($class)); + $code = preg_replace('{^(\s*)class\s+(\S+)}mi', '$1class $2_composer_tmp'.self::$classCounter, $code); + eval('?>'.$code); + $class .= '_composer_tmp'.self::$classCounter; + self::$classCounter++; + } + + if ($oldInstallerPlugin) { + $installer = new $class($this->io, $this->composer); + $this->composer->getInstallationManager()->addInstaller($installer); + } else { + $plugin = new $class(); + $this->addPlugin($plugin); + $this->registeredPlugins[] = $package->getName(); + } + } + } + + /** + * Retrieves the path a package is installed to. + * + * @param PackageInterface $package + * @param bool $global Whether this is a global package + * + * @return string Install path + */ + public function getInstallPath(PackageInterface $package, $global = false) + { + if (!$global) { + return $this->composer->getInstallationManager()->getInstallPath($package); + } + + $targetDir = $package->getTargetDir(); + $vendorDir = $this->composer->getConfig()->get('home').'/vendor'; + + return ($vendorDir ? $vendorDir.'/' : '').$package->getPrettyName().($targetDir ? '/'.$targetDir : ''); + } +} diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php new file mode 100644 index 000000000..7ae6821ce --- /dev/null +++ b/src/Composer/Plugin/PreFileDownloadEvent.php @@ -0,0 +1,78 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Plugin; + +use Composer\EventDispatcher\Event; +use Composer\Util\RemoteFilesystem; + +/** + * The pre file download event. + * + * @author Nils Adermann + */ +class PreFileDownloadEvent extends Event +{ + /** + * @var RemoteFilesystem + */ + private $rfs; + + /** + * @var string + */ + private $processedUrl; + + /** + * Constructor. + * + * @param string $name The event name + * @param RemoteFilesystem $rfs + * @param string $processedUrl + */ + public function __construct($name, RemoteFilesystem $rfs, $processedUrl) + { + parent::__construct($name); + $this->rfs = $rfs; + $this->processedUrl = $processedUrl; + } + + /** + * Returns the remote filesystem + * + * @return RemoteFilesystem + */ + public function getRemoteFilesystem() + { + return $this->rfs; + } + + /** + * Sets the remote filesystem + * + * @param RemoteFilesystem $rfs + */ + public function setRemoteFilesystem(RemoteFilesystem $rfs) + { + $this->rfs = $rfs; + } + + /** + * Retrieves the processed URL this remote filesystem will be used for + * + * @return string + */ + public function getProcessedUrl() + { + return $this->processedUrl; + } +} diff --git a/src/Composer/Repository/ArrayRepository.php b/src/Composer/Repository/ArrayRepository.php index 49ddd99d5..f08f9cda7 100644 --- a/src/Composer/Repository/ArrayRepository.php +++ b/src/Composer/Repository/ArrayRepository.php @@ -14,6 +14,7 @@ namespace Composer\Repository; use Composer\Package\AliasPackage; use Composer\Package\PackageInterface; +use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionParser; /** @@ -74,6 +75,32 @@ class ArrayRepository implements RepositoryInterface return $packages; } + /** + * {@inheritDoc} + */ + public function search($query, $mode = 0) + { + $regex = '{(?:'.implode('|', preg_split('{\s+}', $query)).')}i'; + + $matches = array(); + foreach ($this->getPackages() as $package) { + $name = $package->getName(); + if (isset($matches[$name])) { + continue; + } + if (preg_match($regex, $name) + || ($mode === self::SEARCH_FULLTEXT && $package instanceof CompletePackageInterface && preg_match($regex, implode(' ', (array) $package->getKeywords()) . ' ' . $package->getDescription())) + ) { + $matches[$name] = array( + 'name' => $package->getPrettyName(), + 'description' => $package->getDescription(), + ); + } + } + + return array_values($matches); + } + /** * {@inheritDoc} */ @@ -103,32 +130,17 @@ class ArrayRepository implements RepositoryInterface $package->setRepository($this); $this->packages[] = $package; - // create alias package on the fly if needed - if ($package->getAlias()) { - $alias = $this->createAliasPackage($package); - if (!$this->hasPackage($alias)) { - $this->addPackage($alias); + if ($package instanceof AliasPackage) { + $aliasedPackage = $package->getAliasOf(); + if (null === $aliasedPackage->getRepository()) { + $this->addPackage($aliasedPackage); } } } - /** - * {@inheritDoc} - */ - public function filterPackages($callback, $class = 'Composer\Package\Package') - { - foreach ($this->getPackages() as $package) { - if (false === call_user_func($callback, $package)) { - return false; - } - } - - return true; - } - - protected function createAliasPackage(PackageInterface $package, $alias = null, $prettyAlias = null) + protected function createAliasPackage(PackageInterface $package, $alias, $prettyAlias) { - return new AliasPackage($package, $alias ?: $package->getAlias(), $prettyAlias ?: $package->getPrettyAlias()); + return new AliasPackage($package instanceof AliasPackage ? $package->getAliasOf() : $package, $alias, $prettyAlias); } /** diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php new file mode 100644 index 000000000..0b7775aaf --- /dev/null +++ b/src/Composer/Repository/ArtifactRepository.php @@ -0,0 +1,150 @@ + + * 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\IO\IOInterface; +use Composer\Json\JsonFile; +use Composer\Package\Loader\ArrayLoader; + +/** + * @author Serge Smertin + */ +class ArtifactRepository extends ArrayRepository +{ + /** @var LoaderInterface */ + protected $loader; + + protected $lookup; + + public function __construct(array $repoConfig, IOInterface $io) + { + if (!extension_loaded('zip')) { + throw new \RuntimeException('The artifact repository requires PHP\'s zip extension'); + } + + $this->loader = new ArrayLoader(); + $this->lookup = $repoConfig['url']; + $this->io = $io; + } + + protected function initialize() + { + parent::initialize(); + + $this->scanDirectory($this->lookup); + } + + private function scanDirectory($path) + { + $io = $this->io; + + $directory = new \RecursiveDirectoryIterator($path); + $iterator = new \RecursiveIteratorIterator($directory); + $regex = new \RegexIterator($iterator, '/^.+\.(zip|phar)$/i'); + foreach ($regex as $file) { + /* @var $file \SplFileInfo */ + if (!$file->isFile()) { + continue; + } + + $package = $this->getComposerInformation($file); + if (!$package) { + if ($io->isVerbose()) { + $io->write("File {$file->getBasename()} doesn't seem to hold a package"); + } + continue; + } + + if ($io->isVerbose()) { + $template = 'Found package %s (%s) in file %s'; + $io->write(sprintf($template, $package->getName(), $package->getPrettyVersion(), $file->getBasename())); + } + + $this->addPackage($package); + } + } + + /** + * 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(); + $zip->open($file->getPathname()); + + if (0 == $zip->numFiles) { + return false; + } + + $foundFileIndex = $this->locateFile($zip, 'composer.json'); + if (false === $foundFileIndex) { + return false; + } + + $configurationFileName = $zip->getNameIndex($foundFileIndex); + + $composerFile = "zip://{$file->getPathname()}#$configurationFileName"; + $json = file_get_contents($composerFile); + + $package = JsonFile::parseJson($json, $composerFile); + $package['dist'] = array( + 'type' => 'zip', + 'url' => $file->getPathname(), + 'shasum' => sha1_file($file->getRealPath()) + ); + + $package = $this->loader->load($package); + + return $package; + } +} diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index fdb7fa5cb..82d8b9a36 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; @@ -22,6 +23,9 @@ use Composer\Cache; use Composer\Config; use Composer\IO\IOInterface; use Composer\Util\RemoteFilesystem; +use Composer\Plugin\PluginEvents; +use Composer\Plugin\PreFileDownloadEvent; +use Composer\EventDispatcher\EventDispatcher; /** * @author Jordi Boggiano @@ -33,20 +37,28 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository protected $url; protected $baseUrl; protected $io; + protected $rfs; protected $cache; protected $notifyUrl; + protected $searchUrl; protected $hasProviders = false; + protected $providersUrl; + protected $lazyProvidersUrl; protected $providerListing; protected $providers = array(); protected $providersByUid = array(); protected $loader; protected $rootAliases; + protected $allowSslDowngrade = false; + protected $eventDispatcher; + protected $sourceMirrors; + protected $distMirrors; private $rawData; private $minimalPackages; private $degradedMode = false; private $rootData; - public function __construct(array $repoConfig, IOInterface $io, Config $config) + public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null) { if (!preg_match('{^[\w.]+\??://}', $repoConfig['url'])) { // assume http as the default protocol @@ -59,21 +71,26 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository } $urlBits = parse_url($repoConfig['url']); - if (empty($urlBits['scheme']) || empty($urlBits['host'])) { + if ($urlBits === false || empty($urlBits['scheme'])) { throw new \UnexpectedValueException('Invalid url given for Composer repository: '.$repoConfig['url']); } if (!isset($repoConfig['options'])) { $repoConfig['options'] = array(); } + if (isset($repoConfig['allow_ssl_downgrade']) && true === $repoConfig['allow_ssl_downgrade']) { + $this->allowSslDowngrade = true; + } $this->config = $config; $this->options = $repoConfig['options']; $this->url = $repoConfig['url']; $this->baseUrl = rtrim(preg_replace('{^(.*)(?:/packages.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)); + $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->config, $this->options); + $this->eventDispatcher = $eventDispatcher; } public function setRootAliases(array $rootAliases) @@ -81,6 +98,67 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository $this->rootAliases = $rootAliases; } + /** + * {@inheritDoc} + */ + public function 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; + } + } + } + } + } + + /** + * {@inheritDoc} + */ + public function findPackages($name, $version = null) + { + // normalize name + $name = strtolower($name); + + // normalize version + if (null !== $version) { + $versionParser = new VersionParser(); + $version = $versionParser->normalize($version); + } + + $packages = array(); + + 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; + } + } + } + } + + return $packages; + } + + 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 parent::getPackages(); + } + /** * {@inheritDoc} */ @@ -127,24 +205,55 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository /** * {@inheritDoc} */ - public function filterPackages($callback, $class = 'Composer\Package\Package') + public function search($query, $mode = 0) { - if (null === $this->rawData) { - $this->rawData = $this->loadDataFromServer(); + $this->loadRootServerFile(); + + if ($this->searchUrl && $mode === self::SEARCH_FULLTEXT) { + $url = str_replace('%query%', $query, $this->searchUrl); + + $hostname = parse_url($url, PHP_URL_HOST) ?: $url; + $json = $this->rfs->getContents($hostname, $url, false); + $results = JsonFile::parseJson($json, $url); + + return $results['results']; } - foreach ($this->rawData as $package) { - if (false === call_user_func($callback, $package = $this->createPackage($package, $class))) { - return false; - } - if ($package->getAlias()) { - if (false === call_user_func($callback, $this->createAliasPackage($package))) { - return false; + if ($this->hasProviders()) { + $results = array(); + $regex = '{(?:'.implode('|', preg_split('{\s+}', $query)).')}i'; + + foreach ($this->getProviderNames() as $name) { + if (preg_match($regex, $name)) { + $results[] = array('name' => $name); } } + + return $results; } - return true; + return parent::search($query, $mode); + } + + public function getProviderNames() + { + $this->loadRootServerFile(); + + if (null === $this->providerListing) { + $this->loadProviderListings($this->loadRootServerFile()); + } + + if ($this->providersUrl) { + return array_keys($this->providerListing); + } + + // BC handling for old providers-includes + $providers = array(); + foreach (array_keys($this->providerListing) as $provider) { + $providers[] = substr($provider, 2, -5); + } + + return $providers; } /** @@ -153,11 +262,25 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository public function loadPackage(array $data) { $package = $this->createPackage($data['raw'], 'Composer\Package\Package'); + if ($package instanceof AliasPackage) { + $package = $package->getAliasOf(); + } $package->setRepository($this); return $package; } + protected function configurePackageTransportOptions(PackageInterface $package) + { + foreach ($package->getDistUrls() as $url) { + if (strpos($url, $this->baseUrl) === 0) { + $package->setTransportOptions($this->options); + + return; + } + } + } + /** * {@inheritDoc} */ @@ -188,30 +311,48 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository public function whatProvides(Pool $pool, $name) { - // skip platform packages - if ($name === 'php' || in_array(substr($name, 0, 4), array('ext-', 'lib-'), true) || $name === '__root__') { - return array(); - } - if (isset($this->providers[$name])) { return $this->providers[$name]; } + // skip platform packages + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name) || '__root__' === $name) { + return array(); + } + if (null === $this->providerListing) { $this->loadProviderListings($this->loadRootServerFile()); } - $url = 'p/'.$name.'.json'; + 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(); + } - // package does not exist in this repo - if (!isset($this->providerListing[$url])) { - return array(); + $hash = $this->providerListing[$name]['sha256']; + $url = str_replace(array('%package%', '%hash%'), array($name, $hash), $this->providersUrl); + $cacheKey = 'provider-'.strtr($name, '/', '$').'.json'; + } else { + // BC handling for old providers-includes + $url = 'p/'.$name.'.json'; + + // package does not exist in this repo + if (!isset($this->providerListing[$url])) { + return array(); + } + $hash = $this->providerListing[$url]['sha256']; + $cacheKey = null; } - if ($this->cache->sha256($url) === $this->providerListing[$url]['sha256']) { - $packages = json_decode($this->cache->read($url), true); + if ($cacheKey && $this->cache->sha256($cacheKey) === $hash) { + $packages = json_decode($this->cache->read($cacheKey), true); } else { - $packages = $this->fetchFile($url, null, $this->providerListing[$url]['sha256']); + $packages = $this->fetchFile($url, $cacheKey, $hash); } $this->providers[$name] = array(); @@ -234,6 +375,25 @@ 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; } @@ -242,25 +402,27 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository $package = $this->createPackage($version, 'Composer\Package\Package'); $package->setRepository($this); - $this->providers[$name][$version['uid']] = $package; - $this->providersByUid[$version['uid']] = $package; + if ($package instanceof AliasPackage) { + $aliased = $package->getAliasOf(); + $aliased->setRepository($this); - if ($package->getAlias()) { - $alias = $this->createAliasPackage($package); - $alias->setRepository($this); + $this->providers[$name][$version['uid']] = $aliased; + $this->providers[$name][$version['uid'].'-alias'] = $package; - $this->providers[$name][$version['uid'].'-alias'] = $alias; // override provider with its alias so it can be expanded in the if block above - $this->providersByUid[$version['uid']] = $alias; + $this->providersByUid[$version['uid']] = $package; + } else { + $this->providers[$name][$version['uid']] = $package; + $this->providersByUid[$version['uid']] = $package; } // handle root package aliases unset($rootAliasData); - if (isset($this->rootAliases[$name][$package->getVersion()])) { - $rootAliasData = $this->rootAliases[$name][$package->getVersion()]; - } elseif (($aliasNormalized = $package->getAlias()) && isset($this->rootAliases[$name][$aliasNormalized])) { - $rootAliasData = $this->rootAliases[$name][$aliasNormalized]; + 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)) { @@ -291,6 +453,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) { @@ -311,22 +484,51 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository $data = $this->fetchFile($jsonUrl, 'packages.json'); - if (!empty($data['notify_batch'])) { - if ('/' === $data['notify_batch'][0]) { - $this->notifyUrl = preg_replace('{(https?://[^/]+).*}i', '$1' . $data['notify_batch'], $this->url); - } else { - $this->notifyUrl = $data['notify_batch']; - } + if (!empty($data['notify-batch'])) { + $this->notifyUrl = $this->canonicalizeUrl($data['notify-batch']); + } elseif (!empty($data['notify_batch'])) { + // TODO remove this BC notify_batch support + $this->notifyUrl = $this->canonicalizeUrl($data['notify_batch']); + } elseif (!empty($data['notify'])) { + $this->notifyUrl = $this->canonicalizeUrl($data['notify']); } - if (!$this->notifyUrl && !empty($data['notify'])) { - if ('/' === $data['notify'][0]) { - $this->notifyUrl = preg_replace('{(https?://[^/]+).*}i', '$1' . $data['notify'], $this->url); - } else { - $this->notifyUrl = $data['notify']; + if (!empty($data['search'])) { + $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); + } + + if (!empty($data['providers-url'])) { + $this->providersUrl = $this->canonicalizeUrl($data['providers-url']); + $this->hasProviders = true; + } + if (!empty($data['providers']) || !empty($data['providers-includes'])) { $this->hasProviders = true; } @@ -334,6 +536,15 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository return $this->rootData = $data; } + protected function canonicalizeUrl($url) + { + if ('/' === $url[0]) { + return preg_replace('{(https?://[^/]+).*}i', '$1' . $url, $this->url); + } + + return $url; + } + protected function loadDataFromServer() { $data = $this->loadRootServerFile(); @@ -350,8 +561,23 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository $this->providerListing = array_merge($this->providerListing, $data['providers']); } - if (isset($data['providers-includes'])) { - foreach ($data['providers-includes'] as $include => $metadata) { + if ($this->providersUrl && isset($data['provider-includes'])) { + $includes = $data['provider-includes']; + foreach ($includes as $include => $metadata) { + $url = $this->baseUrl . '/' . str_replace('%hash%', $metadata['sha256'], $include); + $cacheKey = str_replace(array('%hash%','$'), '', $include); + if ($this->cache->sha256($cacheKey) === $metadata['sha256']) { + $includedData = json_decode($this->cache->read($cacheKey), true); + } else { + $includedData = $this->fetchFile($url, $cacheKey, $metadata['sha256']); + } + + $this->loadProviderListings($includedData); + } + } elseif (isset($data['providers-includes'])) { + // BC layer for old-style providers-includes + $includes = $data['providers-includes']; + foreach ($includes as $include => $metadata) { if ($this->cache->sha256($include) === $metadata['sha256']) { $includedData = json_decode($this->cache->read($include), true); } else { @@ -403,9 +629,18 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository protected function createPackage(array $data, $class) { try { - $data['notification-url'] = $this->notifyUrl; + if (!isset($data['notification-url'])) { + $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); } @@ -413,7 +648,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; } @@ -421,39 +656,51 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository $retries = 3; while ($retries--) { try { - $json = new JsonFile($filename, new RemoteFilesystem($this->io, $this->options)); - $data = $json->read(); - $encoded = json_encode($data); - if ($sha256 && $sha256 !== hash('sha256', $encoded)) { + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $filename); + if ($this->eventDispatcher) { + $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + } + + $hostname = parse_url($filename, PHP_URL_HOST) ?: $filename; + $json = $preFileDownloadEvent->getRemoteFilesystem()->getContents($hostname, $filename, false); + if ($sha256 && $sha256 !== hash('sha256', $json)) { if ($retries) { - usleep(100); + usleep(100000); continue; } - // TODO throw SecurityException and abort once we are sure this can not happen accidentally - $this->io->write('The contents of '.$filename.' do not match its signature, this is most likely due to a temporary glitch but could indicate a man-in-the-middle attack. Try running composer again and please report it if it still persists.'); + // TODO use scarier wording once we know for sure it doesn't do false positives anymore + 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); + if ($cacheKey) { + $this->cache->write($cacheKey, $json); } - $this->cache->write($cacheKey, $encoded); break; } catch (\Exception $e) { - if (!$retries) { - if ($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'); - } - $this->degradedMode = true; - $data = JsonFile::parseJson($contents, $this->cache->getRoot().$cacheKey); + if ($retries) { + usleep(100000); + continue; + } - break; + if ($e instanceof RepositorySecurityException) { + throw $e; + } + + 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'); } + $this->degradedMode = true; + $data = JsonFile::parseJson($contents, $this->cache->getRoot().$cacheKey); - throw $e; + break; } - usleep(100); + throw $e; } } diff --git a/src/Composer/Repository/CompositeRepository.php b/src/Composer/Repository/CompositeRepository.php index 3467f04d9..517c52b5b 100644 --- a/src/Composer/Repository/CompositeRepository.php +++ b/src/Composer/Repository/CompositeRepository.php @@ -91,7 +91,21 @@ class CompositeRepository implements RepositoryInterface $packages[] = $repository->findPackages($name, $version); } - return call_user_func_array('array_merge', $packages); + return $packages ? call_user_func_array('array_merge', $packages) : array(); + } + + /** + * {@inheritdoc} + */ + public function search($query, $mode = 0) + { + $matches = array(); + foreach ($this->repositories as $repository) { + /* @var $repository RepositoryInterface */ + $matches[] = $repository->search($query, $mode); + } + + return $matches ? call_user_func_array('array_merge', $matches) : array(); } /** @@ -119,7 +133,7 @@ class CompositeRepository implements RepositoryInterface $packages[] = $repository->getPackages(); } - return call_user_func_array('array_merge', $packages); + return $packages ? call_user_func_array('array_merge', $packages) : array(); } /** diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index 1c205cf54..0ffac9f1d 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -13,7 +13,6 @@ namespace Composer\Repository; use Composer\Json\JsonFile; -use Composer\Package\AliasPackage; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; @@ -23,7 +22,7 @@ use Composer\Package\Dumper\ArrayDumper; * @author Konstantin Kudryashov * @author Jordi Boggiano */ -class FilesystemRepository extends ArrayRepository implements WritableRepositoryInterface +class FilesystemRepository extends WritableArrayRepository { private $file; @@ -58,7 +57,7 @@ class FilesystemRepository extends ArrayRepository implements WritableRepository 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); @@ -76,15 +75,13 @@ class FilesystemRepository extends ArrayRepository implements WritableRepository */ public function write() { - $packages = array(); - $dumper = new ArrayDumper(); - foreach ($this->getPackages() as $package) { - if (!$package instanceof AliasPackage) { - $data = $dumper->dump($package); - $packages[] = $data; - } + $data = array(); + $dumper = new ArrayDumper(); + + foreach ($this->getCanonicalPackages() as $package) { + $data[] = $dumper->dump($package); } - $this->file->write($packages); + $this->file->write($data); } } diff --git a/src/Composer/Repository/InstalledArrayRepository.php b/src/Composer/Repository/InstalledArrayRepository.php index 1343e0d3f..c801d49ea 100644 --- a/src/Composer/Repository/InstalledArrayRepository.php +++ b/src/Composer/Repository/InstalledArrayRepository.php @@ -19,19 +19,6 @@ namespace Composer\Repository; * * @author Jordi Boggiano */ -class InstalledArrayRepository extends ArrayRepository implements InstalledRepositoryInterface +class InstalledArrayRepository extends WritableArrayRepository implements InstalledRepositoryInterface { - /** - * {@inheritDoc} - */ - public function write() - { - } - - /** - * {@inheritDoc} - */ - public function reload() - { - } } diff --git a/src/Composer/Repository/Pear/BaseChannelReader.php b/src/Composer/Repository/Pear/BaseChannelReader.php index 2f586ad87..e3ab71af7 100644 --- a/src/Composer/Repository/Pear/BaseChannelReader.php +++ b/src/Composer/Repository/Pear/BaseChannelReader.php @@ -46,6 +46,7 @@ abstract class BaseChannelReader * * @param $origin string server * @param $path string relative path to content + * @throws \UnexpectedValueException * @return \SimpleXMLElement */ protected function requestContent($origin, $path) @@ -64,6 +65,7 @@ abstract class BaseChannelReader * * @param $origin string server * @param $path string relative path to content + * @throws \UnexpectedValueException * @return \SimpleXMLElement */ protected function requestXml($origin, $path) diff --git a/src/Composer/Repository/Pear/ChannelReader.php b/src/Composer/Repository/Pear/ChannelReader.php index bb909b3b7..849055959 100644 --- a/src/Composer/Repository/Pear/ChannelReader.php +++ b/src/Composer/Repository/Pear/ChannelReader.php @@ -45,6 +45,7 @@ class ChannelReader extends BaseChannelReader * Reads PEAR channel through REST interface and builds list of packages * * @param $url string PEAR Channel url + * @throws \UnexpectedValueException * @return ChannelInfo */ public function read($url) diff --git a/src/Composer/Repository/Pear/ChannelRest10Reader.php b/src/Composer/Repository/Pear/ChannelRest10Reader.php index cd3985da5..5b5fd7828 100644 --- a/src/Composer/Repository/Pear/ChannelRest10Reader.php +++ b/src/Composer/Repository/Pear/ChannelRest10Reader.php @@ -107,7 +107,8 @@ class ChannelRest10Reader extends BaseChannelReader * * @param $baseUrl string * @param $packageName string - * @return ReleaseInfo[] hash array with keys as version numbers + * @throws \Composer\Downloader\TransportException|\Exception + * @return ReleaseInfo[] hash array with keys as version numbers */ private function readPackageReleases($baseUrl, $packageName) { diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php index 8c5d9362b..5006c4927 100644 --- a/src/Composer/Repository/PearRepository.php +++ b/src/Composer/Repository/PearRepository.php @@ -17,6 +17,7 @@ use Composer\Package\Version\VersionParser; use Composer\Repository\Pear\ChannelReader; use Composer\Package\CompletePackage; use Composer\Repository\Pear\ChannelInfo; +use Composer\EventDispatcher\EventDispatcher; use Composer\Package\Link; use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Util\RemoteFilesystem; @@ -43,7 +44,7 @@ class PearRepository extends ArrayRepository */ private $vendorAlias; - public function __construct(array $repoConfig, IOInterface $io, Config $config, RemoteFilesystem $rfs = null) + public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $dispatcher = null, RemoteFilesystem $rfs = null) { if (!preg_match('{^https?://}', $repoConfig['url'])) { $repoConfig['url'] = 'http://'.$repoConfig['url']; @@ -56,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(); } @@ -84,8 +85,8 @@ class PearRepository extends ArrayRepository /** * Builds CompletePackages from PEAR package definition data. * - * @param ChannelInfo $channelInfo - * @param VersionParser $versionParser + * @param ChannelInfo $channelInfo + * @param VersionParser $versionParser * @return CompletePackage */ private function buildComposerPackages(ChannelInfo $channelInfo, VersionParser $versionParser) diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 1cf11c47a..636cba16b 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -14,18 +14,27 @@ namespace Composer\Repository; use Composer\Package\CompletePackage; use Composer\Package\Version\VersionParser; +use Composer\Plugin\PluginInterface; /** * @author Jordi Boggiano */ class PlatformRepository extends ArrayRepository { + const PLATFORM_PACKAGE_REGEX = '{^(?:php(?:-64bit)?|hhvm|(?:ext|lib)-[^/]+)$}i'; + protected function initialize() { parent::initialize(); $versionParser = new VersionParser(); + $prettyVersion = PluginInterface::PLUGIN_API_VERSION; + $version = $versionParser->normalize($prettyVersion); + $composerPluginApi = new CompletePackage('composer-plugin-api', $version, $prettyVersion); + $composerPluginApi->setDescription('The Composer Plugin API'); + parent::addPackage($composerPluginApi); + try { $prettyVersion = PHP_VERSION; $version = $versionParser->normalize($prettyVersion); @@ -38,6 +47,12 @@ class PlatformRepository extends ArrayRepository $php->setDescription('The PHP interpreter'); parent::addPackage($php); + if (PHP_INT_SIZE === 8) { + $php64 = new CompletePackage('php-64bit', $version, $prettyVersion); + $php64->setDescription('The PHP interpreter (64bit)'); + parent::addPackage($php64); + } + $loadedExtensions = get_loaded_extensions(); // Extensions scanning @@ -55,7 +70,8 @@ class PlatformRepository extends ArrayRepository $version = $versionParser->normalize($prettyVersion); } - $ext = new CompletePackage('ext-'.$name, $version, $prettyVersion); + $packageName = $this->buildPackageName($name); + $ext = new CompletePackage($packageName, $version, $prettyVersion); $ext->setDescription('The '.$name.' PHP extension'); parent::addPackage($ext); } @@ -64,6 +80,7 @@ class PlatformRepository extends ArrayRepository // Doing it this way to know that functions or constants exist before // relying on them. foreach ($loadedExtensions as $name) { + $prettyVersion = null; switch ($name) { case 'curl': $curlVersion = curl_version(); @@ -74,6 +91,23 @@ class PlatformRepository extends ArrayRepository $prettyVersion = ICONV_VERSION; break; + case 'intl': + $name = 'ICU'; + if (defined('INTL_ICU_VERSION')) { + $prettyVersion = INTL_ICU_VERSION; + } else { + $reflector = new \ReflectionExtension('intl'); + + ob_start(); + $reflector->info(); + $output = ob_get_clean(); + + preg_match('/^ICU version => (.*)$/m', $output, $matches); + $prettyVersion = $matches[1]; + } + + break; + case 'libxml': $prettyVersion = LIBXML_DOTTED_VERSION; break; @@ -111,5 +145,24 @@ class PlatformRepository extends ArrayRepository $lib->setDescription('The '.$name.' PHP library'); parent::addPackage($lib); } + + if (defined('HHVM_VERSION')) { + try { + $prettyVersion = HHVM_VERSION; + $version = $versionParser->normalize($prettyVersion); + } catch (\UnexpectedValueException $e) { + $prettyVersion = preg_replace('#^([^~+-]+).*$#', '$1', HHVM_VERSION); + $version = $versionParser->normalize($prettyVersion); + } + + $hhvm = new CompletePackage('hhvm', $version, $prettyVersion); + $hhvm->setDescription('The HHVM Runtime (64bit)'); + parent::addPackage($hhvm); + } + } + + private function buildPackageName($name) + { + return 'ext-' . str_replace(' ', '-', $name); } } diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index 69e48e4fb..0d6c50414 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -19,9 +19,13 @@ use Composer\Package\PackageInterface; * * @author Nils Adermann * @author Konstantin Kudryashov + * @author Jordi Boggiano */ interface RepositoryInterface extends \Countable { + const SEARCH_FULLTEXT = 0; + const SEARCH_NAME = 1; + /** * Checks if specified package registered (installed). * @@ -52,23 +56,18 @@ interface RepositoryInterface extends \Countable public function findPackages($name, $version = null); /** - * Filters all the packages through a callback - * - * The packages are not guaranteed to be instances in the repository - * and this can only be used for streaming through a list of packages. - * - * If the callback returns false, the process stops + * Returns list of registered packages. * - * @param callable $callback - * @param string $class - * @return bool false if the process was interrupted, true otherwise + * @return array */ - public function filterPackages($callback, $class = 'Composer\Package\Package'); + public function getPackages(); /** - * Returns list of registered packages. + * Searches the repository for packages containing the query * - * @return array + * @param string $query search query + * @param int $mode a set of SEARCH_* constants to search on, implementations should do a best effort only + * @return array[] an array of array('name' => '...', 'description' => '...') */ - public function getPackages(); + public function search($query, $mode = 0); } diff --git a/src/Composer/Repository/RepositoryManager.php b/src/Composer/Repository/RepositoryManager.php index ea0dca137..83352cf2f 100644 --- a/src/Composer/Repository/RepositoryManager.php +++ b/src/Composer/Repository/RepositoryManager.php @@ -14,6 +14,7 @@ namespace Composer\Repository; use Composer\IO\IOInterface; use Composer\Config; +use Composer\EventDispatcher\EventDispatcher; /** * Repositories manager. @@ -25,16 +26,17 @@ use Composer\Config; class RepositoryManager { private $localRepository; - private $localDevRepository; private $repositories = array(); private $repositoryClasses = array(); private $io; private $config; + private $eventDispatcher; - public function __construct(IOInterface $io, Config $config) + public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null) { $this->io = $io; $this->config = $config; + $this->eventDispatcher = $eventDispatcher; } /** @@ -86,10 +88,10 @@ class RepositoryManager /** * Returns a new repository for a specific installation type. * - * @param string $type repository type - * @param string $config repository configuration + * @param string $type repository type + * @param array $config repository configuration * @return RepositoryInterface - * @throws InvalidArgumentException if repository for provided type is not registered + * @throws \InvalidArgumentException if repository for provided type is not registered */ public function createRepository($type, $config) { @@ -99,7 +101,7 @@ class RepositoryManager $class = $this->repositoryClasses[$type]; - return new $class($config, $this->io, $this->config); + return new $class($config, $this->io, $this->config, $this->eventDispatcher); } /** @@ -126,9 +128,9 @@ class RepositoryManager /** * Sets local repository for the project. * - * @param RepositoryInterface $repository repository instance + * @param WritableRepositoryInterface $repository repository instance */ - public function setLocalRepository(RepositoryInterface $repository) + public function setLocalRepository(WritableRepositoryInterface $repository) { $this->localRepository = $repository; } @@ -136,40 +138,23 @@ class RepositoryManager /** * Returns local repository for the project. * - * @return RepositoryInterface + * @return WritableRepositoryInterface */ public function getLocalRepository() { return $this->localRepository; } - /** - * Sets localDev repository for the project. - * - * @param RepositoryInterface $repository repository instance - */ - public function setLocalDevRepository(RepositoryInterface $repository) - { - $this->localDevRepository = $repository; - } - - /** - * Returns localDev repository for the project. - * - * @return RepositoryInterface - */ - public function getLocalDevRepository() - { - return $this->localDevRepository; - } - /** * Returns all local repositories for the project. * + * @deprecated getLocalDevRepository is gone, so this is useless now, just use getLocalRepository instead * @return array[WritableRepositoryInterface] */ public function getLocalRepositories() { - return array($this->localRepository, $this->localDevRepository); + trigger_error('This method is deprecated, use getLocalRepository instead since the getLocalDevRepository is now gone', E_USER_DEPRECATED); + + return array($this->localRepository); } } diff --git a/src/Composer/Repository/RepositorySecurityException.php b/src/Composer/Repository/RepositorySecurityException.php new file mode 100644 index 000000000..b115d9cbe --- /dev/null +++ b/src/Composer/Repository/RepositorySecurityException.php @@ -0,0 +1,22 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository; + +/** + * Thrown when a security problem, like a broken or missing signature + * + * @author Eric Daspet + */ +class RepositorySecurityException extends \Exception +{ +} diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php index b0daaba5d..12eba6d3a 100644 --- a/src/Composer/Repository/Vcs/GitBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php @@ -12,6 +12,7 @@ namespace Composer\Repository\Vcs; +use Composer\Config; use Composer\Json\JsonFile; use Composer\IO\IOInterface; @@ -65,9 +66,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function getSource($identifier) { - $label = array_search($identifier, $this->getTags()) ?: $identifier; - - return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $label); + return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $identifier); } /** @@ -75,10 +74,9 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function getDist($identifier) { - $label = array_search($identifier, $this->getTags()) ?: $identifier; - $url = $this->getScheme() . '://bitbucket.org/'.$this->owner.'/'.$this->repository.'/get/'.$label.'.zip'; + $url = $this->getScheme() . '://bitbucket.org/'.$this->owner.'/'.$this->repository.'/get/'.$identifier.'.zip'; - return array('type' => 'zip', 'url' => $url, 'reference' => $label, 'shasum' => ''); + return array('type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => ''); } /** @@ -143,7 +141,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface /** * {@inheritDoc} */ - public static function supports(IOInterface $io, $url, $deep = false) + public static function supports(IOInterface $io, Config $config, $url, $deep = false) { 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 ee7489be9..a020b92c2 100644 --- a/src/Composer/Repository/Vcs/GitDriver.php +++ b/src/Composer/Repository/Vcs/GitDriver.php @@ -15,13 +15,17 @@ namespace Composer\Repository\Vcs; use Composer\Json\JsonFile; use Composer\Util\ProcessExecutor; use Composer\Util\Filesystem; +use Composer\Util\Git as GitUtil; use Composer\IO\IOInterface; +use Composer\Cache; +use Composer\Config; /** * @author Jordi Boggiano */ class GitDriver extends VcsDriver { + protected $cache; protected $tags; protected $branches; protected $rootIdentifier; @@ -33,11 +37,14 @@ 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) . '/'; + GitUtil::cleanEnv(); + $fs = new Filesystem(); $fs->ensureDirectoryExists(dirname($this->repoDir)); @@ -49,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); - // added in git 1.7.1, prevents prompting the user - putenv('GIT_ASKPASS=echo'); - $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', '-', $cacheUrl)); } /** @@ -114,9 +126,7 @@ class GitDriver extends VcsDriver */ public function getSource($identifier) { - $label = array_search($identifier, (array) $this->tags) ?: $identifier; - - return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $label); + return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $identifier); } /** @@ -132,8 +142,12 @@ class GitDriver extends VcsDriver */ public function getComposerInformation($identifier) { + if (preg_match('{[a-f0-9]{40}}i', $identifier) && $res = $this->cache->read($identifier)) { + $this->infoCache[$identifier] = JsonFile::parseJson($res); + } + 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)) { @@ -143,10 +157,15 @@ 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); + $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'); } + + if (preg_match('{[a-f0-9]{40}}i', $identifier)) { + $this->cache->write($identifier, json_encode($composer)); + } + $this->infoCache[$identifier] = $composer; } @@ -159,9 +178,14 @@ class GitDriver extends VcsDriver public function getTags() { if (null === $this->tags) { - $this->process->execute('git tag', $output, $this->repoDir); - $output = $this->process->splitLines($output); - $this->tags = $output ? array_combine($output, $output) : array(); + $this->tags = array(); + + $this->process->execute('git show-ref --tags', $output, $this->repoDir); + foreach ($output = $this->process->splitLines($output) as $tag) { + if ($tag && preg_match('{^([a-f0-9]{40}) refs/tags/(\S+)$}', $tag, $match)) { + $this->tags[$match[2]] = $match[1]; + } + } } return $this->tags; @@ -193,20 +217,20 @@ class GitDriver extends VcsDriver /** * {@inheritDoc} */ - public static function supports(IOInterface $io, $url, $deep = false) + public static function supports(IOInterface $io, Config $config, $url, $deep = false) { if (preg_match('#(^git://|\.git$|git(?:olite)?@|//git\.|//github.com/)#i', $url)) { return true; } // 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); } - $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; diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php old mode 100755 new mode 100644 index 1a65bb702..06b805714 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -12,11 +12,11 @@ namespace Composer\Repository\Vcs; +use Composer\Config; use Composer\Downloader\TransportException; use Composer\Json\JsonFile; use Composer\Cache; use Composer\IO\IOInterface; -use Composer\Util\RemoteFilesystem; use Composer\Util\GitHub; /** @@ -46,15 +46,26 @@ class GitHubDriver extends VcsDriver */ public function initialize() { - preg_match('#^(?:(?:https?|git)://github\.com/|git@github\.com:)([^/]+)/(.+?)(?:\.git)?$#', $this->url, $match); - $this->owner = $match[1]; - $this->repository = $match[2]; - $this->originUrl = 'github.com'; + 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} */ @@ -76,7 +87,21 @@ class GitHubDriver extends VcsDriver return $this->gitDriver->getUrl(); } - return $this->url; + return 'https://' . $this->originUrl . '/'.$this->owner.'/'.$this->repository.'.git'; + } + + /** + * {@inheritDoc} + */ + protected function getApiUrl() + { + if ('github.com' === $this->originUrl) { + $apiUrl = 'api.github.com'; + } else { + $apiUrl = $this->originUrl . '/api/v3'; + } + + return 'https://' . $apiUrl; } /** @@ -87,7 +112,6 @@ class GitHubDriver extends VcsDriver if ($this->gitDriver) { return $this->gitDriver->getSource($identifier); } - $label = array_search($identifier, $this->getTags()) ?: $identifier; if ($this->isPrivate) { // Private GitHub repositories should be accessed using the // SSH version of the URL. @@ -96,7 +120,7 @@ class GitHubDriver extends VcsDriver $url = $this->getUrl(); } - return array('type' => 'git', 'url' => $url, 'reference' => $label); + return array('type' => 'git', 'url' => $url, 'reference' => $identifier); } /** @@ -104,13 +128,9 @@ class GitHubDriver extends VcsDriver */ public function getDist($identifier) { - if ($this->gitDriver) { - return $this->gitDriver->getDist($identifier); - } - $label = array_search($identifier, $this->getTags()) ?: $identifier; - $url = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/zipball/'.$label; + $url = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/zipball/'.$identifier; - return array('type' => 'zip', 'url' => $url, 'reference' => $label, 'shasum' => ''); + return array('type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => ''); } /** @@ -127,31 +147,41 @@ class GitHubDriver extends VcsDriver } if (!isset($this->infoCache[$identifier])) { - try { - $resource = 'https://raw.github.com/'.$this->owner.'/'.$this->repository.'/'.urlencode($identifier).'/composer.json'; - $composer = $this->getContents($resource); - } catch (TransportException $e) { - if (404 !== $e->getCode()) { - throw $e; - } + $notFoundRetries = 2; + while ($notFoundRetries) { + try { + $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/contents/composer.json?ref='.urlencode($identifier); + $composer = JsonFile::parseJson($this->getContents($resource)); + if (empty($composer['content']) || $composer['encoding'] !== 'base64' || !($composer = base64_decode($composer['content']))) { + throw new \RuntimeException('Could not retrieve composer.json from '.$resource); + } + break; + } catch (TransportException $e) { + if (404 !== $e->getCode()) { + throw $e; + } - $composer = false; + // TODO should be removed when possible + // retry fetching if github returns a 404 since they happen randomly + $notFoundRetries--; + $composer = false; + } } if ($composer) { $composer = JsonFile::parseJson($composer, $resource); if (!isset($composer['time'])) { - $resource = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier); + $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier); $commit = JsonFile::parseJson($this->getContents($resource), $resource); $composer['time'] = $commit['commit']['committer']['date']; } if (!isset($composer['support']['source'])) { $label = array_search($identifier, $this->getTags()) ?: array_search($identifier, $this->getBranches()) ?: $identifier; - $composer['support']['source'] = sprintf('https://github.com/%s/%s/tree/%s', $this->owner, $this->repository, $label); + $composer['support']['source'] = sprintf('https://%s/%s/%s/tree/%s', $this->originUrl, $this->owner, $this->repository, $label); } if (!isset($composer['support']['issues']) && $this->hasIssues) { - $composer['support']['issues'] = sprintf('https://github.com/%s/%s/issues', $this->owner, $this->repository); + $composer['support']['issues'] = sprintf('https://%s/%s/%s/issues', $this->originUrl, $this->owner, $this->repository); } } @@ -174,12 +204,17 @@ class GitHubDriver extends VcsDriver return $this->gitDriver->getTags(); } if (null === $this->tags) { - $resource = 'https://api.github.com/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; @@ -194,13 +229,22 @@ class GitHubDriver extends VcsDriver return $this->gitDriver->getBranches(); } if (null === $this->branches) { - $resource = 'https://api.github.com/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; @@ -209,9 +253,14 @@ class GitHubDriver extends VcsDriver /** * {@inheritDoc} */ - public static function supports(IOInterface $io, $url, $deep = false) + public static function supports(IOInterface $io, Config $config, $url, $deep = false) { - if (!preg_match('#^((?:https?|git)://github\.com/|git@github\.com:)([^/]+)/(.+?)(?:\.git)?$#', $url)) { + if (!preg_match('#^((?:https?|git)://([^/]+)/|git@([^:]+):)([^/]+)/(.+?)(?:\.git|/)?$#', $url, $matches)) { + return false; + } + + $originUrl = !empty($matches[2]) ? $matches[2] : $matches[3]; + if (!in_array($originUrl, $config->get('github-domains'))) { return false; } @@ -233,7 +282,7 @@ class GitHubDriver extends VcsDriver */ protected function generateSshUrl() { - return 'git@github.com:'.$this->owner.'/'.$this->repository.'.git'; + return 'git@' . $this->originUrl . ':'.$this->owner.'/'.$this->repository.'.git'; } /** @@ -294,7 +343,12 @@ class GitHubDriver extends VcsDriver } if ($rateLimited) { - $this->io->write('GitHub API limit exhausted. You are already authorized so you will have to wait a while before doing more requests'); + $rateLimit = $this->getRateLimit($e->getHeaders()); + $this->io->write(sprintf( + 'GitHub API limit (%d calls/hr) is exhausted. You are already authorized so you have to wait until %s before doing more requests', + $rateLimit['limit'], + $rateLimit['reset'] + )); } throw $e; @@ -305,6 +359,39 @@ class GitHubDriver extends VcsDriver } } + /** + * Extract ratelimit from response. + * + * @param array $headers Headers from Composer\Downloader\TransportException. + * + * @return array Associative array with the keys limit and reset. + */ + protected function getRateLimit(array $headers) + { + $rateLimit = array( + 'limit' => '?', + 'reset' => '?', + ); + + foreach ($headers as $header) { + $header = trim($header); + if (false === strpos($header, 'X-RateLimit-')) { + continue; + } + list($type, $value) = explode(':', $header, 2); + switch ($type) { + case 'X-RateLimit-Limit': + $rateLimit['limit'] = (int) trim($value); + break; + case 'X-RateLimit-Reset': + $rateLimit['reset'] = date('Y-m-d H:i:s', (int) trim($value)); + break; + } + } + + return $rateLimit; + } + /** * Fetch root identifier from GitHub * @@ -312,13 +399,16 @@ class GitHubDriver extends VcsDriver */ protected function fetchRootIdentifier() { - $repoDataUrl = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository; + $repoDataUrl = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository; $repoData = JsonFile::parseJson($this->getContents($repoDataUrl, true), $repoDataUrl); if (null === $repoData && null !== $this->gitDriver) { 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']; @@ -339,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) { @@ -356,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 f31f61c44..c6eac73b9 100644 --- a/src/Composer/Repository/Vcs/HgBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/HgBitbucketDriver.php @@ -12,6 +12,7 @@ namespace Composer\Repository\Vcs; +use Composer\Config; use Composer\Json\JsonFile; use Composer\IO\IOInterface; @@ -44,8 +45,11 @@ class HgBitbucketDriver extends VcsDriver public function getRootIdentifier() { if (null === $this->rootIdentifier) { - $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'; + $resource = $this->getScheme() . '://bitbucket.org/api/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'; $repoData = JsonFile::parseJson($this->getContents($resource), $resource); + if (array() === $repoData || !isset($repoData['tip'])) { + throw new \RuntimeException($this->url.' does not appear to be a mercurial repository, use '.$this->url.'.git if this is a git bitbucket repository'); + } $this->rootIdentifier = $repoData['tip']['raw_node']; } @@ -65,9 +69,7 @@ class HgBitbucketDriver extends VcsDriver */ public function getSource($identifier) { - $label = array_search($identifier, $this->getTags()) ?: $identifier; - - return array('type' => 'hg', 'url' => $this->getUrl(), 'reference' => $label); + return array('type' => 'hg', 'url' => $this->getUrl(), 'reference' => $identifier); } /** @@ -75,10 +77,9 @@ class HgBitbucketDriver extends VcsDriver */ public function getDist($identifier) { - $label = array_search($identifier, $this->getTags()) ?: $identifier; - $url = $this->getScheme() . '://bitbucket.org/'.$this->owner.'/'.$this->repository.'/get/'.$label.'.zip'; + $url = $this->getScheme() . '://bitbucket.org/'.$this->owner.'/'.$this->repository.'/get/'.$identifier.'.zip'; - return array('type' => 'zip', 'url' => $url, 'reference' => $label, 'shasum' => ''); + return array('type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => ''); } /** @@ -87,16 +88,22 @@ class HgBitbucketDriver extends VcsDriver public function getComposerInformation($identifier) { if (!isset($this->infoCache[$identifier])) { - $resource = $this->getScheme() . '://bitbucket.org/'.$this->owner.'/'.$this->repository.'/raw/'.$identifier.'/composer.json'; - $composer = $this->getContents($resource); - if (!$composer) { + $resource = $this->getScheme() . '://bitbucket.org/api/1.0/repositories/'.$this->owner.'/'.$this->repository.'/src/'.$identifier.'/composer.json'; + $repoData = JsonFile::parseJson($this->getContents($resource), $resource); + + // Bitbucket does not send different response codes for found and + // not found files, so we have to check the response structure. + // found: {node: ..., data: ..., size: ..., ...} + // not found: {node: ..., files: [...], directories: [...], ...} + + if (!array_key_exists('data', $repoData)) { return; } - $composer = JsonFile::parseJson($composer, $resource); + $composer = JsonFile::parseJson($repoData['data'], $resource); if (!isset($composer['time'])) { - $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/changesets/'.$identifier; + $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']; } @@ -112,12 +119,13 @@ class HgBitbucketDriver extends VcsDriver public function getTags() { if (null === $this->tags) { - $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'; + $resource = $this->getScheme() . '://bitbucket.org/api/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'; $tagsData = JsonFile::parseJson($this->getContents($resource), $resource); $this->tags = array(); foreach ($tagsData as $tag => $data) { $this->tags[$tag] = $data['raw_node']; } + unset($this->tags['tip']); } return $this->tags; @@ -129,7 +137,7 @@ class HgBitbucketDriver extends VcsDriver public function getBranches() { if (null === $this->branches) { - $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/branches'; + $resource = $this->getScheme() . '://bitbucket.org/api/1.0/repositories/'.$this->owner.'/'.$this->repository.'/branches'; $branchData = JsonFile::parseJson($this->getContents($resource), $resource); $this->branches = array(); foreach ($branchData as $branch => $data) { @@ -143,7 +151,7 @@ class HgBitbucketDriver extends VcsDriver /** * {@inheritDoc} */ - public static function supports(IOInterface $io, $url, $deep = false) + public static function supports(IOInterface $io, Config $config, $url, $deep = false) { 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 old mode 100755 new mode 100644 index 1f7d8ed1a..eb962f276 --- a/src/Composer/Repository/Vcs/HgDriver.php +++ b/src/Composer/Repository/Vcs/HgDriver.php @@ -12,6 +12,7 @@ namespace Composer\Repository\Vcs; +use Composer\Config; use Composer\Json\JsonFile; use Composer\Util\ProcessExecutor; use Composer\Util\Filesystem; @@ -33,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) . '/'; @@ -48,14 +49,14 @@ class HgDriver extends VcsDriver // update the repo if it is a valid hg repository if (is_dir($this->repoDir) && 0 === $this->process->execute('hg summary', $output, $this->repoDir)) { - if (0 !== $this->process->execute('hg pull -u', $output, $this->repoDir)) { + if (0 !== $this->process->execute('hg pull', $output, $this->repoDir)) { $this->io->write('Failed to update '.$this->url.', package information from this repository may be outdated ('.$this->process->getErrorOutput().')'); } } else { // clean up directory and do a fresh clone into it $fs->removeDirectory($this->repoDir); - if (0 !== $this->process->execute(sprintf('hg clone %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)) { @@ -98,9 +99,7 @@ class HgDriver extends VcsDriver */ public function getSource($identifier) { - $label = array_search($identifier, (array) $this->tags) ? : $identifier; - - return array('type' => 'hg', 'url' => $this->getUrl(), 'reference' => $label); + return array('type' => 'hg', 'url' => $this->getUrl(), 'reference' => $identifier); } /** @@ -117,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; @@ -126,7 +125,7 @@ class HgDriver extends VcsDriver $composer = JsonFile::parseJson($composer, $identifier); if (!isset($composer['time'])) { - $this->process->execute(sprintf('hg log --template "{date|rfc822date}" -r %s', escapeshellarg($identifier)), $output, $this->repoDir); + $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'); } @@ -191,20 +190,20 @@ class HgDriver extends VcsDriver /** * {@inheritDoc} */ - public static function supports(IOInterface $io, $url, $deep = false) + public static function supports(IOInterface $io, Config $config, $url, $deep = false) { if (preg_match('#(^(?:https?|ssh)://(?:[^@]@)?bitbucket.org|https://(?:.*?)\.kilnhg.com)#i', $url)) { return true; } // 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); } $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; @@ -216,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 new file mode 100644 index 000000000..7e313bf63 --- /dev/null +++ b/src/Composer/Repository/Vcs/PerforceDriver.php @@ -0,0 +1,186 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository\Vcs; + +use Composer\Config; +use Composer\IO\IOInterface; +use Composer\Util\ProcessExecutor; +use Composer\Util\Perforce; + +/** + * @author Matt Whittom + */ +class PerforceDriver extends VcsDriver +{ + protected $depot; + protected $branch; + protected $perforce; + protected $composerInfo; + protected $composerInfoIdentifier; + + /** + * {@inheritDoc} + */ + public function initialize() + { + $this->depot = $this->repoConfig['depot']; + $this->branch = ''; + if (!empty($this->repoConfig['branch'])) { + $this->branch = $this->repoConfig['branch']; + } + + $this->initPerforce($this->repoConfig); + $this->perforce->p4Login($this->io); + $this->perforce->checkStream($this->depot); + + $this->perforce->writeP4ClientSpec(); + $this->perforce->connectClient(); + + return true; + } + + private function initPerforce($repoConfig) + { + 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->io); + } + + /** + * {@inheritDoc} + */ + public function getComposerInformation($identifier) + { + if (!empty($this->composerInfoIdentifier)) { + if (strcmp($identifier, $this->composerInfoIdentifier) === 0) { + return $this->composerInfo; + } + } + $composer_info = $this->perforce->getComposerInformation($identifier); + + return $composer_info; + } + + /** + * {@inheritDoc} + */ + public function getRootIdentifier() + { + return $this->branch; + } + + /** + * {@inheritDoc} + */ + public function getBranches() + { + $branches = $this->perforce->getBranches(); + + return $branches; + } + + /** + * {@inheritDoc} + */ + public function getTags() + { + $tags = $this->perforce->getTags(); + + return $tags; + } + + /** + * {@inheritDoc} + */ + public function getDist($identifier) + { + return null; + } + + /** + * {@inheritDoc} + */ + public function getSource($identifier) + { + $source = array( + 'type' => 'perforce', + 'url' => $this->repoConfig['url'], + 'reference' => $identifier, + 'p4user' => $this->perforce->getUser() + ); + + return $source; + } + + /** + * {@inheritDoc} + */ + public function getUrl() + { + return $this->url; + } + + /** + * {@inheritDoc} + */ + public function hasComposerFile($identifier) + { + $this->composerInfo = $this->perforce->getComposerInformation('//' . $this->depot . '/' . $identifier); + $this->composerInfoIdentifier = $identifier; + + return !empty($this->composerInfo); + } + + /** + * {@inheritDoc} + */ + public function getContents($url) + { + return false; + } + + /** + * {@inheritDoc} + */ + public static function supports(IOInterface $io, Config $config, $url, $deep = false) + { + if ($deep || preg_match('#\b(perforce|p4)\b#i', $url)) { + return Perforce::checkServerExists($url, new ProcessExecutor($io)); + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function cleanup() + { + $this->perforce->cleanupClientSpec(); + $this->perforce = null; + } + + public function getDepot() + { + return $this->depot; + } + + public function getBranch() + { + return $this->branch; + } + +} diff --git a/src/Composer/Repository/Vcs/SvnDriver.php b/src/Composer/Repository/Vcs/SvnDriver.php old mode 100755 new mode 100644 index af42c1f9b..729bda94e --- a/src/Composer/Repository/Vcs/SvnDriver.php +++ b/src/Composer/Repository/Vcs/SvnDriver.php @@ -13,6 +13,7 @@ namespace Composer\Repository\Vcs; use Composer\Cache; +use Composer\Config; use Composer\Json\JsonFile; use Composer\Util\ProcessExecutor; use Composer\Util\Filesystem; @@ -26,6 +27,10 @@ use Composer\Downloader\TransportException; */ class SvnDriver extends VcsDriver { + + /** + * @var Cache + */ protected $cache; protected $baseUrl; protected $tags; @@ -36,6 +41,8 @@ class SvnDriver extends VcsDriver protected $trunkPath = 'trunk'; protected $branchesPath = 'branches'; protected $tagsPath = 'tags'; + protected $packagePath = ''; + protected $cacheCredentials = true; /** * @var \Composer\Util\Svn @@ -49,6 +56,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']; } @@ -58,6 +67,12 @@ 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'], '/'); + } if (false !== ($pos = strrpos($this->url, '/' . $this->trunkPath))) { $this->baseUrl = substr($this->url, 0, $pos); @@ -167,8 +182,10 @@ class SvnDriver extends VcsDriver $line = trim($line); if ($line && preg_match('{^\s*(\S+).*?(\S+)\s*$}', $line, $match)) { if (isset($match[1]) && isset($match[2]) && $match[2] !== './') { - $this->tags[rtrim($match[2], '/')] = '/' . $this->tagsPath . - '/' . $match[2] . '@' . $match[1]; + $this->tags[rtrim($match[2], '/')] = $this->buildIdentifier( + '/' . $this->tagsPath . '/' . $match[2], + $match[1] + ); } } } @@ -187,14 +204,23 @@ class SvnDriver extends VcsDriver if (null === $this->branches) { $this->branches = array(); - $output = $this->execute('svn ls --verbose', $this->baseUrl . '/'); + if (false === $this->trunkPath) { + $trunkParent = $this->baseUrl . '/'; + } else { + $trunkParent = $this->baseUrl . '/' . $this->trunkPath; + } + + $output = $this->execute('svn ls --verbose', $trunkParent); if ($output) { 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->trunkPath . '/@'.$match[1]; - $this->rootIdentifier = $this->branches[$this->trunkPath]; + if (isset($match[1]) && isset($match[2]) && $match[2] === './') { + $this->branches['trunk'] = $this->buildIdentifier( + '/' . $this->trunkPath, + $match[1] + ); + $this->rootIdentifier = $this->branches['trunk']; break; } } @@ -209,8 +235,10 @@ class SvnDriver extends VcsDriver $line = trim($line); if ($line && preg_match('{^\s*(\S+).*?(\S+)\s*$}', $line, $match)) { if (isset($match[1]) && isset($match[2]) && $match[2] !== './') { - $this->branches[rtrim($match[2], '/')] = '/' . $this->branchesPath . - '/' . $match[2] . '@' . $match[1]; + $this->branches[rtrim($match[2], '/')] = $this->buildIdentifier( + '/' . $this->branchesPath . '/' . $match[2], + $match[1] + ); } } } @@ -224,7 +252,7 @@ class SvnDriver extends VcsDriver /** * {@inheritDoc} */ - public static function supports(IOInterface $io, $url, $deep = false) + public static function supports(IOInterface $io, Config $config, $url, $deep = false) { $url = self::normalizeUrl($url); if (preg_match('#(^svn://|^svn\+ssh://|svn\.)#i', $url)) { @@ -232,7 +260,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; } @@ -278,15 +306,16 @@ class SvnDriver extends VcsDriver * Execute an SVN command and try to fix up the process with credentials * if necessary. * - * @param string $command The svn command to run. - * @param string $url The SVN URL. - * + * @param string $command The svn command to run. + * @param string $url The SVN URL. + * @throws \RuntimeException * @return string */ 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 { @@ -301,4 +330,17 @@ class SvnDriver extends VcsDriver ); } } + + /** + * Build the identifier respecting "package-path" config option + * + * @param string $baseDir The path to trunk/branch/tag + * @param int $revision The revision mark to add to identifier + * + * @return string + */ + protected function buildIdentifier($baseDir, $revision) + { + return rtrim($baseDir, '/') . $this->packagePath . '/@' . $revision; + } } diff --git a/src/Composer/Repository/Vcs/VcsDriver.php b/src/Composer/Repository/Vcs/VcsDriver.php index 777a11ea0..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,13 +45,17 @@ abstract class VcsDriver implements VcsDriverInterface */ final public function __construct(array $repoConfig, IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null) { + if (Filesystem::isLocalPath($repoConfig['url'])) { + $repoConfig['url'] = Filesystem::getPlatformPath($repoConfig['url']); + } + $this->url = $repoConfig['url']; $this->originUrl = $repoConfig['url']; $this->repoConfig = $repoConfig; $this->io = $io; $this->config = $config; - $this->process = $process ?: new ProcessExecutor; - $this->remoteFilesystem = $remoteFilesystem ?: new RemoteFilesystem($io); + $this->process = $process ?: new ProcessExecutor($io); + $this->remoteFilesystem = $remoteFilesystem ?: new RemoteFilesystem($io, $config); } /** @@ -94,8 +99,11 @@ abstract class VcsDriver implements VcsDriverInterface return $this->remoteFilesystem->getContents($this->originUrl, $url, false); } - protected static function isLocalUrl($url) + /** + * {@inheritDoc} + */ + public function cleanup() { - return (bool) preg_match('{^(file://|/|[a-z]:[\\\\/])}i', $url); + return; } } diff --git a/src/Composer/Repository/Vcs/VcsDriverInterface.php b/src/Composer/Repository/Vcs/VcsDriverInterface.php index 44486f007..dd30baacd 100644 --- a/src/Composer/Repository/Vcs/VcsDriverInterface.php +++ b/src/Composer/Repository/Vcs/VcsDriverInterface.php @@ -12,6 +12,7 @@ namespace Composer\Repository\Vcs; +use Composer\Config; use Composer\IO\IOInterface; /** @@ -81,13 +82,20 @@ interface VcsDriverInterface */ public function hasComposerFile($identifier); + /** + * Performs any cleanup necessary as the driver is not longer needed + * + */ + public function cleanup(); + /** * Checks if this driver can handle a given url * - * @param IOInterface $io IO instance - * @param string $url - * @param bool $deep unless true, only shallow checks (url matching typically) should be done + * @param IOInterface $io IO instance + * @param Config $config current $config + * @param string $url URL to validate/check + * @param bool $deep unless true, only shallow checks (url matching typically) should be done * @return bool */ - public static function supports(IOInterface $io, $url, $deep = false); + public static function supports(IOInterface $io, Config $config, $url, $deep = false); } diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index e2a10a9c8..62e7acef5 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -19,6 +19,7 @@ use Composer\Package\Loader\ArrayLoader; use Composer\Package\Loader\ValidatingArrayLoader; use Composer\Package\Loader\InvalidPackageException; use Composer\Package\Loader\LoaderInterface; +use Composer\EventDispatcher\EventDispatcher; use Composer\IO\IOInterface; use Composer\Config; @@ -38,15 +39,17 @@ class VcsRepository extends ArrayRepository protected $repoConfig; protected $branchErrorOccurred = false; - public function __construct(array $repoConfig, IOInterface $io, Config $config, array $drivers = null) + public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $dispatcher = null, array $drivers = null) { $this->drivers = $drivers ?: array( 'github' => 'Composer\Repository\Vcs\GitHubDriver', 'git-bitbucket' => 'Composer\Repository\Vcs\GitBitbucketDriver', 'git' => 'Composer\Repository\Vcs\GitDriver', - 'svn' => 'Composer\Repository\Vcs\SvnDriver', 'hg-bitbucket' => 'Composer\Repository\Vcs\HgBitbucketDriver', 'hg' => 'Composer\Repository\Vcs\HgDriver', + 'perforce' => 'Composer\Repository\Vcs\PerforceDriver', + // svn must be last because identifying a subversion server for sure is practically impossible + 'svn' => 'Composer\Repository\Vcs\SvnDriver', ); $this->url = $repoConfig['url']; @@ -57,6 +60,11 @@ class VcsRepository extends ArrayRepository $this->repoConfig = $repoConfig; } + public function getRepoConfig() + { + return $this->repoConfig; + } + public function setLoader(LoaderInterface $loader) { $this->loader = $loader; @@ -73,7 +81,7 @@ class VcsRepository extends ArrayRepository } foreach ($this->drivers as $driver) { - if ($driver::supports($this->io, $this->url)) { + if ($driver::supports($this->io, $this->config, $this->url)) { $driver = new $driver($this->repoConfig, $this->io, $this->config); $driver->initialize(); @@ -82,7 +90,7 @@ class VcsRepository extends ArrayRepository } foreach ($this->drivers as $driver) { - if ($driver::supports($this->io, $this->url, true)) { + if ($driver::supports($this->io, $this->config, $this->url, true)) { $driver = new $driver($this->repoConfig, $this->io, $this->config); $driver->initialize(); @@ -246,6 +254,7 @@ class VcsRepository extends ArrayRepository continue; } } + $driver->cleanup(); if (!$verbose) { $this->io->overwrite('', false); diff --git a/src/Composer/Repository/WritableArrayRepository.php b/src/Composer/Repository/WritableArrayRepository.php new file mode 100644 index 000000000..756f24137 --- /dev/null +++ b/src/Composer/Repository/WritableArrayRepository.php @@ -0,0 +1,66 @@ + + * 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; + +/** + * Writable array repository. + * + * @author Jordi Boggiano + */ +class WritableArrayRepository extends ArrayRepository implements WritableRepositoryInterface +{ + /** + * {@inheritDoc} + */ + public function write() + { + } + + /** + * {@inheritDoc} + */ + public function reload() + { + } + + /** + * {@inheritDoc} + */ + public function getCanonicalPackages() + { + $packages = $this->getPackages(); + + // get at most one package of each name, prefering non-aliased ones + $packagesByName = array(); + foreach ($packages as $package) { + if (!isset($packagesByName[$package->getName()]) || $packagesByName[$package->getName()] instanceof AliasPackage) { + $packagesByName[$package->getName()] = $package; + } + } + + $canonicalPackages = array(); + + // unfold aliased packages + foreach ($packagesByName as $package) { + while ($package instanceof AliasPackage) { + $package = $package->getAliasOf(); + } + + $canonicalPackages[] = $package; + } + + return $canonicalPackages; + } +} diff --git a/src/Composer/Repository/WritableRepositoryInterface.php b/src/Composer/Repository/WritableRepositoryInterface.php index ade447286..c046c377c 100644 --- a/src/Composer/Repository/WritableRepositoryInterface.php +++ b/src/Composer/Repository/WritableRepositoryInterface.php @@ -40,6 +40,13 @@ interface WritableRepositoryInterface extends RepositoryInterface */ public function removePackage(PackageInterface $package); + /** + * Get unique packages, with aliases resolved and removed + * + * @return PackageInterface[] + */ + public function getCanonicalPackages(); + /** * Forces a reload of all packages */ diff --git a/src/Composer/Script/CommandEvent.php b/src/Composer/Script/CommandEvent.php index 5d8f732c9..48ea2246a 100644 --- a/src/Composer/Script/CommandEvent.php +++ b/src/Composer/Script/CommandEvent.php @@ -12,8 +12,6 @@ namespace Composer\Script; -use Composer\Composer; - /** * The Command Event. * diff --git a/src/Composer/Script/Event.php b/src/Composer/Script/Event.php index d9be6a944..bde7e3b6f 100644 --- a/src/Composer/Script/Event.php +++ b/src/Composer/Script/Event.php @@ -14,19 +14,16 @@ namespace Composer\Script; use Composer\Composer; use Composer\IO\IOInterface; +use Composer\EventDispatcher\Event as BaseEvent; /** - * The base event class + * The script event class * * @author François Pluchino + * @author Nils Adermann */ -class Event +class Event extends BaseEvent { - /** - * @var string This event's name - */ - private $name; - /** * @var Composer The composer instance */ @@ -49,25 +46,17 @@ class 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) + public function __construct($name, Composer $composer, IOInterface $io, $devMode = false, array $args = array(), array $flags = array()) { - $this->name = $name; + parent::__construct($name, $args, $flags); $this->composer = $composer; $this->io = $io; $this->devMode = $devMode; } - /** - * Returns the event's name. - * - * @return string The event name - */ - public function getName() - { - return $this->name; - } - /** * Returns the composer instance. * diff --git a/src/Composer/Script/EventDispatcher.php b/src/Composer/Script/EventDispatcher.php deleted file mode 100644 index 7e923ab1e..000000000 --- a/src/Composer/Script/EventDispatcher.php +++ /dev/null @@ -1,165 +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\Script; - -use Composer\Autoload\AutoloadGenerator; -use Composer\IO\IOInterface; -use Composer\Composer; -use Composer\DependencyResolver\Operation\OperationInterface; -use Composer\Util\ProcessExecutor; - -/** - * The Event Dispatcher. - * - * Example in command: - * $dispatcher = new EventDispatcher($this->getComposer(), $this->getApplication()->getIO()); - * // ... - * $dispatcher->dispatch(ScriptEvents::POST_INSTALL_CMD); - * // ... - * - * @author François Pluchino - * @author Jordi Boggiano - */ -class EventDispatcher -{ - protected $composer; - protected $io; - protected $loader; - protected $process; - - /** - * Constructor. - * - * @param Composer $composer The composer instance - * @param IOInterface $io The IOInterface instance - * @param ProcessExecutor $process - */ - public function __construct(Composer $composer, IOInterface $io, ProcessExecutor $process = null) - { - $this->composer = $composer; - $this->io = $io; - $this->process = $process ?: new ProcessExecutor(); - } - - /** - * 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 - */ - public function dispatchPackageEvent($eventName, $devMode, OperationInterface $operation) - { - $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 - */ - public function dispatchCommandEvent($eventName, $devMode) - { - $this->doDispatch(new CommandEvent($eventName, $this->composer, $this->io, $devMode)); - } - - /** - * Triggers the listeners of an event. - * - * @param Event $event The event object to pass to the event handlers/listeners. - */ - protected function doDispatch(Event $event) - { - $listeners = $this->getListeners($event); - - foreach ($listeners as $callable) { - if ($this->isPhpScript($callable)) { - $className = substr($callable, 0, strpos($callable, '::')); - $methodName = substr($callable, strpos($callable, '::') + 2); - - if (!class_exists($className)) { - $this->io->write('Class '.$className.' is not autoloadable, can not call '.$event->getName().' script'); - continue; - } - if (!is_callable($callable)) { - $this->io->write('Method '.$callable.' is not callable, can not call '.$event->getName().' script'); - continue; - } - - try { - $this->executeEventPhpScript($className, $methodName, $event); - } 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 !== $this->process->execute($callable)) { - $event->getIO()->write(sprintf('Script %s handling the %s event returned with an error: %s', $callable, $event->getName(), $this->process->getErrorOutput())); - } - } - } - } - - /** - * @param string $className - * @param string $methodName - * @param Event $event Event invoking the PHP callable - */ - protected function executeEventPhpScript($className, $methodName, Event $event) - { - $className::$methodName($event); - } - - /** - * @param Event $event Event object - * @return array Listeners - */ - protected function getListeners(Event $event) - { - $package = $this->composer->getPackage(); - $scripts = $package->getScripts(); - - if (empty($scripts[$event->getName()])) { - return array(); - } - - if ($this->loader) { - $this->loader->unregister(); - } - - $generator = new AutoloadGenerator; - $packages = array_merge( - $this->composer->getRepositoryManager()->getLocalRepository()->getPackages(), - $this->composer->getRepositoryManager()->getLocalDevRepository()->getPackages() - ); - $packageMap = $generator->buildPackageMap($this->composer->getInstallationManager(), $package, $packages); - $map = $generator->parseAutoloads($packageMap, $package); - $this->loader = $generator->createLoader($map); - $this->loader->register(); - - return $scripts[$event->getName()]; - } - - /** - * Checks if string given references a class path and method - * - * @param string $callable - * @return boolean - */ - protected function isPhpScript($callable) - { - return false === strpos($callable, ' ') && false !== strpos($callable, '::'); - } -} diff --git a/src/Composer/Script/PackageEvent.php b/src/Composer/Script/PackageEvent.php index 469eb4120..735de0021 100644 --- a/src/Composer/Script/PackageEvent.php +++ b/src/Composer/Script/PackageEvent.php @@ -32,7 +32,7 @@ class PackageEvent extends Event * Constructor. * * @param string $name The event name - * @param Composer $composer The composer objet + * @param Composer $composer The composer object * @param IOInterface $io The IOInterface object * @param boolean $devMode Whether or not we are in dev mode * @param OperationInterface $operation The operation object diff --git a/src/Composer/Script/ScriptEvents.php b/src/Composer/Script/ScriptEvents.php index 9f5131345..616b2b97e 100644 --- a/src/Composer/Script/ScriptEvents.php +++ b/src/Composer/Script/ScriptEvents.php @@ -56,6 +56,24 @@ class ScriptEvents */ const POST_UPDATE_CMD = 'post-update-cmd'; + /** + * The PRE_STATUS_CMD event occurs before the status command is executed. + * + * The event listener method receives a Composer\Script\CommandEvent instance. + * + * @var string + */ + const PRE_STATUS_CMD = 'pre-status-cmd'; + + /** + * The POST_STATUS_CMD event occurs after the status command is executed. + * + * The event listener method receives a Composer\Script\CommandEvent instance. + * + * @var string + */ + const POST_STATUS_CMD = 'post-status-cmd'; + /** * The PRE_PACKAGE_INSTALL event occurs before a package is installed. * @@ -109,4 +127,59 @@ class ScriptEvents * @var string */ const POST_PACKAGE_UNINSTALL = 'post-package-uninstall'; + + /** + * The PRE_AUTOLOAD_DUMP event occurs before the autoload file is generated. + * + * The event listener method receives a Composer\Script\Event instance. + * + * @var string + */ + const PRE_AUTOLOAD_DUMP = 'pre-autoload-dump'; + + /** + * The POST_AUTOLOAD_DUMP event occurs after the autoload file has been generated. + * + * The event listener method receives a Composer\Script\Event instance. + * + * @var string + */ + const POST_AUTOLOAD_DUMP = 'post-autoload-dump'; + + /** + * The POST_ROOT_PACKAGE_INSTALL event occurs after the root package has been installed. + * + * The event listener method receives a Composer\Script\PackageEvent instance. + * + * @var string + */ + const POST_ROOT_PACKAGE_INSTALL = 'post-root-package-install'; + + /** + * The POST_CREATE_PROJECT event occurs after the create-project command has been executed. + * Note: Event occurs after POST_INSTALL_CMD + * + * The event listener method receives a Composer\Script\PackageEvent instance. + * + * @var string + */ + 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 0d8cb3762..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(); @@ -49,7 +50,6 @@ class ConfigValidator // validate json schema $laxValid = false; - $valid = false; try { $json = new JsonFile($file, new RemoteFilesystem($this->io)); $manifest = $json->read(); @@ -57,7 +57,6 @@ class ConfigValidator $json->validateSchema(JsonFile::LAX_SCHEMA); $laxValid = true; $json->validateSchema(); - $valid = true; } catch (JsonValidationException $e) { foreach ($e->getErrors() as $message) { if ($laxValid) { @@ -74,15 +73,29 @@ class ConfigValidator // validate actual data if (!empty($manifest['license'])) { + // strip proprietary since it's not a valid SPDX identifier, but is accepted by composer + if (is_array($manifest['license'])) { + foreach ($manifest['license'] as $key => $license) { + if ('proprietary' === $license) { + unset($manifest['license'][$key]); + } + } + } + $licenseValidator = new SpdxLicenseIdentifier(); - if (!$licenseValidator->validate($manifest['license'])) { + if ('proprietary' !== $manifest['license'] && array() !== $manifest['license'] && !$licenseValidator->validate($manifest['license'])) { $warnings[] = sprintf( - 'License %s is not a valid SPDX license identifier, see http://www.spdx.org/licenses/ if you use an open license', + 'License %s is not a valid SPDX license identifier, see http://www.spdx.org/licenses/ if you use an open license.' + ."\nIf the software is closed-source, you may use \"proprietary\" as license.", json_encode($manifest['license']) ); } } else { - $warnings[] = 'No license specified, it is recommended to do so'; + $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'])) { @@ -96,8 +109,22 @@ class ConfigValidator ); } + if (!empty($manifest['type']) && $manifest['type'] == 'composer-installer') { + $warnings[] = "The package type 'composer-installer' is deprecated. Please distribute your custom installers as plugins from now on. See http://getcomposer.org/doc/articles/plugins.md for plugin documentation."; + } + + // check for require-dev overrides + if (isset($manifest['require']) && isset($manifest['require-dev'])) { + $requireOverrides = array_intersect_key($manifest['require'], $manifest['require-dev']); + + if (!empty($requireOverrides)) { + $plural = (count($requireOverrides) > 1) ? 'are' : 'is'; + $warnings[] = implode(', ', array_keys($requireOverrides)). " {$plural} required both in require and require-dev, this can lead to unexpected behavior"; + } + } + 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 7a8982131..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,43 +36,97 @@ class Filesystem } if (file_exists($file)) { - return unlink($file); + return $this->unlink($file); } return false; } + /** + * Checks if a directory is empty + * + * @param string $dir + * @return bool + */ + public function isDirEmpty($dir) + { + $finder = Finder::create() + ->ignoreVCS(false) + ->ignoreDotFiles(false) + ->depth(0) + ->in($dir); + + 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); + } + } + } + /** * Recursively remove a directory * * Uses the process component if proc_open is enabled on the PHP * installation. * - * @param string $directory + * @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; } + if (preg_match('{^(?:[a-z]:)?[/\\\\]+$}i', $directory)) { + throw new \RuntimeException('Aborting an attempted deletion of '.$directory.', this was probably not intended, if it is a real use case please report it.'); + } + if (!function_exists('proc_open')) { return $this->removeDirectoryPhp($directory); } 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) === 0; + $result = $this->getProcess()->execute($cmd, $output) === 0; // 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); } /** @@ -81,7 +136,7 @@ class Filesystem * before directories, creating a single non-recursive loop * to delete files/directories in the correct order. * - * @param string $directory + * @param string $directory * @return bool */ public function removeDirectoryPhp($directory) @@ -91,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) @@ -108,7 +163,7 @@ class Filesystem $directory.' exists and is not a directory.' ); } - if (!mkdir($directory, 0777, true)) { + if (!@mkdir($directory, 0777, true)) { throw new \RuntimeException( $directory.' does not exist and could not be created.' ); @@ -116,10 +171,62 @@ 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}. * - * Some systems can't rename and also dont have proc_open, + * Some systems can't rename and also don't have proc_open, * which requires this solution. * * @param string $source @@ -127,17 +234,21 @@ class Filesystem */ public function copyThenRemove($source, $target) { - $it = new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS); - $ri = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::SELF_FIRST); + if (!is_dir($source)) { + copy($source, $target); + $this->unlink($source); - if ( !file_exists($target)) { - mkdir($target, 0777, true); + return; } + $it = new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS); + $ri = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::SELF_FIRST); + $this->ensureDirectoryExists($target); + foreach ($ri as $file) { $targetPath = $target . DIRECTORY_SEPARATOR . $ri->getSubPathName(); if ($file->isDir()) { - mkdir($targetPath); + $this->ensureDirectoryExists($targetPath); } else { copy($file->getPathname(), $targetPath); } @@ -158,8 +269,13 @@ 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)); - if (0 === $this->processExecutor->execute($command)) { + $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 + clearstatcache(); + + if (0 === $result) { $this->remove($source); return; @@ -167,21 +283,27 @@ 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)); - if (0 === $this->processExecutor->execute($command)) { + $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 + clearstatcache(); + + if (0 === $result) { return; } } - throw new \RuntimeException(sprintf('Could not rename "%s" to "%s".', $source, $target)); + return $this->copyThenRemove($source, $target); } /** * Returns the shortest path from $from to $to * - * @param string $from - * @param string $to - * @param bool $directories if true, the source/target are considered to be directories + * @param string $from + * @param string $to + * @param bool $directories if true, the source/target are considered to be directories + * @throws \InvalidArgumentException * @return string */ public function findShortestPath($from, $to, $directories = false) @@ -190,8 +312,8 @@ class Filesystem throw new \InvalidArgumentException(sprintf('$from (%s) and $to (%s) must be absolute paths.', $from, $to)); } - $from = lcfirst(rtrim(strtr($from, '\\', '/'), '/')); - $to = lcfirst(rtrim(strtr($to, '\\', '/'), '/')); + $from = lcfirst($this->normalizePath($from)); + $to = lcfirst($this->normalizePath($to)); if ($directories) { $from .= '/dummy_file'; @@ -202,11 +324,11 @@ class Filesystem } $commonPath = $to; - while (strpos($from, $commonPath) !== 0 && '/' !== $commonPath && !preg_match('{^[a-z]:/?$}i', $commonPath) && '.' !== $commonPath) { + while (strpos($from.'/', $commonPath.'/') !== 0 && '/' !== $commonPath && !preg_match('{^[a-z]:/?$}i', $commonPath)) { $commonPath = strtr(dirname($commonPath), '\\', '/'); } - if (0 !== strpos($from, $commonPath) || '/' === $commonPath || '.' === $commonPath) { + if (0 !== strpos($from, $commonPath) || '/' === $commonPath) { return $to; } @@ -220,9 +342,10 @@ class Filesystem /** * Returns PHP code that, when executed in $from, will return the path to $to * - * @param string $from - * @param string $to - * @param bool $directories if true, the source/target are considered to be directories + * @param string $from + * @param string $to + * @param bool $directories if true, the source/target are considered to be directories + * @throws \InvalidArgumentException * @return string */ public function findShortestPathCode($from, $to, $directories = false) @@ -231,15 +354,15 @@ class Filesystem throw new \InvalidArgumentException(sprintf('$from (%s) and $to (%s) must be absolute paths.', $from, $to)); } - $from = lcfirst(strtr($from, '\\', '/')); - $to = lcfirst(strtr($to, '\\', '/')); + $from = lcfirst($this->normalizePath($from)); + $to = lcfirst($this->normalizePath($to)); if ($from === $to) { return $directories ? '__DIR__' : '__FILE__'; } $commonPath = $to; - while (strpos($from, $commonPath) !== 0 && '/' !== $commonPath && !preg_match('{^[a-z]:/?$}i', $commonPath) && '.' !== $commonPath) { + while (strpos($from.'/', $commonPath.'/') !== 0 && '/' !== $commonPath && !preg_match('{^[a-z]:/?$}i', $commonPath) && '.' !== $commonPath) { $commonPath = strtr(dirname($commonPath), '\\', '/'); } @@ -269,8 +392,164 @@ class Filesystem return substr($path, 0, 1) === '/' || substr($path, 1, 1) === ':'; } + /** + * Returns size of a file or directory specified by path. If a directory is + * given, it's size will be computed recursively. + * + * @param string $path Path to the file or directory + * @throws \RuntimeException + * @return int + */ + public function size($path) + { + if (!file_exists($path)) { + throw new \RuntimeException("$path does not exist."); + } + if (is_dir($path)) { + return $this->directorySize($path); + } + + return filesize($path); + } + + /** + * Normalize a path. This replaces backslashes with slashes, removes ending + * slash and collapses redundant separators and up-level references. + * + * @param string $path Path to the file or directory + * @return string + */ + public function normalizePath($path) + { + $parts = array(); + $path = strtr($path, '\\', '/'); + $prefix = ''; + $absolute = false; + + if (preg_match('{^([0-9a-z]+:(?://(?:[a-z]:)?)?)}i', $path, $match)) { + $prefix = $match[1]; + $path = substr($path, strlen($prefix)); + } + + if (substr($path, 0, 1) === '/') { + $absolute = true; + $path = substr($path, 1); + } + + $up = false; + foreach (explode('/', $path) as $chunk) { + if ('..' === $chunk && ($absolute || $up)) { + array_pop($parts); + $up = !(empty($parts) || '..' === end($parts)); + } elseif ('.' !== $chunk && '' !== $chunk) { + $parts[] = $chunk; + $up = '..' !== $chunk; + } + } + + 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); + $ri = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST); + + $size = 0; + foreach ($ri as $file) { + if ($file->isFile()) { + $size += $file->getSize(); + } + } + + return $size; + } + protected function getProcess() { 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 new file mode 100644 index 000000000..4a0c3933f --- /dev/null +++ b/src/Composer/Util/Git.php @@ -0,0 +1,192 @@ + + * 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 Git +{ + 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'); + } + + // added in git 1.7.1, prevents prompting the user for username/password + if (getenv('GIT_ASKPASS') !== 'echo') { + putenv('GIT_ASKPASS=echo'); + } + + // clean up rogue git env vars in case this is running in a git hook + if (getenv('GIT_DIR')) { + putenv('GIT_DIR'); + } + if (getenv('GIT_WORK_TREE')) { + putenv('GIT_WORK_TREE'); + } + } + + 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 573ecf516..784cb51c9 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); } /** @@ -51,7 +51,7 @@ class GitHub */ public function authorizeOAuth($originUrl) { - if ('github.com' !== $originUrl) { + if (!in_array($originUrl, $this->config->get('github-domains'))) { return false; } @@ -68,24 +68,32 @@ class GitHub /** * Authorizes a GitHub domain interactively via OAuth * - * @param string $originUrl The host this GitHub instance is located at - * @param string $message The reason this authorization is required - * @return bool true on success + * @param string $originUrl The host this GitHub instance is located at + * @param string $message The reason this authorization is required + * @throws \RuntimeException + * @throws TransportException|\Exception + * @return bool true on success */ 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('The credentials will be swapped for an OAuth token stored in '.$this->config->getAuthConfigSource()->getName().', your password will not be stored'); $this->io->write('To revoke access to this token you can visit https://github.com/settings/applications'); while ($attemptCounter++ < 5) { try { - $username = $this->io->ask('Username: '); - $password = $this->io->askAndHideAnswer('Password: '); - $this->io->setAuthentication($originUrl, $username, $password); + if (empty($otp) || !$this->io->hasAuthentication($originUrl)) { + $username = $this->io->ask('Username: '); + $password = $this->io->askAndHideAnswer('Password: '); + $otp = null; + + $this->io->setAuthentication($originUrl, $username, $password); + } // build up OAuth app name $appName = 'Composer'; @@ -93,20 +101,81 @@ class GitHub $appName .= ' on ' . trim($output); } - $contents = JsonFile::parseJson($this->remoteFilesystem->getContents($originUrl, 'https://api.github.com/authorizations', false, array( + $headers = array(); + if ($otp) { + $headers = array('X-GitHub-OTP: ' . $otp); + } + + // try retrieving an existing token with the same name + $contents = null; + $auths = JsonFile::parseJson($this->remoteFilesystem->getContents($originUrl, 'https://'. $apiUrl . '/authorizations', false, array( + 'retry-auth-failure' => false, '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/', - )), + 'header' => $headers ) ))); + foreach ($auths as $auth) { + if ( + isset($auth['app']['name']) + && 0 === strpos($auth['app']['name'], $appName) + && $auth['app']['url'] === 'https://getcomposer.org/' + ) { + $this->io->write('An existing OAuth token for Composer is present and will be reused'); + + $contents['token'] = $auth['token']; + break; + } + } + + // no existing token, create one + if (empty($contents['token'])) { + $headers[] = 'Content-Type: application/json'; + + $contents = JsonFile::parseJson($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' => $appName, + 'note_url' => 'https://getcomposer.org/', + )), + ) + ))); + $this->io->write('Token successfully created'); + } } catch (TransportException $e) { if (in_array($e->getCode(), array(403, 401))) { + // 401 when authentication was supplied, handle 2FA if required. + if ($this->io->hasAuthentication($originUrl)) { + $headerNames = array_map(function ($header) { + return strtolower(strstr($header, ':', true)); + }, $e->getHeaders()); + + if ($key = array_search('x-github-otp', $headerNames)) { + $headers = $e->getHeaders(); + 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.'); + } + + $otp = $this->io->ask('Authentication Code: '); + + continue; + } + } + } + $this->io->write('Invalid credentials.'); continue; } @@ -117,9 +186,8 @@ class GitHub $this->io->setAuthentication($originUrl, $contents['token'], 'x-oauth-basic'); // 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->getConfigSource()->removeConfigSetting('github-oauth.'.$originUrl); + $this->config->getAuthConfigSource()->addConfigSetting('github-oauth.'.$originUrl, $contents['token']); return true; } diff --git a/src/Composer/Util/NoProxyPattern.php b/src/Composer/Util/NoProxyPattern.php new file mode 100644 index 000000000..533dbc19c --- /dev/null +++ b/src/Composer/Util/NoProxyPattern.php @@ -0,0 +1,147 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +/** + * Tests URLs against no_proxy patterns. + */ +class NoProxyPattern +{ + /** + * @var string[] + */ + protected $rules = array(); + + /** + * @param string $pattern no_proxy pattern + */ + public function __construct($pattern) + { + $this->rules = preg_split("/[\s,]+/", $pattern); + } + + /** + * Test a URL against the stored pattern. + * + * @param string $url + * + * @return true if the URL matches one of the rules. + */ + public function test($url) + { + $host = parse_url($url, PHP_URL_HOST); + $port = parse_url($url, PHP_URL_PORT); + + if (empty($port)) { + switch (parse_url($url, PHP_URL_SCHEME)) { + case 'http': + $port = 80; + break; + case 'https': + $port = 443; + break; + } + } + + foreach ($this->rules as $rule) { + if ($rule == '*') { + return true; + } + + $match = false; + + list($ruleHost) = explode(':', $rule); + list($base) = explode('/', $ruleHost); + + if (filter_var($base, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + // ip or cidr match + + if (!isset($ip)) { + $ip = gethostbyname($host); + } + + if (strpos($ruleHost, '/') === false) { + $match = $ip === $ruleHost; + } else { + // gethostbyname() failed to resolve $host to an ip, so we assume + // it must be proxied to let the proxy's DNS resolve it + if ($ip === $host) { + $match = false; + } else { + // match resolved IP against the rule + $match = self::inCIDRBlock($ruleHost, $ip); + } + } + } else { + // match end of domain + + $haystack = '.' . trim($host, '.') . '.'; + $needle = '.'. trim($ruleHost, '.') .'.'; + $match = stripos(strrev($haystack), strrev($needle)) === 0; + } + + // final port check + if ($match && strpos($rule, ':') !== false) { + list(, $rulePort) = explode(':', $rule); + if (!empty($rulePort) && $port != $rulePort) { + $match = false; + } + } + + if ($match) { + return true; + } + } + + return false; + } + + /** + * Check an IP address against a CIDR + * + * http://framework.zend.com/svn/framework/extras/incubator/library/ZendX/Whois/Adapter/Cidr.php + * + * @param string $cidr IPv4 block in CIDR notation + * @param string $ip IPv4 address + * + * @return boolean + */ + private static function inCIDRBlock($cidr, $ip) + { + // Get the base and the bits from the CIDR + list($base, $bits) = explode('/', $cidr); + + // Now split it up into it's classes + list($a, $b, $c, $d) = explode('.', $base); + + // 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)); + + // Here's our lowest int + $low = $i & $mask; + + // Here's our highest int + $high = $i | (~$mask & 0xFFFFFFFF); + + // Now split the ip we're checking against up into classes + list($a, $b, $c, $d) = explode('.', $ip); + + // Now convert the ip we're checking against to an int + $check = ($a << 24) + ($b << 16) + ($c << 8) + $d; + + // If the ip is within the range, including highest/lowest values, + // then it's within the CIDR range + return $check >= $low && $check <= $high; + } +} diff --git a/src/Composer/Util/Perforce.php b/src/Composer/Util/Perforce.php new file mode 100644 index 000000000..d3dcd696f --- /dev/null +++ b/src/Composer/Util/Perforce.php @@ -0,0 +1,574 @@ + + * 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\IO\IOInterface; +use Symfony\Component\Process\Process; + +/** + * @author Matt Whittom + */ +class Perforce +{ + protected $path; + protected $p4Depot; + protected $p4Client; + protected $p4User; + protected $p4Password; + protected $p4Port; + protected $p4Stream; + protected $p4ClientSpec; + protected $p4DepotType; + protected $p4Branch; + protected $process; + protected $uniquePerforceClientName; + protected $windowsFlag; + protected $commandResult; + + 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, IOInterface $io) + { + $isWindows = defined('PHP_WINDOWS_VERSION_BUILD'); + $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(); + if (null == $repoConfig) { + return; + } + if (isset($repoConfig['unique_perforce_client_name'])) { + $this->uniquePerforceClientName = $repoConfig['unique_perforce_client_name']; + } + + if (isset($repoConfig['depot'])) { + $this->p4Depot = $repoConfig['depot']; + } + if (isset($repoConfig['branch'])) { + $this->p4Branch = $repoConfig['branch']; + } + if (isset($repoConfig['p4user'])) { + $this->p4User = $repoConfig['p4user']; + } else { + $this->p4User = $this->getP4variable('P4USER'); + } + if (isset($repoConfig['p4password'])) { + $this->p4Password = $repoConfig['p4password']; + } + } + + public function initializeDepotAndBranch($depot, $branch) + { + if (isset($depot)) { + $this->p4Depot = $depot; + } + if (isset($branch)) { + $this->p4Branch = $branch; + } + } + + public function generateUniquePerforceClientName() + { + return gethostname() . "_" . time(); + } + + public function cleanupClientSpec() + { + $client = $this->getClient(); + $task = 'client -d ' . $client; + $useP4Client = false; + $command = $this->generateP4Command($task, $useP4Client); + $this->executeCommand($command); + $clientSpec = $this->getP4ClientSpec(); + $fileSystem = $this->getFilesystem(); + $fileSystem->remove($clientSpec); + } + + protected function executeCommand($command) + { + $this->commandResult = ""; + $exit_code = $this->process->execute($command, $this->commandResult); + + return $exit_code; + } + + public function getClient() + { + if (!isset($this->p4Client)) { + $cleanStreamName = str_replace('@', '', str_replace('/', '_', str_replace('//', '', $this->getStream()))); + $this->p4Client = 'composer_perforce_' . $this->uniquePerforceClientName . '_' . $cleanStreamName; + } + + return $this->p4Client; + } + + protected function getPath() + { + return $this->path; + } + + public function initializePath($path) + { + $this->path = $path; + $fs = $this->getFilesystem(); + $fs->ensureDirectoryExists($path); + } + + protected function getPort() + { + return $this->p4Port; + } + + public function setStream($stream) + { + $this->p4Stream = $stream; + $index = strrpos($stream, '/'); + //Stream format is //depot/stream, while non-streaming depot is //depot + if ($index > 2) { + $this->p4DepotType = 'stream'; + } + } + + public function isStream() + { + return (strcmp($this->p4DepotType, 'stream') === 0); + } + + public function getStream() + { + if (!isset($this->p4Stream)) { + if ($this->isStream()) { + $this->p4Stream = '//' . $this->p4Depot . '/' . $this->p4Branch; + } else { + $this->p4Stream = '//' . $this->p4Depot; + } + } + + return $this->p4Stream; + } + + public function getStreamWithoutLabel($stream) + { + $index = strpos($stream, '@'); + if ($index === false) { + return $stream; + } + + return substr($stream, 0, $index); + } + + public function getP4ClientSpec() + { + $p4clientSpec = $this->path . '/' . $this->getClient() . '.p4.spec'; + + return $p4clientSpec; + } + + public function getUser() + { + return $this->p4User; + } + + public function setUser($user) + { + $this->p4User = $user; + } + + public function queryP4User() + { + $this->getUser(); + if (strlen($this->p4User) > 0) { + return; + } + $this->p4User = $this->getP4variable('P4USER'); + if (strlen($this->p4User) > 0) { + return; + } + $this->p4User = $this->io->ask('Enter P4 User:'); + if ($this->windowsFlag) { + $command = 'p4 set P4USER=' . $this->p4User; + } else { + $command = 'export P4USER=' . $this->p4User; + } + $this->executeCommand($command); + } + + protected function getP4variable($name) + { + if ($this->windowsFlag) { + $command = 'p4 set'; + $this->executeCommand($command); + $result = trim($this->commandResult); + $resArray = explode(PHP_EOL, $result); + foreach ($resArray as $line) { + $fields = explode('=', $line); + if (strcmp($name, $fields[0]) == 0) { + $index = strpos($fields[1], ' '); + if ($index === false) { + $value = $fields[1]; + } else { + $value = substr($fields[1], 0, $index); + } + $value = trim($value); + + return $value; + } + } + } else { + $command = 'echo $' . $name; + $this->executeCommand($command); + $result = trim($this->commandResult); + + return $result; + } + } + + public function queryP4Password() + { + if (isset($this->p4Password)) { + return $this->p4Password; + } + $password = $this->getP4variable('P4PASSWD'); + if (strlen($password) <= 0) { + $password = $this->io->askAndHideAnswer('Enter password for Perforce user ' . $this->getUser() . ': '); + } + $this->p4Password = $password; + + return $password; + } + + public function generateP4Command($command, $useClient = true) + { + $p4Command = 'p4 '; + $p4Command = $p4Command . '-u ' . $this->getUser() . ' '; + if ($useClient) { + $p4Command = $p4Command . '-c ' . $this->getClient() . ' '; + } + $p4Command = $p4Command . '-p ' . $this->getPort() . ' '; + $p4Command = $p4Command . $command; + + return $p4Command; + } + + public function isLoggedIn() + { + $command = $this->generateP4Command('login -s', false); + $exitCode = $this->executeCommand($command); + if ($exitCode) { + $errorOutput = $this->process->getErrorOutput(); + $index = strpos($errorOutput, $this->getUser()); + if ($index === false) { + $index = strpos($errorOutput, 'p4'); + 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; + } + + public function connectClient() + { + $p4CreateClientCommand = $this->generateP4Command('client -i < ' . str_replace( " ", "\\ ", $this->getP4ClientSpec() )); + $this->executeCommand($p4CreateClientCommand); + } + + public function syncCodeBase($sourceReference) + { + $prevDir = getcwd(); + chdir($this->path); + $p4SyncCommand = $this->generateP4Command('sync -f '); + if (null != $sourceReference) { + $p4SyncCommand = $p4SyncCommand . '@' . $sourceReference; + } + $this->executeCommand($p4SyncCommand); + chdir($prevDir); + } + + public function writeClientSpecToFile($spec) + { + fwrite($spec, 'Client: ' . $this->getClient() . PHP_EOL . PHP_EOL); + fwrite($spec, 'Update: ' . date('Y/m/d H:i:s') . PHP_EOL . PHP_EOL); + fwrite($spec, 'Access: ' . date('Y/m/d H:i:s') . PHP_EOL); + fwrite($spec, 'Owner: ' . $this->getUser() . PHP_EOL . PHP_EOL); + fwrite($spec, 'Description:' . PHP_EOL); + fwrite($spec, ' Created by ' . $this->getUser() . ' from composer.' . PHP_EOL . PHP_EOL); + fwrite($spec, 'Root: ' . $this->getPath() . PHP_EOL . PHP_EOL); + fwrite($spec, 'Options: noallwrite noclobber nocompress unlocked modtime rmdir' . PHP_EOL . PHP_EOL); + fwrite($spec, 'SubmitOptions: revertunchanged' . PHP_EOL . PHP_EOL); + fwrite($spec, 'LineEnd: local' . PHP_EOL . PHP_EOL); + if ($this->isStream()) { + fwrite($spec, 'Stream:' . PHP_EOL); + fwrite($spec, ' ' . $this->getStreamWithoutLabel($this->p4Stream) . PHP_EOL); + } else { + fwrite( + $spec, + 'View: ' . $this->getStream() . '/... //' . $this->getClient() . '/... ' . PHP_EOL + ); + } + } + + public function writeP4ClientSpec() + { + $clientSpec = $this->getP4ClientSpec(); + $spec = fopen($clientSpec, 'w'); + try { + $this->writeClientSpecToFile($spec); + } catch (\Exception $e) { + fclose($spec); + throw $e; + } + fclose($spec); + } + + protected function read($pipe, $name) + { + if (feof($pipe)) { + return; + } + $line = fgets($pipe); + while ($line != false) { + $line = fgets($pipe); + } + + return; + } + + public function windowsLogin($password) + { + $command = $this->generateP4Command(' login -a'); + $process = new Process($command, null, null, $password); + + return $process->run(); + } + + public function p4Login() + { + $this->queryP4User(); + if (!$this->isLoggedIn()) { + $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) { + throw new \Exception("Error logging in:" . $this->process->getErrorOutput()); + } + } + } + } + + public function getComposerInformation($identifier) + { + $index = strpos($identifier, '@'); + if ($index === false) { + $composerJson = $identifier. '/composer.json'; + + return $this->getComposerInformationFromPath($composerJson); + } + + return $this->getComposerInformationFromLabel($identifier, $index); + } + + public function getComposerInformationFromPath($composerJson) + { + $command = $this->generateP4Command(' print ' . $composerJson); + $this->executeCommand($command); + $result = $this->commandResult; + $index = strpos($result, '{'); + if ($index === false) { + return ''; + } + if ($index >= 0) { + $rawData = substr($result, $index); + $composer_info = json_decode($rawData, true); + + return $composer_info; + } + + return ''; + } + + public function getComposerInformationFromLabel($identifier, $index) + { + $composerJsonPath = substr($identifier, 0, $index) . '/composer.json' . substr($identifier, $index); + $command = $this->generateP4Command(' files ' . $composerJsonPath, false); + $this->executeCommand($command); + $result = $this->commandResult; + $index2 = strpos($result, 'no such file(s).'); + if ($index2 === false) { + $index3 = strpos($result, 'change'); + if (!($index3 === false)) { + $phrase = trim(substr($result, $index3)); + $fields = explode(' ', $phrase); + $id = $fields[1]; + $composerJson = substr($identifier, 0, $index) . '/composer.json@' . $id; + + return $this->getComposerInformationFromPath($composerJson); + } + } + + return ""; + } + + public function getBranches() + { + $possibleBranches = array(); + if (!$this->isStream()) { + $possibleBranches[$this->p4Branch] = $this->getStream(); + } else { + $command = $this->generateP4Command('streams //' . $this->p4Depot . '/...'); + $this->executeCommand($command); + $result = $this->commandResult; + $resArray = explode(PHP_EOL, $result); + foreach ($resArray as $line) { + $resBits = explode(' ', $line); + if (count($resBits) > 4) { + $branch = preg_replace('/[^A-Za-z0-9 ]/', '', $resBits[4]); + $possibleBranches[$branch] = $resBits[1]; + } + } + } + $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; + } + + public function getTags() + { + $command = $this->generateP4Command('labels'); + $this->executeCommand($command); + $result = $this->commandResult; + $resArray = explode(PHP_EOL, $result); + $tags = array(); + foreach ($resArray as $line) { + $index = strpos($line, 'Label'); + if (!($index === false)) { + $fields = explode(' ', $line); + $tags[$fields[1]] = $this->getStream() . '@' . $fields[1]; + } + } + + return $tags; + } + + public function checkStream() + { + $command = $this->generateP4Command('depots', false); + $this->executeCommand($command); + $result = $this->commandResult; + $resArray = explode(PHP_EOL, $result); + foreach ($resArray as $line) { + $index = strpos($line, 'Depot'); + if (!($index === false)) { + $fields = explode(' ', $line); + if (strcmp($this->p4Depot, $fields[1]) === 0) { + $this->p4DepotType = $fields[3]; + + return $this->isStream(); + } + } + } + + return false; + } + + protected function getChangeList($reference) + { + $index = strpos($reference, '@'); + if ($index === false) { + return; + } + $label = substr($reference, $index); + $command = $this->generateP4Command(' changes -m1 ' . $label); + $this->executeCommand($command); + $changes = $this->commandResult; + if (strpos($changes, 'Change') !== 0) { + return; + } + $fields = explode(' ', $changes); + $changeList = $fields[1]; + + return $changeList; + } + + public function getCommitLogs($fromReference, $toReference) + { + $fromChangeList = $this->getChangeList($fromReference); + if ($fromChangeList == null) { + return; + } + $toChangeList = $this->getChangeList($toReference); + if ($toChangeList == null) { + return; + } + $index = strpos($fromReference, '@'); + $main = substr($fromReference, 0, $index) . '/...'; + $command = $this->generateP4Command('filelog ' . $main . '@' . $fromChangeList. ',' . $toChangeList); + $this->executeCommand($command); + $result = $this->commandResult; + + 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 ab60e2bbc..d5a0549d3 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -13,6 +13,8 @@ namespace Composer\Util; use Symfony\Component\Process\Process; +use Symfony\Component\Process\ProcessUtils; +use Composer\IO\IOInterface; /** * @author Robert Schönthal @@ -23,18 +25,35 @@ class ProcessExecutor protected $captureOutput; protected $errorOutput; + protected $io; + + public function __construct(IOInterface $io = null) + { + $this->io = $io; + } /** * 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) { + if ($this->io && $this->io->isDebug()) { + $safeCommand = preg_replace('{(://[^:/\s]+:)[^@\s/]+}i', '$1****', $command); + $this->io->write('Executing command ('.($cwd ?: 'CWD').'): '.$safeCommand); + } + + // make sure that null translate to the proper directory in case the dir is a symlink + // and we call a git command, because msysgit does not handle symlinks properly + if (null === $cwd && defined('PHP_WINDOWS_VERSION_BUILD') && false !== strpos($command, 'git') && getcwd()) { + $cwd = realpath(getcwd()); + } + $this->captureOutput = count(func_get_args()) > 1; $this->errorOutput = null; $process = new Process($command, $cwd, null, null, static::getTimeout()); @@ -53,6 +72,8 @@ class ProcessExecutor public function splitLines($output) { + $output = trim($output); + return ((string) $output === '') ? array() : preg_split('{\r?\n}', $output); } @@ -84,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 882ff4bce..780f437c6 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -13,34 +13,43 @@ namespace Composer\Util; use Composer\Composer; +use Composer\Config; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; /** * @author François Pluchino + * @author Jordi Boggiano + * @author Nils Adermann */ class RemoteFilesystem { private $io; + private $config; private $firstCall; private $bytesMax; private $originUrl; private $fileUrl; private $fileName; - private $result; + private $retry; 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; } @@ -57,9 +66,7 @@ class RemoteFilesystem */ public function copy($originUrl, $fileUrl, $fileName, $progress = true, $options = array()) { - $this->get($originUrl, $fileUrl, $options, $fileName, $progress); - - return $this->result; + return $this->get($originUrl, $fileUrl, $options, $fileName, $progress); } /** @@ -70,13 +77,31 @@ 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()) { - $this->get($originUrl, $fileUrl, $options, null, $progress); + return $this->get($originUrl, $fileUrl, $options, null, $progress); + } + + /** + * Retrieve the options set in the constructor + * + * @return array Options + */ + public function getOptions() + { + return $this->options; + } - return $this->result; + /** + * Returns the headers of the last request + * + * @return array + */ + public function getLastHeaders() + { + return $this->lastHeaders; } /** @@ -88,20 +113,50 @@ class RemoteFilesystem * @param string $fileName the local filename * @param boolean $progress Display the progression * - * @throws TransportException When the file could not be downloaded + * @throws TransportException|\Exception + * @throws TransportException When the file could not be downloaded + * + * @return bool|string */ 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->result = null; $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); - $ctx = StreamContextFactory::getContext($options, array('notification' => array($this, 'callbackGet'))); + + if ($this->io->isDebug()) { + $this->io->write((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl); + } + if (isset($options['github-token'])) { + $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) { $this->io->write(" Downloading: connection...", false); @@ -109,6 +164,7 @@ class RemoteFilesystem $errorMessage = ''; $errorCode = 0; + $result = false; set_error_handler(function ($code, $msg) use (&$errorMessage) { if ($errorMessage) { $errorMessage .= "\n"; @@ -121,23 +177,33 @@ 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.')'; } restore_error_handler(); - if (isset($e)) { + if (isset($e) && !$this->retry) { 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 - if (false !== $result && extension_loaded('zlib') && substr($fileUrl, 0, 4) === 'http') { + if ($result && extension_loaded('zlib') && substr($fileUrl, 0, 4) === 'http') { $decode = false; foreach ($http_response_header as $header) { if (preg_match('{^content-encoding: *gzip *$}i', $header)) { @@ -158,12 +224,16 @@ class RemoteFilesystem } } - if ($this->progress) { + if ($this->progress && !$this->retry) { $this->io->overwrite(" Downloading: 100%"); } // handle copy command if download was successful if (false !== $result && null !== $fileName) { + if ('' === $result) { + throw new TransportException('"'.$this->fileUrl.'" appears broken, and returned an empty 200 response'); + } + $errorMessage = ''; set_error_handler(function ($code, $msg) use (&$errorMessage) { if ($errorMessage) { @@ -174,64 +244,69 @@ class RemoteFilesystem $result = (bool) file_put_contents($fileName, $result); restore_error_handler(); if (false === $result) { - throw new TransportException('The "'.$fileUrl.'" file could not be written to '.$fileName.': '.$errorMessage); + throw new TransportException('The "'.$this->fileUrl.'" file could not be written to '.$fileName.': '.$errorMessage); } } - // avoid overriding if content was loaded by a sub-call to get() - if (null === $this->result) { - $this->result = $result; + if ($this->retry) { + $this->retry = false; + + $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 === $this->result) { - $e = new TransportException('The "'.$fileUrl.'" file could not be downloaded: '.$errorMessage, $errorCode); + if (false === $result) { + $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded: '.$errorMessage, $errorCode); if (!empty($http_response_header[0])) { $e->setHeaders($http_response_header); } throw $e; } + + if (!empty($http_response_header[0])) { + $this->lastHeaders = $http_response_header; + } + + return $result; } /** * Get notification action. * - * @param integer $notificationCode The notification code - * @param integer $severity The severity level - * @param string $message The message - * @param integer $messageCode The message code - * @param integer $bytesTransferred The loaded size - * @param integer $bytesMax The total size + * @param integer $notificationCode The notification code + * @param integer $severity The severity level + * @param string $message The message + * @param integer $messageCode The message code + * @param integer $bytesTransferred The loaded size + * @param integer $bytesMax The total size + * @throws TransportException */ protected function callbackGet($notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax) { switch ($notificationCode) { case STREAM_NOTIFY_FAILURE: - throw new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.trim($message).')', $messageCode); - break; - 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->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->get($this->originUrl, $this->fileUrl, $this->fileName, $this->progress); + $this->promptAuthAndRetry($messageCode); + break; } break; case STREAM_NOTIFY_AUTH_RESULT: if (403 === $messageCode) { - $message = "The '" . $this->fileUrl . "' URL could not be accessed: " . $message; - - throw new TransportException($message, 403); + $this->promptAuthAndRetry($messageCode, $message); + break; } break; @@ -261,6 +336,49 @@ class RemoteFilesystem } } + protected function promptAuthAndRetry($httpStatus, $reason = null) + { + 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, $this); + 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'); + } + protected function getOptionsForUrl($originUrl, $additionalOptions) { $headers = array( @@ -279,14 +397,18 @@ class RemoteFilesystem $headers[] = 'Accept-Encoding: gzip'; } + $options = array_replace_recursive($this->options, $additionalOptions); + if ($this->io->hasAuthentication($originUrl)) { $auth = $this->io->getAuthentication($originUrl); - $authStr = base64_encode($auth['username'] . ':' . $auth['password']); - $headers[] = 'Authorization: Basic '.$authStr; + if ('github.com' === $originUrl && 'x-oauth-basic' === $auth['password']) { + $options['github-token'] = $auth['username']; + } else { + $authStr = base64_encode($auth['username'] . ':' . $auth['password']); + $headers[] = 'Authorization: Basic '.$authStr; + } } - $options = array_replace_recursive($this->options, $additionalOptions); - if (isset($options['http']['header']) && !is_array($options['http']['header'])) { $options['http']['header'] = explode("\r\n", trim($options['http']['header'], "\r\n")); } diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index 02ff62a7b..1c3c20e44 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -16,20 +16,26 @@ namespace Composer\Util; * Allows the creation of a basic context supporting http proxy * * @author Jordan Alliot + * @author Markus Tacker */ final class StreamContextFactory { /** * Creates a context supporting HTTP proxies * + * @param string $url URL the context is to be used for * @param array $defaultOptions Options to merge with the default * @param array $defaultParams Parameters to specify on the context * @return resource Default context * @throws \RuntimeException if https proxy required and OpenSSL uninstalled */ - public static function getContext(array $defaultOptions = array(), array $defaultParams = array()) + public static function getContext($url, array $defaultOptions = array(), array $defaultParams = array()) { - $options = array('http' => array()); + $options = array('http' => array( + // specify defaults again to try and work better with curlwrappers enabled + 'follow_location' => 1, + 'max_redirects' => 20, + )); // Handle system proxy if (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy'])) { @@ -56,32 +62,82 @@ final class StreamContextFactory throw new \RuntimeException('You must enable the openssl extension to use a proxy over https'); } - $options['http'] = array( - 'proxy' => $proxyURL, - 'request_fulluri' => true, - ); + $options['http']['proxy'] = $proxyURL; - if (isset($proxy['user'])) { - $auth = $proxy['user']; - if (isset($proxy['pass'])) { - $auth .= ':' . $proxy['pass']; + // 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']); } - $auth = base64_encode($auth); + } + + // 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; + } + + 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']); + // 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}"); } - $defaultOptions['http']['header'][] = "Proxy-Authorization: Basic {$auth}"; - } else { - $options['http']['header'] = array("Proxy-Authorization: Basic {$auth}"); } } } $options = array_replace_recursive($options, $defaultOptions); + if (isset($options['http']['header'])) { + $options['http']['header'] = self::fixHttpHeaderField($options['http']['header']); + } + return stream_context_create($options, $defaultParams); } + + /** + * A bug in PHP prevents the headers from correctly being sent when a content-type header is present and + * NOT at the end of the array + * + * This method fixes the array by moving the content-type header to the end + * + * @link https://bugs.php.net/bug.php?id=61548 + * @param $header + * @return array + */ + private static function fixHttpHeaderField($header) + { + if (!is_array($header)) { + $header = explode("\r\n", $header); + } + uasort($header, function ($el) { + return preg_match('{^content-type}i', $el) ? 1 : -1; + }); + + return $header; + } } diff --git a/src/Composer/Util/Svn.php b/src/Composer/Util/Svn.php index d979a2ddb..4b6d615fb 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; /** @@ -20,6 +21,8 @@ use Composer\IO\IOInterface; */ class Svn { + const MAX_QTY_AUTH_TRIES = 5; + /** * @var array */ @@ -50,18 +53,36 @@ class Svn */ protected $process; + /** + * @var integer + */ + 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. @@ -85,6 +106,9 @@ class Svn if ($type !== 'out') { return; } + if ('Redirecting to URL ' === substr($buffer, 0, 19)) { + return; + } $output .= $buffer; if ($verbose) { $io->write($buffer, false); @@ -100,23 +124,19 @@ class Svn } // the error is not auth-related - if (false === stripos($output, 'Could not authenticate to server:')) { + if (false === stripos($output, 'Could not authenticate to server:') + && false === stripos($output, 'authorization failed') + && false === stripos($output, 'svn: E170001:') + && false === stripos($output, 'svn: E215004:')) { 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.')' - ); - } - - // TODO keep a count of user auth attempts and ask 5 times before - // failing hard (currently it fails hard directly if the URL has credentials) - - // try to authenticate if (!$this->hasAuth()) { $this->doAuthDance(); + } + + // try to authenticate if maximum quantity of tries not reached + if ($this->qtyAuthTries++ < self::MAX_QTY_AUTH_TRIES) { // restart the process return $this->execute($command, $url, $cwd, $path, $verbose); @@ -127,13 +147,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; @@ -160,11 +196,11 @@ class Svn $cmd, '--non-interactive ', $this->getCredentialString(), - escapeshellarg($url) + ProcessExecutor::escape($url) ); if ($path) { - $cmd .= ' ' . escapeshellarg($path); + $cmd .= ' ' . ProcessExecutor::escape($path); } return $cmd; @@ -186,8 +222,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()) ); } @@ -232,17 +268,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; } /** @@ -254,4 +284,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/src/bootstrap.php b/src/bootstrap.php index 59af59453..338b3cce4 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -12,14 +12,12 @@ function includeIfExists($file) { - if (file_exists($file)) { - return include $file; - } + return file_exists($file) ? include $file : false; } if ((!$loader = includeIfExists(__DIR__.'/../vendor/autoload.php')) && (!$loader = includeIfExists(__DIR__.'/../../../autoload.php'))) { echo 'You must set up the project dependencies, run the following commands:'.PHP_EOL. - 'curl -s http://getcomposer.org/installer | php'.PHP_EOL. + 'curl -sS https://getcomposer.org/installer | php'.PHP_EOL. 'php composer.phar install'.PHP_EOL; exit(1); } diff --git a/tests/Composer/Test/AllFunctionalTest.php b/tests/Composer/Test/AllFunctionalTest.php index 1bd1ed6b9..f27d58117 100644 --- a/tests/Composer/Test/AllFunctionalTest.php +++ b/tests/Composer/Test/AllFunctionalTest.php @@ -1,5 +1,15 @@ + * 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 Symfony\Component\Process\Process; @@ -55,10 +65,11 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase $fs->ensureDirectoryExists(dirname(self::$pharPath)); chdir(dirname(self::$pharPath)); - $proc = new Process('php '.escapeshellarg(__DIR__.'/../../../bin/compile')); + $proc = new Process('php '.escapeshellarg(__DIR__.'/../../../bin/compile'), dirname(self::$pharPath)); $exitcode = $proc->run(); - - $this->assertSame(0, $exitcode); + if ($exitcode !== 0 || trim($proc->getOutput())) { + $this->fail($proc->getOutput()); + } $this->assertTrue(file_exists(self::$pharPath)); } @@ -74,7 +85,7 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase putenv('COMPOSER_HOME='.$this->testDir.'home'); $cmd = 'php '.escapeshellarg(self::$pharPath).' --no-ansi '.$testData['RUN']; - $proc = new Process($cmd); + $proc = new Process($cmd, __DIR__.'/Fixtures/functional'); $exitcode = $proc->run(); if (isset($testData['EXPECT'])) { @@ -113,7 +124,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/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index 9693d8826..7c4ddccd9 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -17,37 +17,83 @@ use Composer\Package\Link; use Composer\Util\Filesystem; use Composer\Package\AliasPackage; use Composer\Package\Package; -use Composer\Test\TestCase; +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; + protected function setUp() { $this->fs = new Filesystem; $that = $this; - $this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest'; + $this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.md5(uniqid('', true)); $this->fs->ensureDirectoryExists($this->workingDir); - $this->vendorDir = $this->workingDir.DIRECTORY_SEPARATOR.'composer-test-autoload-'.md5(uniqid('', true)); + $this->vendorDir = $this->workingDir.DIRECTORY_SEPARATOR.'composer-test-autoload'; $this->ensureDirectoryExistsAndClear($this->vendorDir); $this->config = $this->getMock('Composer\Config'); - $this->config->expects($this->any()) + + $this->config->expects($this->at(0)) + ->method('get') + ->with($this->equalTo('vendor-dir')) + ->will($this->returnCallback(function () use ($that) { + return $that->vendorDir; + })); + + $this->config->expects($this->at(1)) ->method('get') ->with($this->equalTo('vendor-dir')) ->will($this->returnCallback(function () use ($that) { return $that->vendorDir; })); - $this->dir = getcwd(); + $this->origDir = getcwd(); chdir($this->workingDir); $this->im = $this->getMockBuilder('Composer\Installer\InstallationManager') @@ -56,16 +102,22 @@ class AutoloadGeneratorTest extends TestCase $this->im->expects($this->any()) ->method('getInstallPath') ->will($this->returnCallback(function ($package) use ($that) { - return $that->vendorDir.'/'.$package->getName(); + $targetDir = $package->getTargetDir(); + + return $that->vendorDir.'/'.$package->getName() . ($targetDir ? '/'.$targetDir : ''); })); - $this->repository = $this->getMock('Composer\Repository\RepositoryInterface'); + $this->repository = $this->getMock('Composer\Repository\InstalledRepositoryInterface'); - $this->generator = new AutoloadGenerator(); + $this->eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') + ->disableOriginalConstructor() + ->getMock(); + + $this->generator = new AutoloadGenerator($this->eventDispatcher); } protected function tearDown() { - chdir($this->dir); + chdir($this->origDir); if (is_dir($this->workingDir)) { $this->fs->removeDirectory($this->workingDir); @@ -79,25 +131,117 @@ class AutoloadGeneratorTest extends TestCase { $package = new Package('a', '1.0', '1.0'); $package->setAutoload(array( - 'psr-0' => array('Main' => 'src/', 'Lala' => array('src/', 'lib/')), + 'psr-0' => array( + 'Main' => 'src/', + 'Lala' => array('src/', 'lib/'), + ), + 'psr-4' => array( + 'Acme\Fruit\\' => 'src-fruit/', + 'Acme\Cake\\' => array('src-cake/', 'lib-cake/'), + ), 'classmap' => array('composersrc/'), )); $this->repository->expects($this->once()) - ->method('getPackages') + ->method('getCanonicalPackages') ->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', 'createClassFile($this->workingDir); + $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_1'); - $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_1'); + // Assert that autoload_namespaces.php was correctly generated. $this->assertAutoloadFiles('main', $this->vendorDir.'/composer'); + + // Assert that autoload_psr4.php was correctly generated. + $this->assertAutoloadFiles('psr4', $this->vendorDir.'/composer', 'psr4'); + + // Assert that autoload_classmap.php was correctly generated. $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; @@ -105,21 +249,27 @@ class AutoloadGeneratorTest extends TestCase $package = new Package('a', '1.0', '1.0'); $package->setAutoload(array( 'psr-0' => array('Main' => 'src/', 'Lala' => 'src/'), + 'psr-4' => array( + 'Acme\Fruit\\' => 'src-fruit/', + 'Acme\Cake\\' => array('src-cake/', 'lib-cake/'), + ), 'classmap' => array('composersrc/'), )); $this->repository->expects($this->once()) - ->method('getPackages') + ->method('getCanonicalPackages') ->will($this->returnValue(array())); $this->fs->ensureDirectoryExists($this->vendorDir.'/composer'); $this->fs->ensureDirectoryExists($this->vendorDir.'/src/Main'); file_put_contents($this->vendorDir.'/src/Main/Foo.php', 'createClassFile($this->vendorDir); + $this->fs->ensureDirectoryExists($this->vendorDir.'/composersrc'); + file_put_contents($this->vendorDir.'/composersrc/foo.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_2'); $this->assertAutoloadFiles('main3', $this->vendorDir.'/composer'); + $this->assertAutoloadFiles('psr4_3', $this->vendorDir.'/composer', 'psr4'); $this->assertAutoloadFiles('classmap3', $this->vendorDir.'/composer', 'classmap'); } @@ -128,11 +278,15 @@ class AutoloadGeneratorTest extends TestCase $package = new Package('a', '1.0', '1.0'); $package->setAutoload(array( 'psr-0' => array('Main' => 'src/', 'Lala' => 'src/'), + 'psr-4' => array( + 'Acme\Fruit\\' => 'src-fruit/', + 'Acme\Cake\\' => array('src-cake/', 'lib-cake/'), + ), 'classmap' => array('composersrc/'), )); $this->repository->expects($this->once()) - ->method('getPackages') + ->method('getCanonicalPackages') ->will($this->returnValue(array())); $this->vendorDir .= '/subdir'; @@ -140,9 +294,11 @@ class AutoloadGeneratorTest extends TestCase $this->fs->ensureDirectoryExists($this->vendorDir.'/composer'); $this->fs->ensureDirectoryExists($this->workingDir.'/src'); - $this->createClassFile($this->workingDir); + $this->fs->ensureDirectoryExists($this->workingDir.'/composersrc'); + file_put_contents($this->workingDir.'/composersrc/foo.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_3'); $this->assertAutoloadFiles('main2', $this->vendorDir.'/composer'); + $this->assertAutoloadFiles('psr4_2', $this->vendorDir.'/composer', 'psr4'); $this->assertAutoloadFiles('classmap2', $this->vendorDir.'/composer', 'classmap'); } @@ -151,40 +307,29 @@ class AutoloadGeneratorTest extends TestCase $package = new Package('a', '1.0', '1.0'); $package->setAutoload(array( 'psr-0' => array('Main\\Foo' => '', 'Main\\Bar' => ''), + 'classmap' => array('Main/Foo/src', 'lib'), + 'files' => array('foo.php', 'Main/Foo/bar.php'), )); $package->setTargetDir('Main/Foo/'); $this->repository->expects($this->once()) - ->method('getPackages') + ->method('getCanonicalPackages') ->will($this->returnValue(array())); $this->fs->ensureDirectoryExists($this->vendorDir.'/a'); + $this->fs->ensureDirectoryExists($this->workingDir.'/src'); + $this->fs->ensureDirectoryExists($this->workingDir.'/lib'); + + file_put_contents($this->workingDir.'/src/rootfoo.php', 'workingDir.'/lib/rootbar.php', 'workingDir.'/foo.php', 'workingDir.'/bar.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, 'TargetDir'); $this->assertFileEquals(__DIR__.'/Fixtures/autoload_target_dir.php', $this->vendorDir.'/autoload.php'); $this->assertFileEquals(__DIR__.'/Fixtures/autoload_real_target_dir.php', $this->vendorDir.'/composer/autoload_real.php'); - } - - public function testMainPackageAutoloadingWithTargetDirAndClassmap() - { - $package = new Package('a', '1.0', '1.0'); - $package->setAutoload(array( - 'classmap' => array('Main/Foo/composersrc/'), - )); - $package->setTargetDir('Main/Foo/'); - - $this->repository->expects($this->once()) - ->method('getPackages') - ->will($this->returnValue(array())); - - $this->vendorDir .= '/subdir'; - - $this->fs->ensureDirectoryExists($this->vendorDir.'/composer'); - $this->fs->ensureDirectoryExists($this->workingDir.'/src'); - - $this->createClassFile($this->workingDir); - $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, 'TargetDirNoPsr'); - $this->assertAutoloadFiles('classmap2', $this->vendorDir.'/composer', 'classmap'); + $this->assertFileEquals(__DIR__.'/Fixtures/autoload_files_target_dir.php', $this->vendorDir.'/composer/autoload_files.php'); + $this->assertAutoloadFiles('classmap6', $this->vendorDir.'/composer', 'classmap'); } public function testVendorsAutoloading() @@ -199,7 +344,7 @@ class AutoloadGeneratorTest extends TestCase $b->setAutoload(array('psr-0' => array('B\\Sub\\Name' => 'src/'))); $this->repository->expects($this->once()) - ->method('getPackages') + ->method('getCanonicalPackages') ->will($this->returnValue($packages)); $this->fs->ensureDirectoryExists($this->vendorDir.'/composer'); @@ -212,14 +357,17 @@ class AutoloadGeneratorTest extends TestCase $this->assertTrue(file_exists($this->vendorDir.'/composer/autoload_classmap.php'), "ClassMap file needs to be generated, even if empty."); } - public function testPSR0ToClassMapIgnoresNonExistingDir() + public function testPSRToClassMapIgnoresNonExistingDir() { $package = new Package('a', '1.0', '1.0'); - $package->setAutoload(array('psr-0' => array('foo/bar/non/existing/'))); + $package->setAutoload(array( + 'psr-0' => array('Prefix' => 'foo/bar/non/existing/'), + 'psr-4' => array('Prefix\\' => 'foo/bar/non/existing2/') + )); $this->repository->expects($this->once()) - ->method('getPackages') + ->method('getCanonicalPackages') ->will($this->returnValue(array())); $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_8'); @@ -241,7 +389,7 @@ class AutoloadGeneratorTest extends TestCase $b->setAutoload(array('classmap' => array('src/', 'lib/'))); $this->repository->expects($this->once()) - ->method('getPackages') + ->method('getCanonicalPackages') ->will($this->returnValue($packages)); $this->fs->ensureDirectoryExists($this->vendorDir.'/composer'); @@ -255,16 +403,51 @@ class AutoloadGeneratorTest extends TestCase $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_6'); $this->assertTrue(file_exists($this->vendorDir.'/composer/autoload_classmap.php'), "ClassMap file needs to be generated."); $this->assertEquals( - $this->normalizePaths(array( + array( 'ClassMapBar' => $this->vendorDir.'/b/b/src/b.php', 'ClassMapBaz' => $this->vendorDir.'/b/b/lib/c.php', 'ClassMapFoo' => $this->vendorDir.'/a/a/src/a.php', - )), - $this->normalizePaths(include $this->vendorDir.'/composer/autoload_classmap.php') + ), + include $this->vendorDir.'/composer/autoload_classmap.php' ); $this->assertAutoloadFiles('classmap4', $this->vendorDir.'/composer', 'classmap'); } + public function testVendorsClassMapAutoloadingWithTargetDir() + { + $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'); + $a->setAutoload(array('classmap' => array('target/src/', 'lib/'))); + $a->setTargetDir('target'); + $b->setAutoload(array('classmap' => array('src/'))); + + $this->repository->expects($this->once()) + ->method('getCanonicalPackages') + ->will($this->returnValue($packages)); + + $this->fs->ensureDirectoryExists($this->vendorDir.'/composer'); + $this->fs->ensureDirectoryExists($this->vendorDir.'/a/a/target/src'); + $this->fs->ensureDirectoryExists($this->vendorDir.'/a/a/target/lib'); + $this->fs->ensureDirectoryExists($this->vendorDir.'/b/b/src'); + file_put_contents($this->vendorDir.'/a/a/target/src/a.php', 'vendorDir.'/a/a/target/lib/b.php', 'vendorDir.'/b/b/src/c.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_6'); + $this->assertTrue(file_exists($this->vendorDir.'/composer/autoload_classmap.php'), "ClassMap file needs to be generated."); + $this->assertEquals( + array( + 'ClassMapBar' => $this->vendorDir.'/a/a/target/lib/b.php', + 'ClassMapBaz' => $this->vendorDir.'/b/b/src/c.php', + 'ClassMapFoo' => $this->vendorDir.'/a/a/target/src/a.php', + ), + include $this->vendorDir.'/composer/autoload_classmap.php' + ); + } + public function testClassMapAutoloadingEmptyDirAndExactFile() { $package = new Package('a', '1.0', '1.0'); @@ -278,7 +461,7 @@ class AutoloadGeneratorTest extends TestCase $c->setAutoload(array('classmap' => array('./'))); $this->repository->expects($this->once()) - ->method('getPackages') + ->method('getCanonicalPackages') ->will($this->returnValue($packages)); $this->fs->ensureDirectoryExists($this->vendorDir.'/composer'); @@ -292,12 +475,12 @@ class AutoloadGeneratorTest extends TestCase $this->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( - $this->normalizePaths(array( + array( 'ClassMapBar' => $this->vendorDir.'/b/b/test.php', 'ClassMapBaz' => $this->vendorDir.'/c/c/foo/test.php', 'ClassMapFoo' => $this->vendorDir.'/a/a/src/a.php', - )), - $this->normalizePaths(include $this->vendorDir.'/composer/autoload_classmap.php') + ), + include $this->vendorDir.'/composer/autoload_classmap.php' ); $this->assertAutoloadFiles('classmap5', $this->vendorDir.'/composer', 'classmap'); } @@ -310,26 +493,35 @@ class AutoloadGeneratorTest extends TestCase $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('files' => array('test.php'))); $b->setAutoload(array('files' => array('test2.php'))); + $c->setAutoload(array('files' => array('test3.php', 'foo/bar/test4.php'))); + $c->setTargetDir('foo/bar'); $this->repository->expects($this->once()) - ->method('getPackages') + ->method('getCanonicalPackages') ->will($this->returnValue($packages)); $this->fs->ensureDirectoryExists($this->vendorDir.'/a/a'); $this->fs->ensureDirectoryExists($this->vendorDir.'/b/b'); + $this->fs->ensureDirectoryExists($this->vendorDir.'/c/c/foo/bar'); file_put_contents($this->vendorDir.'/a/a/test.php', 'vendorDir.'/b/b/test2.php', 'vendorDir.'/c/c/foo/bar/test3.php', 'vendorDir.'/c/c/foo/bar/test4.php', 'workingDir.'/root.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, 'FilesAutoload'); $this->assertFileEquals(__DIR__.'/Fixtures/autoload_functions.php', $this->vendorDir.'/autoload.php'); $this->assertFileEquals(__DIR__.'/Fixtures/autoload_real_functions.php', $this->vendorDir.'/composer/autoload_real.php'); + $this->assertFileEquals(__DIR__.'/Fixtures/autoload_files_functions.php', $this->vendorDir.'/composer/autoload_files.php'); include $this->vendorDir . '/autoload.php'; $this->assertTrue(function_exists('testFilesAutoloadGeneration1')); $this->assertTrue(function_exists('testFilesAutoloadGeneration2')); + $this->assertTrue(function_exists('testFilesAutoloadGeneration3')); + $this->assertTrue(function_exists('testFilesAutoloadGeneration4')); $this->assertTrue(function_exists('testFilesAutoloadGenerationRoot')); } @@ -363,7 +555,7 @@ class AutoloadGeneratorTest extends TestCase $e->setRequires(array(new Link('e/e', 'c/lorem'))); $this->repository->expects($this->once()) - ->method('getPackages') + ->method('getCanonicalPackages') ->will($this->returnValue($packages)); $this->fs->ensureDirectoryExists($this->vendorDir . '/z/foo'); @@ -392,20 +584,34 @@ 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('getPackages') + ->method('getCanonicalPackages') ->will($this->returnValue($packages)); $this->fs->ensureDirectoryExists($this->workingDir.'/lib/A/B'); @@ -415,24 +621,41 @@ 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', 'workingDir, '\\', '/'); $expectedNamespace = << array(\$vendorDir . '/b/b/src'), + 'A\\\\B' => array(\$baseDir . '/lib', \$vendorDir . '/a/a/lib'), + 'A' => array(\$vendorDir . '/a/a/src'), +); + +EOF; + + // autoload_psr4.php is expected to be empty in this example. + $expectedPsr4 = << \$vendorDir . '/b/b/src/', - 'A\\\\B' => array('$workDir/lib', \$vendorDir . '/a/a/lib/'), - 'A' => \$vendorDir . '/a/a/src/', ); EOF; @@ -440,9 +663,9 @@ EOF; $expectedClassmap = <<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')); } @@ -476,7 +700,7 @@ EOF; $packages[] = $c; $this->repository->expects($this->once()) - ->method("getPackages") + ->method("getCanonicalPackages") ->will($this->returnValue($packages)); $this->fs->ensureDirectoryExists($this->vendorDir.'/composer'); @@ -505,7 +729,7 @@ EOF; $packages[] = $a; $this->repository->expects($this->once()) - ->method("getPackages") + ->method("getCanonicalPackages") ->will($this->returnValue($packages)); mkdir($this->vendorDir."/composer", 0777, true); @@ -533,7 +757,7 @@ EOF; $a->setIncludePaths(array("lib/")); $this->repository->expects($this->once()) - ->method("getPackages") + ->method("getCanonicalPackages") ->will($this->returnValue($packages)); mkdir($this->vendorDir."/composer", 0777, true); @@ -561,7 +785,7 @@ EOF; $packages[] = $a; $this->repository->expects($this->once()) - ->method("getPackages") + ->method("getCanonicalPackages") ->will($this->returnValue($packages)); mkdir($this->vendorDir."/composer", 0777, true); @@ -571,36 +795,354 @@ EOF; $this->assertFalse(file_exists($this->vendorDir."/composer/include_paths.php")); } - private function createClassFile($basedir) + public function testPreAndPostEventsAreDispatchedDuringAutoloadDump() { - if (!is_dir($basedir.'/composersrc')) { - mkdir($basedir.'/composersrc', 0777, true); - } + $this->eventDispatcher + ->expects($this->at(0)) + ->method('dispatchScript') + ->with(ScriptEvents::PRE_AUTOLOAD_DUMP, false); + + $this->eventDispatcher + ->expects($this->at(1)) + ->method('dispatchScript') + ->with(ScriptEvents::POST_AUTOLOAD_DUMP, false); + + $package = new Package('a', '1.0', '1.0'); + $package->setAutoload(array('psr-0' => array('foo/bar/non/existing/'))); - file_put_contents($basedir.'/composersrc/foo.php', 'repository->expects($this->once()) + ->method('getCanonicalPackages') + ->will($this->returnValue(array())); + + $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_8'); } - private function assertAutoloadFiles($name, $dir, $type = 'namespaces') + public function testUseGlobalIncludePath() { - $a = __DIR__.'/Fixtures/autoload_'.$name.'.php'; - $b = $dir.'/autoload_'.$type.'.php'; - $this->assertEquals( - str_replace('%vendorDir%', basename($this->vendorDir), file_get_contents($a)), - file_get_contents($b), - $a .' does not equal '. $b - ); + $package = new Package('a', '1.0', '1.0'); + $package->setAutoload(array( + 'psr-0' => array('Main\\Foo' => '', 'Main\\Bar' => ''), + )); + $package->setTargetDir('Main/Foo/'); + + $this->repository->expects($this->once()) + ->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->fs->ensureDirectoryExists($this->vendorDir.'/a'); + + $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, 'IncludePath'); + $this->assertFileEquals(__DIR__.'/Fixtures/autoload_real_include_path.php', $this->vendorDir.'/composer/autoload_real.php'); } - private function normalizePaths($paths) + public function testVendorDirExcludedFromWorkingDir() { - if (!is_array($paths)) { - return strtr($paths, '\\', '/'); - } + $workingDir = $this->vendorDir.'/working-dir'; + $vendorDir = $workingDir.'/../vendor'; - foreach ($paths as $key => $path) { - $paths[$key] = strtr($path, '\\', '/'); - } + $this->fs->ensureDirectoryExists($workingDir); + chdir($workingDir); + + $package = new Package('a', '1.0', '1.0'); + $package->setAutoload(array( + 'psr-0' => array('Foo' => 'src'), + 'psr-4' => array('Acme\Foo\\' => 'src-psr4'), + 'classmap' => array('classmap'), + 'files' => array('test.php'), + )); + + $vendorPackage = new Package('b/b', '1.0', '1.0'); + $vendorPackage->setAutoload(array( + 'psr-0' => array('Bar' => 'lib'), + 'psr-4' => array('Acme\Bar\\' => 'lib-psr4'), + 'classmap' => array('classmaps'), + 'files' => array('bootstrap.php'), + )); + + $this->repository->expects($this->once()) + ->method('getCanonicalPackages') + ->will($this->returnValue(array($vendorPackage))); + + $im = $this->getMockBuilder('Composer\Installer\InstallationManager') + ->disableOriginalConstructor() + ->getMock(); + $im->expects($this->any()) + ->method('getInstallPath') + ->will($this->returnCallback(function ($package) use ($vendorDir) { + $targetDir = $package->getTargetDir(); + + return $vendorDir.'/'.$package->getName() . ($targetDir ? '/'.$targetDir : ''); + })); + + $this->fs->ensureDirectoryExists($workingDir.'/src/Foo'); + $this->fs->ensureDirectoryExists($workingDir.'/classmap'); + $this->fs->ensureDirectoryExists($vendorDir.'/composer'); + $this->fs->ensureDirectoryExists($vendorDir.'/b/b/lib/Bar'); + $this->fs->ensureDirectoryExists($vendorDir.'/b/b/classmaps'); + file_put_contents($workingDir.'/src/Foo/Bar.php', 'vendorDir; + $this->vendorDir = $vendorDir; + $this->generator->dump($this->config, $this->repository, $package, $im, 'composer', true, '_13'); + $this->vendorDir = $oldVendorDir; + + $expectedNamespace = <<<'EOF' + array($baseDir . '/src'), + 'Bar' => array($vendorDir . '/b/b/lib'), +); + +EOF; + + $expectedPsr4 = <<<'EOF' + array($baseDir . '/src-psr4'), + 'Acme\\Bar\\' => array($vendorDir . '/b/b/lib-psr4'), +); + +EOF; + + $expectedClassmap = <<<'EOF' + $vendorDir . '/b/b/classmaps/classes.php', + 'Bar\\Foo' => $vendorDir . '/b/b/lib/Bar/Foo.php', + 'Foo\\Bar' => $baseDir . '/src/Foo/Bar.php', + 'Foo\\Foo' => $baseDir . '/classmap/classes.php', +); + +EOF; + + $this->assertEquals($expectedNamespace, file_get_contents($vendorDir.'/composer/autoload_namespaces.php')); + $this->assertEquals($expectedPsr4, file_get_contents($vendorDir.'/composer/autoload_psr4.php')); + $this->assertEquals($expectedClassmap, file_get_contents($vendorDir.'/composer/autoload_classmap.php')); + $this->assertContains("\n \$vendorDir . '/b/b/bootstrap.php',\n", file_get_contents($vendorDir.'/composer/autoload_files.php')); + $this->assertContains("\n \$baseDir . '/test.php',\n", file_get_contents($vendorDir.'/composer/autoload_files.php')); + } + + public function testUpLevelRelativePaths() + { + $workingDir = $this->workingDir.'/working-dir'; + mkdir($workingDir, 0777, true); + chdir($workingDir); + + $package = new Package('a', '1.0', '1.0'); + $package->setAutoload(array( + 'psr-0' => array('Foo' => '../path/../src'), + 'psr-4' => array('Acme\Foo\\' => '../path/../src-psr4'), + 'classmap' => array('../classmap'), + 'files' => array('../test.php'), + )); + + $this->repository->expects($this->once()) + ->method('getCanonicalPackages') + ->will($this->returnValue(array())); + + $this->fs->ensureDirectoryExists($this->workingDir.'/src/Foo'); + $this->fs->ensureDirectoryExists($this->workingDir.'/classmap'); + file_put_contents($this->workingDir.'/src/Foo/Bar.php', 'workingDir.'/classmap/classes.php', 'workingDir.'/test.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_14'); + + $expectedNamespace = <<<'EOF' + array($baseDir . '/../src'), +); + +EOF; + + $expectedPsr4 = <<<'EOF' + array($baseDir . '/../src-psr4'), +); + +EOF; + + $expectedClassmap = <<<'EOF' + $baseDir . '/../src/Foo/Bar.php', + 'Foo\\Foo' => $baseDir . '/../classmap/classes.php', +); + +EOF; - return $paths; + $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')); + $this->assertContains("\n \$baseDir . '/../test.php',\n", file_get_contents($this->vendorDir.'/composer/autoload_files.php')); + } + + public function testEmptyPaths() + { + $package = new Package('a', '1.0', '1.0'); + $package->setAutoload(array( + 'psr-0' => array('Foo' => ''), + 'psr-4' => array('Acme\Foo\\' => ''), + 'classmap' => array(''), + )); + + $this->repository->expects($this->once()) + ->method('getCanonicalPackages') + ->will($this->returnValue(array())); + + $this->fs->ensureDirectoryExists($this->workingDir.'/Foo'); + file_put_contents($this->workingDir.'/Foo/Bar.php', 'workingDir.'/class.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_15'); + + $expectedNamespace = <<<'EOF' + array($baseDir . '/'), +); + +EOF; + + $expectedPsr4 = <<<'EOF' + array($baseDir . '/'), +); + +EOF; + + $expectedClassmap = <<<'EOF' + $baseDir . '/class.php', + 'Foo\\Bar' => $baseDir . '/Foo/Bar.php', +); + +EOF; + + $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')); + } + + public function testVendorSubstringPath() + { + $package = new Package('a', '1.0', '1.0'); + $package->setAutoload(array( + 'psr-0' => array('Foo' => 'composer-test-autoload-src/src'), + 'psr-4' => array('Acme\Foo\\' => 'composer-test-autoload-src/src-psr4'), + )); + + $this->repository->expects($this->once()) + ->method('getCanonicalPackages') + ->will($this->returnValue(array())); + + $this->fs->ensureDirectoryExists($this->vendorDir.'/a'); + + $expectedNamespace = <<<'EOF' + array($baseDir . '/composer-test-autoload-src/src'), +); + +EOF; + + $expectedPsr4 = <<<'EOF' + array($baseDir . '/composer-test-autoload-src/src-psr4'), +); + +EOF; + + $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, 'VendorSubstring'); + $this->assertEquals($expectedNamespace, file_get_contents($this->vendorDir.'/composer/autoload_namespaces.php')); + $this->assertEquals($expectedPsr4, file_get_contents($this->vendorDir.'/composer/autoload_psr4.php')); + } + + private function assertAutoloadFiles($name, $dir, $type = 'namespaces') + { + $a = __DIR__.'/Fixtures/autoload_'.$name.'.php'; + $b = $dir.'/autoload_'.$type.'.php'; + $this->assertFileEquals($a, $b); } } diff --git a/tests/Composer/Test/Autoload/ClassLoaderTest.php b/tests/Composer/Test/Autoload/ClassLoaderTest.php new file mode 100644 index 000000000..eee942e42 --- /dev/null +++ b/tests/Composer/Test/Autoload/ClassLoaderTest.php @@ -0,0 +1,58 @@ +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) + { + $loader = new ClassLoader(); + $loader->add('Namespaced\\', __DIR__ . '/Fixtures'); + $loader->add('Pearlike_', __DIR__ . '/Fixtures'); + $loader->addPsr4('ShinyVendor\\ShinyPackage\\', __DIR__ . '/Fixtures'); + + if ($prependSeparator) { + $prepend = '\\'; + $message = "->loadClass() loads '$class'."; + } else { + $prepend = ''; + $message = "->loadClass() loads '\\$class', as required in PHP 5.3.0 - 5.3.2."; + } + + $loader->loadClass($prepend . $class); + $this->assertTrue(class_exists($class, false), $message); + } + + /** + * Provides arguments for ->testLoadClass(). + * + * @return array Array of parameter sets to test with. + */ + public function getLoadClassTests() + { + return array( + array('Namespaced\\Foo'), + array('Pearlike_Foo'), + array('ShinyVendor\\ShinyPackage\\SubNamespace\\Foo'), + // "Bar" would not work here, since it is defined in a ".inc" file, + // instead of a ".php" file. So, use "Baz" instead. + array('Namespaced\\Baz', true), + array('Pearlike_Bar', true), + array('ShinyVendor\\ShinyPackage\\SubNamespace\\Bar', true), + ); + } +} diff --git a/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php b/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php index 40d79849c..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 { @@ -55,6 +57,8 @@ class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase 'Foo\\LargeGap' => realpath(__DIR__).'/Fixtures/classmap/LargeGap.php', 'Foo\\MissingSpace' => realpath(__DIR__).'/Fixtures/classmap/MissingSpace.php', 'Foo\\StripNoise' => realpath(__DIR__).'/Fixtures/classmap/StripNoise.php', + 'Foo\\SlashedA' => realpath(__DIR__).'/Fixtures/classmap/BackslashLineEndingString.php', + 'Foo\\SlashedB' => realpath(__DIR__).'/Fixtures/classmap/BackslashLineEndingString.php', 'Unicode\\↑\\↑' => realpath(__DIR__).'/Fixtures/classmap/Unicode.php', )), array(__DIR__.'/Fixtures/template', array()), @@ -76,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( @@ -102,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 @@ -121,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/SubNamespace/Bar.php b/tests/Composer/Test/Autoload/Fixtures/SubNamespace/Bar.php new file mode 100644 index 000000000..74cd9d7f3 --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/SubNamespace/Bar.php @@ -0,0 +1,5 @@ + $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_classmap2.php b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap2.php index 6016af10e..9cc0484f1 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap2.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap2.php @@ -1,8 +1,8 @@ $baseDir . '/composersrc/foo.php', - 'Main\\Foo' => $baseDir . '/src/Main/Foo.php', + 'ClassMapFoo' => $vendorDir . '/composersrc/foo.php', + 'Main\\Foo' => $vendorDir . '/src/Main/Foo.php', ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap4.php b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap4.php index 8896cdfb8..ae8025544 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap4.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap4.php @@ -1,12 +1,12 @@ $baseDir . '/%vendorDir%/b/b/src/b.php', - 'ClassMapBaz' => $baseDir . '/%vendorDir%/b/b/lib/c.php', - 'ClassMapFoo' => $baseDir . '/%vendorDir%/a/a/src/a.php', + 'ClassMapBar' => $vendorDir . '/b/b/src/b.php', + 'ClassMapBaz' => $vendorDir . '/b/b/lib/c.php', + 'ClassMapFoo' => $vendorDir . '/a/a/src/a.php', ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap5.php b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap5.php index 0a1f5def8..71bbc004d 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap5.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap5.php @@ -1,12 +1,12 @@ $baseDir . '/%vendorDir%/b/b/test.php', - 'ClassMapBaz' => $baseDir . '/%vendorDir%/c/c/foo/test.php', - 'ClassMapFoo' => $baseDir . '/%vendorDir%/a/a/src/a.php', + 'ClassMapBar' => $vendorDir . '/b/b/test.php', + 'ClassMapBaz' => $vendorDir . '/c/c/foo/test.php', + 'ClassMapFoo' => $vendorDir . '/a/a/src/a.php', ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap6.php b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap6.php new file mode 100644 index 000000000..ef97fb501 --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap6.php @@ -0,0 +1,11 @@ + $baseDir . '/lib/rootbar.php', + 'ClassMapFoo' => $baseDir . '/src/rootfoo.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_files.php b/tests/Composer/Test/Autoload/Fixtures/autoload_files.php new file mode 100644 index 000000000..17225d047 --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_files.php @@ -0,0 +1,11 @@ + $baseDir . '/src/', - 'Lala' => array($baseDir . '/src/', $baseDir . '/lib/'), + 'Main' => array($baseDir . '/src'), + 'Lala' => array($baseDir . '/src', $baseDir . '/lib'), ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_main2.php b/tests/Composer/Test/Autoload/Fixtures/autoload_main2.php index d857655af..93a2ec130 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_main2.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_main2.php @@ -1,11 +1,11 @@ $baseDir . '/src/', - 'Lala' => $baseDir . '/src/', + 'Main' => array($baseDir . '/src'), + 'Lala' => array($baseDir . '/src'), ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_main3.php b/tests/Composer/Test/Autoload/Fixtures/autoload_main3.php index 7fb27d0bd..c5a2ab17d 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_main3.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_main3.php @@ -1,11 +1,11 @@ $baseDir . '/src/', - 'Lala' => $baseDir . '/src/', + 'Main' => array($vendorDir . '/src'), + 'Lala' => array($vendorDir . '/src'), ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_main4.php b/tests/Composer/Test/Autoload/Fixtures/autoload_main4.php new file mode 100644 index 000000000..e6032e67e --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_main4.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_psr4.php b/tests/Composer/Test/Autoload/Fixtures/autoload_psr4.php new file mode 100644 index 000000000..78b609869 --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_psr4.php @@ -0,0 +1,11 @@ + array($baseDir . '/src-fruit'), + 'Acme\\Cake\\' => array($baseDir . '/src-cake', $baseDir . '/lib-cake'), +); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_psr4_2.php b/tests/Composer/Test/Autoload/Fixtures/autoload_psr4_2.php new file mode 100644 index 000000000..ab9ca2f54 --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_psr4_2.php @@ -0,0 +1,11 @@ + array($baseDir . '/src-fruit'), + 'Acme\\Cake\\' => array($baseDir . '/src-cake', $baseDir . '/lib-cake'), +); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_psr4_3.php b/tests/Composer/Test/Autoload/Fixtures/autoload_psr4_3.php new file mode 100644 index 000000000..a903b17b8 --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_psr4_3.php @@ -0,0 +1,11 @@ + array($vendorDir . '/src-fruit'), + 'Acme\\Cake\\' => array($vendorDir . '/src-cake', $vendorDir . '/lib-cake'), +); 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 6434b76dd..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 @@ -1,6 +1,6 @@ $path) { - $loader->add($namespace, $path); + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; @@ -36,15 +38,18 @@ class ComposerAutoloaderInitFilesAutoloadOrder $loader->addClassMap($classMap); } - $loader->register(); + $loader->register(true); - require $vendorDir . '/c/lorem/testC.php'; - require $vendorDir . '/z/foo/testA.php'; - require $vendorDir . '/d/d/testD.php'; - require $vendorDir . '/b/bar/testB.php'; - require $vendorDir . '/e/e/testE.php'; - require $baseDir . '/root.php'; + $includeFiles = require __DIR__ . '/autoload_files.php'; + foreach ($includeFiles as $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 a25e920db..1c0154964 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php @@ -1,6 +1,6 @@ $path) { - $loader->add($namespace, $path); + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; @@ -36,12 +38,18 @@ class ComposerAutoloaderInitFilesAutoload $loader->addClassMap($classMap); } - $loader->register(); + $loader->register(true); - require $vendorDir . '/a/a/test.php'; - require $vendorDir . '/b/b/test2.php'; - require $baseDir . '/root.php'; + $includeFiles = require __DIR__ . '/autoload_files.php'; + foreach ($includeFiles as $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 new file mode 100644 index 000000000..65ba6819e --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_include_path.php @@ -0,0 +1,71 @@ + $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + + $loader->setUseIncludePath(true); + spl_autoload_register(array('ComposerAutoloaderInitIncludePath', 'autoload'), true, true); + + $loader->register(true); + + return $loader; + } + + public static function autoload($class) + { + $dir = dirname(dirname(__DIR__)) . '/'; + $prefixes = array('Main\\Foo', 'Main\\Bar'); + foreach ($prefixes as $prefix) { + if (0 !== strpos($class, $prefix)) { + continue; + } + $path = $dir . implode('/', array_slice(explode('\\', $class), 2)).'.php'; + if (!$path = stream_resolve_include_path($path)) { + return false; + } + require $path; + + return true; + } + } +} + +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 ec8e392be..dc786f767 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_target_dir.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_target_dir.php @@ -1,6 +1,6 @@ $path) { - $loader->add($namespace, $path); + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; @@ -36,9 +38,14 @@ class ComposerAutoloaderInitTargetDir $loader->addClassMap($classMap); } - spl_autoload_register(array('ComposerAutoloaderInitTargetDir', 'autoload')); + spl_autoload_register(array('ComposerAutoloaderInitTargetDir', 'autoload'), true, true); - $loader->register(); + $loader->register(true); + + $includeFiles = require __DIR__ . '/autoload_files.php'; + foreach ($includeFiles as $file) { + composerRequireTargetDir($file); + } return $loader; } @@ -61,3 +68,8 @@ class ComposerAutoloaderInitTargetDir } } } + +function composerRequireTargetDir($file) +{ + require $file; +} diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_target_dir.php b/tests/Composer/Test/Autoload/Fixtures/autoload_target_dir.php index aa4b6725d..63ef49f61 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_target_dir.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_target_dir.php @@ -1,6 +1,6 @@ $vendorDir . '/b/b/src/', - 'A\\B' => $vendorDir . '/a/a/lib/', - 'A' => $vendorDir . '/a/a/src/', + 'B\\Sub\\Name' => array($vendorDir . '/b/b/src'), + 'A\\B' => array($vendorDir . '/a/a/lib'), + 'A' => array($vendorDir . '/a/a/src'), ); diff --git a/tests/Composer/Test/Autoload/Fixtures/classmap/BackslashLineEndingString.php b/tests/Composer/Test/Autoload/Fixtures/classmap/BackslashLineEndingString.php new file mode 100644 index 000000000..6c8b94c9b --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/classmap/BackslashLineEndingString.php @@ -0,0 +1,16 @@ + array +( + 'handler' => array ('midgard_admin_asgard_handler_preferences', 'ajax'), + 'fixed_args' => array('preferences', 'ajax'), + 'variable_args' => 0, +), diff --git a/tests/Composer/Test/Autoload/Fixtures/template/template_3.php b/tests/Composer/Test/Autoload/Fixtures/template/template_3.php index 5a0d0e38b..7f20be82f 100644 --- a/tests/Composer/Test/Autoload/Fixtures/template/template_3.php +++ b/tests/Composer/Test/Autoload/Fixtures/template/template_3.php @@ -7,4 +7,4 @@ class inner { } -class trailing { } \ No newline at end of file +class trailing { } diff --git a/tests/Composer/Test/CacheTest.php b/tests/Composer/Test/CacheTest.php new file mode 100644 index 000000000..80c7de6bf --- /dev/null +++ b/tests/Composer/Test/CacheTest.php @@ -0,0 +1,95 @@ + + * 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\Cache; +use Composer\TestCase; + +class CacheTest extends TestCase +{ + private $files, $root, $finder, $cache; + + public function setUp() + { + $this->root = sys_get_temp_dir() . '/composer_testdir'; + $this->ensureDirectoryExistsAndClear($this->root); + + $this->files = array(); + $zeros = str_repeat('0', 1000); + for ($i = 0; $i < 4; $i++) { + file_put_contents("{$this->root}/cached.file{$i}.zip", $zeros); + $this->files[] = new \SplFileInfo("{$this->root}/cached.file{$i}.zip"); + } + $this->finder = $this->getMockBuilder('Symfony\Component\Finder\Finder')->disableOriginalConstructor()->getMock(); + + $io = $this->getMock('Composer\IO\IOInterface'); + $this->cache = $this->getMock( + 'Composer\Cache', + array('getFinder'), + array($io, $this->root) + ); + $this->cache + ->expects($this->any()) + ->method('getFinder') + ->will($this->returnValue($this->finder)); + } + + public function testRemoveOutdatedFiles() + { + $outdated = array_slice($this->files, 1); + $this->finder + ->expects($this->once()) + ->method('getIterator') + ->will($this->returnValue(new \ArrayIterator($outdated))); + $this->finder + ->expects($this->once()) + ->method('date') + ->will($this->returnValue($this->finder)); + + $this->cache->gc(600, 1024 * 1024 * 1024); + + for ($i = 1; $i < 4; $i++) { + $this->assertFileNotExists("{$this->root}/cached.file{$i}.zip"); + } + $this->assertFileExists("{$this->root}/cached.file0.zip"); + } + + public function testRemoveFilesWhenCacheIsTooLarge() + { + $emptyFinder = $this->getMockBuilder('Symfony\Component\Finder\Finder')->disableOriginalConstructor()->getMock(); + $emptyFinder + ->expects($this->once()) + ->method('getIterator') + ->will($this->returnValue(new \EmptyIterator())); + + $this->finder + ->expects($this->once()) + ->method('date') + ->will($this->returnValue($emptyFinder)); + $this->finder + ->expects($this->once()) + ->method('getIterator') + ->will($this->returnValue(new \ArrayIterator($this->files))); + $this->finder + ->expects($this->once()) + ->method('sortByAccessedTime') + ->will($this->returnValue($this->finder)); + + $this->cache->gc(600, 1500); + + for ($i = 0; $i < 3; $i++) { + $this->assertFileNotExists("{$this->root}/cached.file{$i}.zip"); + } + $this->assertFileExists("{$this->root}/cached.file3.zip"); + } +} diff --git a/tests/Composer/Test/Command/InitCommandTest.php b/tests/Composer/Test/Command/InitCommandTest.php new file mode 100644 index 000000000..dbcbe0bda --- /dev/null +++ b/tests/Composer/Test/Command/InitCommandTest.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\Command; + +use Composer\Command\InitCommand; +use Composer\TestCase; + +class InitCommandTest extends TestCase +{ + public function testParseValidAuthorString() + { + $command = new InitCommand; + $author = $command->parseAuthorString('John Smith '); + $this->assertEquals('John Smith', $author['name']); + $this->assertEquals('john@example.com', $author['email']); + } + + public function testParseValidUtf8AuthorString() + { + $command = new InitCommand; + $author = $command->parseAuthorString('Matti Meikäläinen '); + $this->assertEquals('Matti Meikäläinen', $author['name']); + $this->assertEquals('matti@example.com', $author['email']); + } + + public function testParseEmptyAuthorString() + { + $command = new InitCommand; + $this->setExpectedException('InvalidArgumentException'); + $command->parseAuthorString(''); + } + + public function testParseAuthorStringWithInvalidEmail() + { + $command = new InitCommand; + $this->setExpectedException('InvalidArgumentException'); + $command->parseAuthorString('John Smith '); + } +} diff --git a/tests/Composer/Test/ComposerTest.php b/tests/Composer/Test/ComposerTest.php index c23488251..df00c36f7 100644 --- a/tests/Composer/Test/ComposerTest.php +++ b/tests/Composer/Test/ComposerTest.php @@ -1,4 +1,5 @@ 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 fc714c9bc..d0b78f3e3 100644 --- a/tests/Composer/Test/Config/JsonConfigSourceTest.php +++ b/tests/Composer/Test/Config/JsonConfigSourceTest.php @@ -16,11 +16,6 @@ use Composer\Config\JsonConfigSource; use Composer\Json\JsonFile; use Composer\Util\Filesystem; -/** - * JsonConfigSource Test - * - * @author Beau Simensen - */ class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase { private $workingDir; @@ -68,7 +63,6 @@ class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase $twoOfEverything = $this->fixturePath('composer-two-of-everything.json'); return array( - $this->addLinkDataArguments('require', 'my-vend/my-lib', '1.*', 'require-from-empty', $empty), $this->addLinkDataArguments('require', 'my-vend/my-lib', '1.*', 'require-from-oneOfEverything', $oneOfEverything), $this->addLinkDataArguments('require', 'my-vend/my-lib', '1.*', 'require-from-twoOfEverything', $twoOfEverything), @@ -125,7 +119,6 @@ class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase $name, $after ?: $this->fixturePath('removeLink/'.$fixtureBasename.'-after.json'), ); - } /** diff --git a/tests/Composer/Test/ConfigTest.php b/tests/Composer/Test/ConfigTest.php index 22ac59830..705fe78e0 100644 --- a/tests/Composer/Test/ConfigTest.php +++ b/tests/Composer/Test/ConfigTest.php @@ -35,7 +35,7 @@ class ConfigTest extends \PHPUnit_Framework_TestCase $data = array(); $data['local config inherits system defaults'] = array( array( - 'packagist' => array('type' => 'composer', 'url' => 'https?://packagist.org') + 'packagist' => array('type' => 'composer', 'url' => 'https?://packagist.org', 'allow_ssl_downgrade' => true) ), array(), ); @@ -51,7 +51,7 @@ class ConfigTest extends \PHPUnit_Framework_TestCase array( 1 => array('type' => 'vcs', 'url' => 'git://github.com/composer/composer.git'), 0 => array('type' => 'pear', 'url' => 'http://pear.composer.org'), - 'packagist' => array('type' => 'composer', 'url' => 'https?://packagist.org'), + 'packagist' => array('type' => 'composer', 'url' => 'https?://packagist.org', 'allow_ssl_downgrade' => true), ), array( array('type' => 'vcs', 'url' => 'git://github.com/composer/composer.git'), @@ -62,7 +62,7 @@ class ConfigTest extends \PHPUnit_Framework_TestCase $data['system config adds above core defaults'] = array( array( 'example.com' => array('type' => 'composer', 'url' => 'http://example.com'), - 'packagist' => array('type' => 'composer', 'url' => 'https?://packagist.org') + 'packagist' => array('type' => 'composer', 'url' => 'https?://packagist.org', 'allow_ssl_downgrade' => true) ), array(), array( @@ -109,12 +109,24 @@ class ConfigTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array('foo' => 'bar', 'bar' => 'baz'), $config->get('github-oauth')); } + public function testVarReplacement() + { + $config = new Config(); + $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 testOverrideGithubProtocols() { $config = new Config(); - $config->merge(array('config' => array('github-protocols' => array('https', 'http')))); - $config->merge(array('config' => array('github-protocols' => array('http')))); + $config->merge(array('config' => array('github-protocols' => array('https', 'git')))); + $config->merge(array('config' => array('github-protocols' => array('https')))); - $this->assertEquals(array('http'), $config->get('github-protocols')); + $this->assertEquals(array('https'), $config->get('github-protocols')); } } diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index 7a1a6be58..9e9952228 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -19,7 +19,7 @@ use Composer\DependencyResolver\Pool; use Composer\Package\Link; use Composer\Package\AliasPackage; use Composer\Package\LinkConstraint\VersionConstraint; -use Composer\Test\TestCase; +use Composer\TestCase; class DefaultPolicyTest extends TestCase { @@ -65,6 +65,49 @@ class DefaultPolicyTest extends TestCase $this->assertEquals($expected, $selected); } + public function testSelectNewestPicksLatest() + { + $this->repo->addPackage($packageA1 = $this->getPackage('A', '1.0.0')); + $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.1-alpha')); + $this->pool->addRepository($this->repo); + + $literals = array($packageA1->getId(), $packageA2->getId()); + $expected = array($packageA2->getId()); + + $selected = $this->policy->selectPreferedPackages($this->pool, array(), $literals); + + $this->assertEquals($expected, $selected); + } + + public function testSelectNewestPicksLatestStableWithPreferStable() + { + $this->repo->addPackage($packageA1 = $this->getPackage('A', '1.0.0')); + $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.1-alpha')); + $this->pool->addRepository($this->repo); + + $literals = array($packageA1->getId(), $packageA2->getId()); + $expected = array($packageA1->getId()); + + $policy = new DefaultPolicy(true); + $selected = $policy->selectPreferedPackages($this->pool, array(), $literals); + + $this->assertEquals($expected, $selected); + } + + public function testSelectNewestWithDevPicksNonDev() + { + $this->repo->addPackage($packageA1 = $this->getPackage('A', 'dev-foo')); + $this->repo->addPackage($packageA2 = $this->getPackage('A', '1.0.0')); + $this->pool->addRepository($this->repo); + + $literals = array($packageA1->getId(), $packageA2->getId()); + $expected = array($packageA2->getId()); + + $selected = $this->policy->selectPreferedPackages($this->pool, array(), $literals); + + $this->assertEquals($expected, $selected); + } + public function testSelectNewestOverInstalled() { $this->repo->addPackage($packageA = $this->getPackage('A', '2.0')); @@ -148,7 +191,6 @@ class DefaultPolicyTest extends TestCase public function testPreferNonReplacingFromSameRepo() { - $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageB = $this->getPackage('B', '2.0')); @@ -164,6 +206,38 @@ class DefaultPolicyTest extends TestCase $this->assertEquals($expected, $selected); } + public function testPreferReplacingPackageFromSameVendor() + { + // test with default order + $this->repo->addPackage($packageB = $this->getPackage('vendor-b/replacer', '1.0')); + $this->repo->addPackage($packageA = $this->getPackage('vendor-a/replacer', '1.0')); + + $packageA->setReplaces(array(new Link('vendor-a/replacer', 'vendor-a/package', new VersionConstraint('==', '1.0'), 'replaces'))); + $packageB->setReplaces(array(new Link('vendor-b/replacer', 'vendor-a/package', new VersionConstraint('==', '1.0'), 'replaces'))); + + $this->pool->addRepository($this->repo); + + $literals = array($packageA->getId(), $packageB->getId()); + $expected = $literals; + + $selected = $this->policy->selectPreferedPackages($this->pool, array(), $literals, 'vendor-a/package'); + $this->assertEquals($expected, $selected); + + // test with reversed order in repo + $repo = new ArrayRepository; + $repo->addPackage($packageA = clone $packageA); + $repo->addPackage($packageB = clone $packageB); + + $pool = new Pool('dev'); + $pool->addRepository($this->repo); + + $literals = array($packageA->getId(), $packageB->getId()); + $expected = $literals; + + $selected = $this->policy->selectPreferedPackages($this->pool, array(), $literals, 'vendor-a/package'); + $this->assertEquals($expected, $selected); + } + protected function mapFromRepo(RepositoryInterface $repo) { $map = array(); diff --git a/tests/Composer/Test/DependencyResolver/PoolTest.php b/tests/Composer/Test/DependencyResolver/PoolTest.php index aa38fa31d..14b24fc9f 100644 --- a/tests/Composer/Test/DependencyResolver/PoolTest.php +++ b/tests/Composer/Test/DependencyResolver/PoolTest.php @@ -15,7 +15,7 @@ namespace Composer\Test\DependencyResolver; use Composer\DependencyResolver\Pool; use Composer\Repository\ArrayRepository; use Composer\Package\BasePackage; -use Composer\Test\TestCase; +use Composer\TestCase; class PoolTest extends TestCase { diff --git a/tests/Composer/Test/DependencyResolver/RequestTest.php b/tests/Composer/Test/DependencyResolver/RequestTest.php index 89639bc44..0ba43ca73 100644 --- a/tests/Composer/Test/DependencyResolver/RequestTest.php +++ b/tests/Composer/Test/DependencyResolver/RequestTest.php @@ -15,7 +15,7 @@ namespace Composer\Test\DependencyResolver; use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Pool; use Composer\Repository\ArrayRepository; -use Composer\Test\TestCase; +use Composer\TestCase; class RequestTest extends TestCase { @@ -39,9 +39,9 @@ class RequestTest extends TestCase $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), + array('cmd' => 'install', 'packageName' => 'bar', 'constraint' => null), + array('cmd' => 'remove', 'packageName' => 'foobar', 'constraint' => null), ), $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), ), $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 10ec17501..1598e7717 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php @@ -17,7 +17,7 @@ use Composer\DependencyResolver\RuleSet; use Composer\DependencyResolver\RuleSetIterator; use Composer\DependencyResolver\Pool; -class ResultSetIteratorTest extends \PHPUnit_Framework_TestCase +class RuleSetIteratorTest extends \PHPUnit_Framework_TestCase { protected $rules; diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index a6b108500..35e3f17d6 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -16,7 +16,7 @@ use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\RuleSet; use Composer\DependencyResolver\Pool; use Composer\Repository\ArrayRepository; -use Composer\Test\TestCase; +use Composer\TestCase; class RuleSetTest extends TestCase { diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index 8d4c732a2..10667632d 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -15,7 +15,7 @@ namespace Composer\Test\DependencyResolver; use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\Pool; use Composer\Repository\ArrayRepository; -use Composer\Test\TestCase; +use Composer\TestCase; class RuleTest extends TestCase { diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 5f8c33936..ac78b5a26 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -18,7 +18,7 @@ use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Solver; use Composer\DependencyResolver\SolverProblemsException; use Composer\Package\Link; -use Composer\Test\TestCase; +use Composer\TestCase; use Composer\Package\LinkConstraint\MultiConstraint; class SolverTest extends TestCase @@ -75,6 +75,7 @@ class SolverTest extends TestCase } catch (SolverProblemsException $e) { $problems = $e->getProblems(); $this->assertEquals(1, count($problems)); + $this->assertEquals(2, $e->getCode()); $this->assertEquals("\n - The requested package b could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString()); } } @@ -404,7 +405,7 @@ class SolverTest extends TestCase { $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); - $packageB->setReplaces(array('a' => new Link('B', 'A', null))); + $packageB->setReplaces(array('a' => new Link('B', 'A', new MultiConstraint(array())))); $this->reposComplete(); @@ -440,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() @@ -464,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')); @@ -475,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() @@ -573,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), )); } @@ -610,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), @@ -674,9 +674,9 @@ class SolverTest extends TestCase $msg = "\n"; $msg .= " Problem 1\n"; - $msg .= " - Installation request for a -> satisfiable by A 1.0.\n"; - $msg .= " - B 1.0 conflicts with A 1.0.\n"; - $msg .= " - Installation request for b -> satisfiable by B 1.0.\n"; + $msg .= " - Installation request for a -> satisfiable by A[1.0].\n"; + $msg .= " - B 1.0 conflicts with A[1.0].\n"; + $msg .= " - Installation request for b -> satisfiable by B[1.0].\n"; $this->assertEquals($msg, $e->getMessage()); } } @@ -704,7 +704,7 @@ class SolverTest extends TestCase $msg = "\n"; $msg .= " Problem 1\n"; - $msg .= " - Installation request for a -> satisfiable by A 1.0.\n"; + $msg .= " - Installation request for a -> satisfiable by A[1.0].\n"; $msg .= " - A 1.0 requires b >= 2.0 -> no matching package found.\n\n"; $msg .= "Potential causes:\n"; $msg .= " - A typo in the package name\n"; @@ -749,12 +749,12 @@ class SolverTest extends TestCase $msg = "\n"; $msg .= " Problem 1\n"; - $msg .= " - C 1.0 requires d >= 1.0 -> satisfiable by D 1.0.\n"; - $msg .= " - D 1.0 requires b < 1.0 -> satisfiable by B 0.9.\n"; - $msg .= " - B 1.0 requires c >= 1.0 -> satisfiable by C 1.0.\n"; - $msg .= " - Can only install one of: B 0.9, B 1.0.\n"; - $msg .= " - A 1.0 requires b >= 1.0 -> satisfiable by B 1.0.\n"; - $msg .= " - Installation request for a -> satisfiable by A 1.0.\n"; + $msg .= " - C 1.0 requires d >= 1.0 -> satisfiable by D[1.0].\n"; + $msg .= " - D 1.0 requires b < 1.0 -> satisfiable by B[0.9].\n"; + $msg .= " - B 1.0 requires c >= 1.0 -> satisfiable by C[1.0].\n"; + $msg .= " - Can only install one of: B[0.9, 1.0].\n"; + $msg .= " - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n"; + $msg .= " - Installation request for a -> satisfiable by A[1.0].\n"; $this->assertEquals($msg, $e->getMessage()); } } diff --git a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php index 9fd1efb0d..aecc45604 100644 --- a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php @@ -33,6 +33,10 @@ class ArchiveDownloaderTest extends \PHPUnit_Framework_TestCase public function testProcessUrl() { + if (!extension_loaded('openssl')) { + $this->markTestSkipped('Requires openssl'); + } + $downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface'), $this->getMock('Composer\Config'))); $method = new \ReflectionMethod($downloader, 'processUrl'); $method->setAccessible(true); @@ -40,15 +44,15 @@ class ArchiveDownloaderTest extends \PHPUnit_Framework_TestCase $expected = 'https://github.com/composer/composer/zipball/master'; $url = $method->invoke($downloader, $this->getMock('Composer\Package\PackageInterface'), $expected); - if (extension_loaded('openssl')) { - $this->assertEquals($expected, $url); - } else { - $this->assertEquals('http://nodeload.github.com/composer/composer/zip/master', $url); - } + $this->assertEquals($expected, $url); } public function testProcessUrl2() { + if (!extension_loaded('openssl')) { + $this->markTestSkipped('Requires openssl'); + } + $downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface'), $this->getMock('Composer\Config'))); $method = new \ReflectionMethod($downloader, 'processUrl'); $method->setAccessible(true); @@ -56,15 +60,15 @@ class ArchiveDownloaderTest extends \PHPUnit_Framework_TestCase $expected = 'https://github.com/composer/composer/archive/master.tar.gz'; $url = $method->invoke($downloader, $this->getMock('Composer\Package\PackageInterface'), $expected); - if (extension_loaded('openssl')) { - $this->assertEquals($expected, $url); - } else { - $this->assertEquals('http://nodeload.github.com/composer/composer/tar.gz/master', $url); - } + $this->assertEquals($expected, $url); } public function testProcessUrl3() { + if (!extension_loaded('openssl')) { + $this->markTestSkipped('Requires openssl'); + } + $downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface'), $this->getMock('Composer\Config'))); $method = new \ReflectionMethod($downloader, 'processUrl'); $method->setAccessible(true); @@ -72,11 +76,7 @@ class ArchiveDownloaderTest extends \PHPUnit_Framework_TestCase $expected = 'https://api.github.com/repos/composer/composer/zipball/master'; $url = $method->invoke($downloader, $this->getMock('Composer\Package\PackageInterface'), $expected); - if (extension_loaded('openssl')) { - $this->assertEquals($expected, $url); - } else { - $this->assertEquals('http://nodeload.github.com/composer/composer/zip/master', $url); - } + $this->assertEquals($expected, $url); } /** @@ -84,6 +84,10 @@ class ArchiveDownloaderTest extends \PHPUnit_Framework_TestCase */ public function testProcessUrlRewriteDist($url) { + if (!extension_loaded('openssl')) { + $this->markTestSkipped('Requires openssl'); + } + $downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface'), $this->getMock('Composer\Config'))); $method = new \ReflectionMethod($downloader, 'processUrl'); $method->setAccessible(true); @@ -97,11 +101,7 @@ class ArchiveDownloaderTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('ref')); $url = $method->invoke($downloader, $package, $url); - if (extension_loaded('openssl')) { - $this->assertEquals($expected, $url); - } else { - $this->assertEquals('http://nodeload.github.com/composer/composer/'.$type.'/ref', $url); - } + $this->assertEquals($expected, $url); } public function provideUrls() 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 83a1c7b5f..f0578f6be 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -17,13 +17,13 @@ use Composer\Util\Filesystem; class FileDownloaderTest extends \PHPUnit_Framework_TestCase { - protected function getDownloader($io = null, $config = null, $rfs = null) + protected function getDownloader($io = null, $config = null, $eventDispatcher = null, $cache = null, $rfs = null, $filesystem = null) { $io = $io ?: $this->getMock('Composer\IO\IOInterface'); $config = $config ?: $this->getMock('Composer\Config'); $rfs = $rfs ?: $this->getMockBuilder('Composer\Util\RemoteFilesystem')->disableOriginalConstructor()->getMock(); - return new FileDownloader($io, $config, null, $rfs); + return new FileDownloader($io, $config, $eventDispatcher, $cache, $rfs, $filesystem); } /** @@ -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,17 +91,25 @@ 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 { - $path = sys_get_temp_dir().'/'.md5(time().rand()); + $path = sys_get_temp_dir().'/'.md5(time().mt_rand()); } while (file_exists($path)); $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'); } @@ -123,23 +135,63 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase } } + public function testCacheGarbageCollectionIsCalled() + { + $expectedTtl = '99999999'; + + $configMock = $this->getMock('Composer\Config'); + $configMock + ->expects($this->at(0)) + ->method('get') + ->with('cache-files-ttl') + ->will($this->returnValue($expectedTtl)); + $configMock + ->expects($this->at(1)) + ->method('get') + ->with('cache-files-maxsize') + ->will($this->returnValue('500M')); + + $cacheMock = $this->getMockBuilder('Composer\Cache') + ->disableOriginalConstructor() + ->getMock(); + $cacheMock + ->expects($this->any()) + ->method('gcIsNecessary') + ->will($this->returnValue(true)); + $cacheMock + ->expects($this->once()) + ->method('gc') + ->with($expectedTtl, $this->anything()); + + $downloader = $this->getDownloader(null, $configMock, null, $cacheMock, null, null); + } + public function testDownloadFileWithInvalidChecksum() { $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 { - $path = sys_get_temp_dir().'/'.md5(time().rand()); + $path = sys_get_temp_dir().'/'.md5(time().mt_rand()); } while (file_exists($path)); - $downloader = $this->getDownloader(); + $downloader = $this->getDownloader(null, null, null, null, null, $filesystem); // make sure the file expected to be downloaded is on disk already mkdir($path, 0777, true); diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 692534cd7..194fc96c9 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -14,6 +14,7 @@ namespace Composer\Test\Downloader; use Composer\Downloader\GitDownloader; use Composer\Config; +use Composer\Util\Filesystem; class GitDownloaderTest extends \PHPUnit_Framework_TestCase { @@ -23,10 +24,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase $executor = $executor ?: $this->getMock('Composer\Util\ProcessExecutor'); $filesystem = $filesystem ?: $this->getMock('Composer\Util\Filesystem'); if (!$config) { - $config = $this->getMock('Composer\Config'); - $config->expects($this->any()) - ->method('has') - ->will($this->returnValue(false)); + $config = new Config(); } return new GitDownloader($io, $config, $executor, $filesystem); @@ -53,14 +51,14 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->method('getSourceReference') ->will($this->returnValue('1234567890123456789012345678901234567890')); $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'))); $packageMock->expects($this->any()) ->method('getPrettyVersion') ->will($this->returnValue('dev-master')); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); - $expectedGitCommand = $this->getCmd("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)) @@ -68,17 +66,17 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase $processExecutor->expects($this->at(1)) ->method('execute') - ->with($this->equalTo($this->getCmd("git branch -r")), $this->equalTo(null), $this->equalTo('composerPath')) + ->with($this->equalTo($this->winCompat("git branch -r")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(2)) ->method('execute') - ->with($this->equalTo($this->getCmd("git checkout 'master'")), $this->equalTo(null), $this->equalTo('composerPath')) + ->with($this->equalTo($this->winCompat("git checkout 'master'")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(3)) ->method('execute') - ->with($this->equalTo($this->getCmd("git reset --hard '1234567890123456789012345678901234567890'")), $this->equalTo(null), $this->equalTo('composerPath')) + ->with($this->equalTo($this->winCompat("git reset --hard '1234567890123456789012345678901234567890'")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, null, $processExecutor); @@ -92,77 +90,88 @@ 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')); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); - $expectedGitCommand = $this->getCmd("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/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'git://github.com/composer/composer' && git fetch composer"); $processExecutor->expects($this->at(0)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) ->will($this->returnValue(1)); - $expectedGitCommand = $this->getCmd("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/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://github.com/composer/composer' && git fetch composer"); $processExecutor->expects($this->at(2)) - ->method('execute') - ->with($this->equalTo($expectedGitCommand)) - ->will($this->returnValue(1)); - - $expectedGitCommand = $this->getCmd("git clone 'http://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'http://github.com/composer/composer' && git fetch composer"); - $processExecutor->expects($this->at(4)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) ->will($this->returnValue(0)); - $expectedGitCommand = $this->getCmd("git remote set-url --push origin 'git@github.com:composer/composer.git'"); - $processExecutor->expects($this->at(5)) + $expectedGitCommand = $this->winCompat("git remote set-url --push origin 'git@github.com:composer/composer.git'"); + $processExecutor->expects($this->at(3)) ->method('execute') - ->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo('composerPath')) + ->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); - $processExecutor->expects($this->at(6)) + $processExecutor->expects($this->at(4)) ->method('execute') ->with($this->equalTo('git branch -r')) ->will($this->returnValue(0)); - $processExecutor->expects($this->at(7)) + $processExecutor->expects($this->at(5)) ->method('execute') - ->with($this->equalTo($this->getCmd("git checkout 'ref' && git reset --hard 'ref'")), $this->equalTo(null), $this->equalTo('composerPath')) + ->with($this->equalTo($this->winCompat("git checkout 'ref' && git reset --hard 'ref'")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); $downloader->download($packageMock, 'composerPath'); } - public function testDownloadUsesCustomVariousProtocolsForGithub() + public function pushUrlProvider() + { + return array( + array('git', 'git@github.com:composer/composer.git'), + array('https', 'https://github.com/composer/composer.git'), + ); + } + + /** + * @dataProvider pushUrlProvider + */ + public function testDownloadAndSetPushUrlUseCustomVariousProtocolsForGithub($protocol, $pushUrl) { $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://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')); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); - $expectedGitCommand = $this->getCmd("git clone 'http://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'http://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)) ->will($this->returnValue(0)); + $expectedGitCommand = $this->winCompat("git remote set-url --push origin '{$pushUrl}'"); + $processExecutor->expects($this->at(1)) + ->method('execute') + ->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) + ->will($this->returnValue(0)); + $processExecutor->expects($this->exactly(4)) ->method('execute') ->will($this->returnValue(0)); $config = new Config(); - $config->merge(array('config' => array('github-protocols' => array('http')))); + $config->merge(array('config' => array('github-protocols' => array($protocol)))); $downloader = $this->getDownloaderMock(null, $config, $processExecutor); $downloader->download($packageMock, 'composerPath'); @@ -173,14 +182,14 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase */ public function testDownloadThrowsRuntimeExceptionIfGitCommandFails() { - $expectedGitCommand = $this->getCmd("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') @@ -208,38 +217,45 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase public function testUpdate() { - $expectedGitUpdateCommand = $this->getCmd("cd 'composerPath' && git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer"); + $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer"); + $tmpDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.md5(uniqid('', true)); + $fs = new Filesystem; + $fs->ensureDirectoryExists($tmpDir.'/.git'); $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://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')); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); $processExecutor->expects($this->at(0)) ->method('execute') - ->with($this->equalTo($this->getCmd("cd 'composerPath' && git remote -v"))) + ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(1)) ->method('execute') - ->with($this->equalTo($expectedGitUpdateCommand)) + ->with($this->equalTo($this->winCompat("git remote -v"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(2)) ->method('execute') - ->with($this->equalTo('git branch -r')) + ->with($this->equalTo($expectedGitUpdateCommand)) ->will($this->returnValue(0)); $processExecutor->expects($this->at(3)) ->method('execute') - ->with($this->equalTo($this->getCmd("git checkout 'ref' && git reset --hard 'ref'")), $this->equalTo(null), $this->equalTo('composerPath')) + ->with($this->equalTo('git branch -r')) + ->will($this->returnValue(0)); + $processExecutor->expects($this->at(4)) + ->method('execute') + ->with($this->equalTo($this->winCompat("git checkout 'ref' && git reset --hard 'ref'")), $this->equalTo(null), $this->equalTo($this->winCompat($tmpDir))) ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); - $downloader->update($packageMock, $packageMock, 'composerPath'); + $downloader->update($packageMock, $packageMock, $tmpDir); } /** @@ -247,32 +263,39 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase */ public function testUpdateThrowsRuntimeExceptionIfGitCommandFails() { - $expectedGitUpdateCommand = $this->getCmd("cd 'composerPath' && git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer"); + $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer"); + $tmpDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.md5(uniqid('', true)); + $fs = new Filesystem; + $fs->ensureDirectoryExists($tmpDir.'/.git'); $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://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') - ->with($this->equalTo($this->getCmd("cd 'composerPath' && git remote -v"))) + ->with($this->equalTo($this->winCompat("git status --porcelain --untracked-files=no"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(1)) + ->method('execute') + ->with($this->equalTo($this->winCompat("git remote -v"))) + ->will($this->returnValue(0)); + $processExecutor->expects($this->at(2)) ->method('execute') ->with($this->equalTo($expectedGitUpdateCommand)) ->will($this->returnValue(1)); $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); - $downloader->update($packageMock, $packageMock, 'composerPath'); + $downloader->update($packageMock, $packageMock, $tmpDir); } public function testRemove() { - $expectedGitResetCommand = $this->getCmd("cd 'composerPath' && git status --porcelain --untracked-files=no"); + $expectedGitResetCommand = $this->winCompat("cd 'composerPath' && git status --porcelain --untracked-files=no"); $packageMock = $this->getMock('Composer\Package\PackageInterface'); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); @@ -297,9 +320,12 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals('source', $downloader->getInstallationSource()); } - private function getCmd($cmd) + private function winCompat($cmd) { if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $cmd = str_replace('cd ', 'cd /D ', $cmd); + $cmd = str_replace('composerPath', getcwd().'/composerPath', $cmd); + return strtr($cmd, "'", '"'); } diff --git a/tests/Composer/Test/Downloader/HgDownloaderTest.php b/tests/Composer/Test/Downloader/HgDownloaderTest.php index 46be59db2..ab9ec28cd 100644 --- a/tests/Composer/Test/Downloader/HgDownloaderTest.php +++ b/tests/Composer/Test/Downloader/HgDownloaderTest.php @@ -13,6 +13,7 @@ namespace Composer\Test\Downloader; use Composer\Downloader\HgDownloader; +use Composer\Util\Filesystem; class HgDownloaderTest extends \PHPUnit_Framework_TestCase { @@ -42,16 +43,23 @@ class HgDownloaderTest extends \PHPUnit_Framework_TestCase public function testDownload() { - $expectedGitCommand = $this->getCmd('hg clone \'https://mercurial.dev/l3l0/composer\' \'composerPath\' && cd \'composerPath\' && hg up \'ref\''); $packageMock = $this->getMock('Composer\Package\PackageInterface'); $packageMock->expects($this->any()) ->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'); - $processExecutor->expects($this->once()) + + $expectedGitCommand = $this->getCmd('hg clone \'https://mercurial.dev/l3l0/composer\' \'composerPath\''); + $processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedGitCommand)) + ->will($this->returnValue(0)); + + $expectedGitCommand = $this->getCmd('hg up \'ref\''); + $processExecutor->expects($this->at(1)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) ->will($this->returnValue(0)); @@ -77,23 +85,31 @@ class HgDownloaderTest extends \PHPUnit_Framework_TestCase public function testUpdate() { - $expectedUpdateCommand = $this->getCmd("cd 'composerPath' && hg pull 'https://github.com/l3l0/composer' && hg up 'ref'"); - + $tmpDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.md5(uniqid('', true)); + $fs = new Filesystem; + $fs->ensureDirectoryExists($tmpDir.'/.hg'); $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://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"); $processExecutor->expects($this->at(0)) ->method('execute') - ->with($this->equalTo($expectedUpdateCommand)) + ->with($this->equalTo($expectedHgCommand)) + ->will($this->returnValue(0)); + $expectedHgCommand = $this->getCmd("hg pull 'https://github.com/l3l0/composer' && hg up 'ref'"); + $processExecutor->expects($this->at(1)) + ->method('execute') + ->with($this->equalTo($expectedHgCommand)) ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, null, $processExecutor); - $downloader->update($packageMock, $packageMock, 'composerPath'); + $downloader->update($packageMock, $packageMock, $tmpDir); } public function testRemove() diff --git a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php new file mode 100644 index 000000000..9e26689a9 --- /dev/null +++ b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php @@ -0,0 +1,160 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +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 +{ + + 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->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); + } + + 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) + { + $package = $this->getMock('Composer\Package\PackageInterface'); + $package->expects($this->any())->method('getRepository')->will($this->returnValue($repository)); + + return $package; + } + + 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() + { + $ref = 'SOURCE_REF'; + $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 899d10b49..58e0078b0 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -28,11 +28,23 @@ 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'); $config = $this->getMock('Composer\Config'); + $config->expects($this->any()) + ->method('get') + ->with('vendor-dir') + ->will($this->returnValue(sys_get_temp_dir().'/composer-zip-test-vendor')); $downloader = new ZipDownloader($io, $config); try { diff --git a/tests/Composer/Test/Script/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php similarity index 58% rename from tests/Composer/Test/Script/EventDispatcherTest.php rename to tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index 866d111b8..69e1de290 100644 --- a/tests/Composer/Test/Script/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -10,11 +10,13 @@ * file that was distributed with this source code. */ -namespace Composer\Test\Script; +namespace Composer\Test\EventDispatcher; -use Composer\Test\TestCase; -use Composer\Script\Event; -use Composer\Script\EventDispatcher; +use Composer\EventDispatcher\Event; +use Composer\EventDispatcher\EventDispatcher; +use Composer\Installer\InstallerEvents; +use Composer\TestCase; +use Composer\Script\ScriptEvents; use Composer\Util\ProcessExecutor; class EventDispatcherTest extends TestCase @@ -26,14 +28,14 @@ class EventDispatcherTest extends TestCase { $io = $this->getMock('Composer\IO\IOInterface'); $dispatcher = $this->getDispatcherStubForListenersTest(array( - "Composer\Test\Script\EventDispatcherTest::call" + "Composer\Test\EventDispatcher\EventDispatcherTest::call" ), $io); $io->expects($this->once()) ->method('write') - ->with('Script Composer\Test\Script\EventDispatcherTest::call handling the post-install-cmd event terminated with an exception'); + ->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); } /** @@ -43,7 +45,7 @@ class EventDispatcherTest extends TestCase public function testDispatcherCanExecuteSingleCommandLineScript($command) { $process = $this->getMock('Composer\Util\ProcessExecutor'); - $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher') + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->setConstructorArgs(array( $this->getMock('Composer\Composer'), $this->getMock('Composer\IO\IOInterface'), @@ -59,15 +61,16 @@ class EventDispatcherTest extends TestCase $process->expects($this->once()) ->method('execute') - ->with($command); + ->with($command) + ->will($this->returnValue(0)); - $dispatcher->dispatchCommandEvent("post-install-cmd", false); + $dispatcher->dispatchCommandEvent(ScriptEvents::POST_INSTALL_CMD, false); } public function testDispatcherCanExecuteCliAndPhpInSameEventScriptStack() { $process = $this->getMock('Composer\Util\ProcessExecutor'); - $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher') + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->setConstructorArgs(array( $this->getMock('Composer\Composer'), $this->getMock('Composer\IO\IOInterface'), @@ -80,11 +83,12 @@ class EventDispatcherTest extends TestCase ->getMock(); $process->expects($this->exactly(2)) - ->method('execute'); + ->method('execute') + ->will($this->returnValue(0)); $listeners = array( 'echo -n foo', - 'Composer\\Test\\Script\\EventDispatcherTest::someMethod', + 'Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod', 'echo -n bar', ); $dispatcher->expects($this->atLeastOnce()) @@ -93,15 +97,15 @@ class EventDispatcherTest extends TestCase $dispatcher->expects($this->once()) ->method('executeEventPhpScript') - ->with('Composer\Test\Script\EventDispatcherTest', 'someMethod') + ->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) { - $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher') + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->setConstructorArgs(array( $this->getMock('Composer\Composer'), $io, @@ -127,7 +131,7 @@ class EventDispatcherTest extends TestCase public function testDispatcherOutputsCommands() { - $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher') + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->setConstructorArgs(array( $this->getMock('Composer\Composer'), $this->getMock('Composer\IO\IOInterface'), @@ -142,13 +146,13 @@ 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())); } public function testDispatcherOutputsErrorOnFailedCommand() { - $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher') + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->setConstructorArgs(array( $this->getMock('Composer\Composer'), $io = $this->getMock('Composer\IO\IOInterface'), @@ -165,9 +169,35 @@ class EventDispatcherTest extends TestCase $io->expects($this->once()) ->method('write') - ->with($this->equalTo('Script '.$code.' handling the post-install-cmd event returned with an error: ')); + ->with($this->equalTo('Script '.$code.' handling the post-install-cmd event returned with an error')); + + $this->setExpectedException('RuntimeException'); + $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->dispatchCommandEvent("post-install-cmd", false); + $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() diff --git a/tests/Composer/Test/Fixtures/functional/create-project-command.test b/tests/Composer/Test/Fixtures/functional/create-project-command.test index 3bf035335..2e4b2762a 100644 --- a/tests/Composer/Test/Fixtures/functional/create-project-command.test +++ b/tests/Composer/Test/Fixtures/functional/create-project-command.test @@ -3,10 +3,10 @@ create-project seld/jsonlint %testDir% 1.0.0 --prefer-source -n --EXPECT-- Installing seld/jsonlint (1.0.0) - Installing seld/jsonlint (1.0.0) - Cloning 1.0.0 + Cloning 3b4bc2a96ff5d3fe6866bfe9dd0c845246705791 Created project in %testDir% Loading composer repositories with package information -Installing dependencies +Installing dependencies (including require-dev) Nothing to install or update Generating autoload files diff --git a/tests/Composer/Test/Fixtures/installer/alias-with-reference.test b/tests/Composer/Test/Fixtures/installer/alias-with-reference.test new file mode 100644 index 000000000..d1609ed9a --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/alias-with-reference.test @@ -0,0 +1,31 @@ +--TEST-- +Aliases of referenced packages work +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/aliased", "version": "dev-master", + "source": { "reference": "orig", "type": "git", "url": "" } + }, + { + "name": "b/requirer", "version": "1.0.0", + "require": { "a/aliased": "1.0.0" }, + "source": { "reference": "1.0.0", "type": "git", "url": "" } + } + ] + } + ], + "require": { + "a/aliased": "dev-master#abcd as 1.0.0", + "b/requirer": "*" + } +} +--RUN-- +install +--EXPECT-- +Installing a/aliased (dev-master abcd) +Marking a/aliased (1.0.0) as installed, alias of a/aliased (dev-master abcd) +Installing b/requirer (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/circular-dependency.test b/tests/Composer/Test/Fixtures/installer/circular-dependency.test new file mode 100644 index 000000000..9b079bc6f --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/circular-dependency.test @@ -0,0 +1,54 @@ +--TEST-- +Circular dependencies are possible between packages +--COMPOSER-- +{ + "name": "root/package", + "type": "library", + "minimum-stability": "dev", + "version": "dev-master", + "require": { + "required/package": "1.0" + }, + "replace": { + "provided/dependency": "self.version" + }, + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "required/package", + "version": "1.0", + "type": "library", + "source": { "reference": "some.branch", "type": "git", "url": "" }, + "require": { + "provided/dependency": "2.*" + } + } + ] + }, + { + "type": "package", + "package": [ + { + "name": "root/package", + "version": "2.0-dev", + "type": "library", + "source": { "reference": "other.branch", "type": "git", "url": "" }, + "replace": { + "provided/dependency": "self.version" + } + } + ] + } + ] +} +--RUN-- +update +--EXPECT-- +Installing required/package (1.0) diff --git a/tests/Composer/Test/Fixtures/installer/disjunctive-multi-constraints.test b/tests/Composer/Test/Fixtures/installer/disjunctive-multi-constraints.test new file mode 100644 index 000000000..b274c5de2 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/disjunctive-multi-constraints.test @@ -0,0 +1,24 @@ +--TEST-- +Disjunctive multi constraints work +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "foo", "version": "1.1.0" }, + { "name": "foo", "version": "1.0.0" }, + { "name": "bar", "version": "1.1.0", "require": { "foo": "1.0.*" } } + ] + } + ], + "require": { + "bar": "1.*", + "foo": "1.0.*|1.1.*" + } +} +--RUN-- +install +--EXPECT-- +Installing foo (1.0.0) +Installing bar (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test new file mode 100644 index 000000000..f535caa7e --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test @@ -0,0 +1,36 @@ +--TEST-- +Installing double aliased package +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/a", "version": "dev-master", + "dist": { "type": "file", "url": "" }, + "require": { + "b/b": "dev-master" + } + }, + { + "name": "b/b", "version": "dev-foo", + "extra": { "branch-alias": { "dev-foo": "1.0.x-dev" } }, + "dist": { "type": "file", "url": "" } + } + ] + } + ], + "require": { + "a/a": "dev-master", + "b/b": "1.0.x-dev as dev-master" + }, + "minimum-stability": "dev" +} +--RUN-- +install +--EXPECT-- +Installing b/b (dev-foo) +Marking b/b (dev-master) as installed, alias of b/b (dev-foo) +Installing a/a (dev-master) +Marking b/b (1.0.x-dev) as installed, alias of b/b (dev-foo) 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 69b56fa1d..af4eed811 100644 --- a/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test +++ b/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test @@ -41,12 +41,15 @@ install --prefer-dist "type": "library" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": { "a/a": 20 - } + }, + "prefer-stable": false, + "platform": [], + "platform-dev": [] } --EXPECT-- Installing a/a (dev-master) 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 d754651a0..88c3e8fa7 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test @@ -19,12 +19,13 @@ Requirements from the composer file are not installed if the lock file is presen --LOCK-- { "packages": [ - { "package": "required", "version": "1.0.0" } + { "name": "required", "version": "1.0.0" } ], "packages-dev": null, "aliases": [], "minimum-stability": "stable", - "stability-flags": [] + "stability-flags": [], + "prefer-stable": false } --RUN-- install 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..298846609 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,8 @@ 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 } --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 new file mode 100644 index 000000000..f9fd5058a --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test @@ -0,0 +1,67 @@ +--TEST-- +Partial update from lock file should apply lock file and downgrade unstable packages even if not whitelisted +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/old", "version": "1.0.0" }, + { "name": "a/old", "version": "2.0.0" }, + { "name": "b/unstable", "version": "1.0.0" }, + { "name": "b/unstable", "version": "1.1.0-alpha" }, + { "name": "c/uptodate", "version": "1.0.0" }, + { "name": "d/removed", "version": "1.0.0" } + ] + } + ], + "require": { + "a/old": "*", + "b/unstable": "*", + "c/uptodate": "*" + } +} +--LOCK-- +{ + "packages": [ + { "name": "a/old", "version": "1.0.0" }, + { "name": "b/unstable", "version": "1.1.0-alpha" }, + { "name": "c/uptodate", "version": "1.0.0" }, + { "name": "d/removed", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "b/unstable": 15 + }, + "prefer-stable": false, + "platform": [], + "platform-dev": [] +} +--INSTALLED-- +[ + { "name": "a/old", "version": "0.9.0" }, + { "name": "b/unstable", "version": "1.1.0-alpha" }, + { "name": "c/uptodate", "version": "2.0.0" } +] +--RUN-- +update c/uptodate +--EXPECT-LOCK-- +{ + "packages": [ + { "name": "a/old", "version": "1.0.0", "type": "library" }, + { "name": "b/unstable", "version": "1.0.0", "type": "library" }, + { "name": "c/uptodate", "version": "2.0.0", "type": "library" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- +Updating a/old (0.9.0) to a/old (1.0.0) +Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test new file mode 100644 index 000000000..5b904f9b5 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test @@ -0,0 +1,68 @@ +--TEST-- +Partial update from lock file should update everything to the state of the lock, remove overly unstable packages +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/old", "version": "1.0.0" }, + { "name": "a/old", "version": "2.0.0" }, + { "name": "b/unstable", "version": "1.0.0" }, + { "name": "b/unstable", "version": "1.1.0-alpha" }, + { "name": "c/uptodate", "version": "1.0.0" }, + { "name": "d/removed", "version": "1.0.0" } + ] + } + ], + "require": { + "a/old": "*", + "b/unstable": "*", + "c/uptodate": "*" + } +} +--LOCK-- +{ + "packages": [ + { "name": "a/old", "version": "1.0.0" }, + { "name": "b/unstable", "version": "1.1.0-alpha" }, + { "name": "c/uptodate", "version": "1.0.0" }, + { "name": "d/removed", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "b/unstable": 15 + }, + "prefer-stable": false, + "platform": [], + "platform-dev": [] +} +--INSTALLED-- +[ + { "name": "a/old", "version": "0.9.0" }, + { "name": "b/unstable", "version": "1.1.0-alpha" }, + { "name": "c/uptodate", "version": "2.0.0" } +] +--RUN-- +update b/unstable +--EXPECT-LOCK-- +{ + "packages": [ + { "name": "a/old", "version": "1.0.0", "type": "library" }, + { "name": "b/unstable", "version": "1.0.0", "type": "library" }, + { "name": "c/uptodate", "version": "1.0.0", "type": "library" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- +Updating a/old (0.9.0) to a/old (1.0.0) +Updating c/uptodate (2.0.0) to c/uptodate (1.0.0) +Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test new file mode 100644 index 000000000..224e58f7d --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test @@ -0,0 +1,50 @@ +--TEST-- +Partial update without lock file should update everything whitelisted, remove overly unstable packages +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/old", "version": "1.0.0" }, + { "name": "a/old", "version": "2.0.0" }, + { "name": "b/unstable", "version": "1.0.0" }, + { "name": "b/unstable", "version": "1.1.0-alpha" }, + { "name": "c/uptodate", "version": "1.0.0" }, + { "name": "d/removed", "version": "1.0.0" } + ] + } + ], + "require": { + "a/old": "*", + "b/unstable": "*", + "c/uptodate": "*" + } +} +--INSTALLED-- +[ + { "name": "a/old", "version": "1.0.0" }, + { "name": "b/unstable", "version": "1.1.0-alpha" }, + { "name": "c/uptodate", "version": "1.0.0" }, + { "name": "d/removed", "version": "1.0.0" } +] +--RUN-- +update b/unstable +--EXPECT-LOCK-- +{ + "packages": [ + { "name": "a/old", "version": "1.0.0", "type": "library" }, + { "name": "b/unstable", "version": "1.0.0", "type": "library" }, + { "name": "c/uptodate", "version": "1.0.0", "type": "library" }, + { "name": "d/removed", "version": "1.0.0", "type": "library" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- +Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test b/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test new file mode 100644 index 000000000..ad34e9c02 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test @@ -0,0 +1,31 @@ +--TEST-- +Composer installers are installed first if they have no meaningful requirements +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "pkg", "version": "1.0.0" }, + { "name": "pkg2", "version": "1.0.0" }, + { "name": "inst", "version": "1.0.0", "type": "composer-plugin" }, + { "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", + "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 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/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 f30f3a17f..0fc5fe301 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test @@ -38,7 +38,8 @@ Update aliased package does not mess up the lock file "packages-dev": null, "aliases": [], "minimum-stability": "dev", - "stability-flags": [] + "stability-flags": [], + "prefer-stable": false } --INSTALLED-- [ @@ -60,10 +61,13 @@ update "type": "library" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [] + "stability-flags": [], + "prefer-stable": false, + "platform": [], + "platform-dev": [] } --EXPECT-- Updating a/a (dev-master 1234) to a/a (dev-master master) \ No newline at end of file diff --git a/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test b/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test new file mode 100644 index 000000000..cca859e9f --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-all-dry-run.test @@ -0,0 +1,40 @@ +--TEST-- +Updates updateable packages in dry-run mode +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0" }, + { "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.*", + "a/c": "1.*" + }, + "require-dev": { + "a/b": "*" + } +} +--INSTALLED-- +[ + { "name": "a/a", "version": "1.0.0" }, + { "name": "a/c", "version": "1.0.0" }, + { "name": "a/b", "version": "1.0.0" } +] +--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 35a2e9337..a9bb435a1 100644 --- a/tests/Composer/Test/Fixtures/installer/update-all.test +++ b/tests/Composer/Test/Fixtures/installer/update-all.test @@ -30,14 +30,11 @@ Updates updateable packages --INSTALLED-- [ { "name": "a/a", "version": "1.0.0" }, - { "name": "a/c", "version": "1.0.0" } -] ---INSTALLED-DEV-- -[ + { "name": "a/c", "version": "1.0.0" }, { "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-downgrades-unstable-packages.test b/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test new file mode 100644 index 000000000..1b6e55ef9 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test @@ -0,0 +1,49 @@ +--TEST-- +Downgrading from unstable to more stable package should work even if already installed +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "abcd", "url": "", "type": "git" } + }, + { + "name": "a/a", "version": "1.0.0", + "source": { "reference": "1.0.0", "url": "", "type": "git" }, + "dist": { "reference": "1.0.0", "url": "", "type": "zip", "shasum": "" } + }, + { + "name": "b/b", "version": "dev-master", + "source": { "reference": "abcd", "url": "", "type": "git" } + }, + { + "name": "b/b", "version": "1.0.0", + "source": { "reference": "1.0.0", "url": "", "type": "git" }, + "dist": { "reference": "1.0.0", "url": "", "type": "zip", "shasum": "" } + } + ] + } + ], + "require": { + "a/a": "*", + "b/b": "*@dev" + } +} +--INSTALLED-- +[ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "abcd", "url": "", "type": "git" } + }, + { + "name": "b/b", "version": "dev-master", + "source": { "reference": "abcd", "url": "", "type": "git" } + } +] +--RUN-- +update +--EXPECT-- +Updating a/a (dev-master abcd) to a/a (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-installed-alias-dry-run.test b/tests/Composer/Test/Fixtures/installer/update-installed-alias-dry-run.test new file mode 100644 index 000000000..b53287a32 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-installed-alias-dry-run.test @@ -0,0 +1,44 @@ +--TEST-- +Updates installed alias packages in dry-run mode +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/a", "version": "dev-master", + "require": { "b/b": "2.0.*" }, + "source": { "reference": "abcdef", "url": "", "type": "git" }, + "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } } + }, + { + "name": "b/b", "version": "dev-master", + "source": { "reference": "123456", "url": "", "type": "git" }, + "extra": { "branch-alias": { "dev-master": "2.0.x-dev" } } + } + ] + } + ], + "require": { + "a/a": "~1.0@dev", + "b/b": "@dev" + } +} +--INSTALLED-- +[ + { + "name": "a/a", "version": "dev-master", + "require": { "b/b": "2.0.*" }, + "source": { "reference": "abcdef", "url": "", "type": "git" }, + "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } } + }, + { + "name": "b/b", "version": "dev-master", + "source": { "reference": "123456", "url": "", "type": "git" }, + "extra": { "branch-alias": { "dev-master": "2.0.x-dev" } } + } +] +--RUN-- +update --dry-run +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-installed-alias.test b/tests/Composer/Test/Fixtures/installer/update-installed-alias.test new file mode 100644 index 000000000..f5b7e0549 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-installed-alias.test @@ -0,0 +1,44 @@ +--TEST-- +Updates installed alias packages +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/a", "version": "dev-master", + "require": { "b/b": "2.0.*" }, + "source": { "reference": "abcdef", "url": "", "type": "git" }, + "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } } + }, + { + "name": "b/b", "version": "dev-master", + "source": { "reference": "123456", "url": "", "type": "git" }, + "extra": { "branch-alias": { "dev-master": "2.0.x-dev" } } + } + ] + } + ], + "require": { + "a/a": "~1.0@dev", + "b/b": "@dev" + } +} +--INSTALLED-- +[ + { + "name": "a/a", "version": "dev-master", + "require": { "b/b": "2.0.*" }, + "source": { "reference": "abcdef", "url": "", "type": "git" }, + "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } } + }, + { + "name": "b/b", "version": "dev-master", + "source": { "reference": "123456", "url": "", "type": "git" }, + "extra": { "branch-alias": { "dev-master": "2.0.x-dev" } } + } +] +--RUN-- +update +--EXPECT-- 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-whitelist-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test index 6586e461f..381416af1 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test @@ -1,5 +1,5 @@ --TEST-- -Update with a package whitelist only updates those packages and their dependencies if they are not present in composer.json +Update with a package whitelist only updates those packages if they are not present in composer.json --COMPOSER-- { "repositories": [ @@ -30,7 +30,7 @@ Update with a package whitelist only updates those packages and their dependenci { "name": "fixed-sub-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted +update whitelisted dependency --EXPECT-- Updating dependency (1.0.0) to dependency (1.1.0) Updating whitelisted (1.0.0) to whitelisted (1.1.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 63c01b7d5..3bc189015 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test @@ -24,14 +24,15 @@ Limited update takes rules from lock if available, and not from the installed re --LOCK-- { "packages": [ - { "package": "old/installed", "version": "1.0.0" }, - { "package": "toupdate/installed", "version": "1.0.0" }, - { "package": "toupdate/notinstalled", "version": "1.0.0" } + { "name": "old/installed", "version": "1.0.0" }, + { "name": "toupdate/installed", "version": "1.0.0" }, + { "name": "toupdate/notinstalled", "version": "1.0.0" } ], "packages-dev": null, "aliases": [], "minimum-stability": "stable", - "stability-flags": [] + "stability-flags": [], + "prefer-stable": false } --INSTALLED-- [ diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test new file mode 100644 index 000000000..bb2e04193 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test @@ -0,0 +1,40 @@ +--TEST-- +Update with a package whitelist only updates those packages and their dependencies listed as command arguments +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "fixed", "version": "1.1.0" }, + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.1.0" } }, + { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "dependency", "version": "1.1.0" }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.1.0" }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ] + } + ], + "require": { + "fixed": "1.*", + "whitelisted": "1.*", + "unrelated": "1.*" + } +} +--INSTALLED-- +[ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } +] +--RUN-- +update whitelisted --with-dependencies +--EXPECT-- +Updating dependency (1.0.0) to dependency (1.1.0) +Updating whitelisted (1.0.0) to whitelisted (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test new file mode 100644 index 000000000..f63229fbc --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test @@ -0,0 +1,38 @@ +--TEST-- +Update with a package whitelist only updates whitelisted packages if no dependency conflicts +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "fixed", "version": "1.1.0" }, + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.1.0" } }, + { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "dependency", "version": "1.1.0" }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.1.0" }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ] + } + ], + "require": { + "fixed": "1.*", + "whitelisted": "1.*", + "unrelated": "1.*" + } +} +--INSTALLED-- +[ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } +] +--RUN-- +update whitelisted +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist.test b/tests/Composer/Test/Fixtures/installer/update-whitelist.test index 3d7ca30af..751d79e70 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist.test @@ -1,5 +1,5 @@ --TEST-- -Update with a package whitelist only updates those packages and their dependencies listed as command arguments +Update with a package whitelist only updates those packages listed as command arguments --COMPOSER-- { "repositories": [ @@ -8,8 +8,8 @@ Update with a package whitelist only updates those packages and their dependenci "package": [ { "name": "fixed", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.1.0" } }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.*" } }, + { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.*" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, @@ -28,7 +28,7 @@ Update with a package whitelist only updates those packages and their dependenci --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.*" } }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } @@ -36,5 +36,4 @@ Update with a package whitelist only updates those packages and their dependenci --RUN-- update whitelisted --EXPECT-- -Updating dependency (1.0.0) to dependency (1.1.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..bd94617bc 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,8 @@ Installing locked dev packages should remove old dependencies "packages-dev": null, "aliases": [], "minimum-stability": "dev", - "stability-flags": [] + "stability-flags": [], + "prefer-stable": 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 new file mode 100644 index 000000000..849296850 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test @@ -0,0 +1,66 @@ +--TEST-- +Updating a dev package for new reference updates the url and reference +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "newref", "url": "newurl", "type": "git" }, + "dist": { "reference": "newref", "url": "newurl", "type": "zip", "shasum": "" } + } + ] + } + ], + "minimum-stability": "dev", + "require": { + "a/a": "dev-master" + } +} +--LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "oldref", "url": "oldurl", "type": "git" }, + "dist": { "reference": "oldref", "url": "oldurl", "type": "zip", "shasum": "" } + } + ], + "packages-dev": null, + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {"a/a":20}, + "prefer-stable": false +} +--INSTALLED-- +[ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "oldref", "url": "oldurl", "type": "git" }, + "dist": { "reference": "oldref", "url": "oldurl", "type": "zip", "shasum": "" } + } +] +--RUN-- +update +--EXPECT-LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "dev-master", + "type": "library", + "source": { "reference": "newref", "url": "newurl", "type": "git" }, + "dist": { "reference": "newref", "url": "newurl", "type": "zip", "shasum": "" } + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {"a/a":20}, + "prefer-stable": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- +Updating a/a (dev-master oldref) to a/a (dev-master newref) diff --git a/tests/Composer/Test/IO/ConsoleIOTest.php b/tests/Composer/Test/IO/ConsoleIOTest.php index 0d76758d4..3a4313f69 100644 --- a/tests/Composer/Test/IO/ConsoleIOTest.php +++ b/tests/Composer/Test/IO/ConsoleIOTest.php @@ -13,7 +13,7 @@ namespace Composer\Test\IO; use Composer\IO\ConsoleIO; -use Composer\Test\TestCase; +use Composer\TestCase; class ConsoleIOTest extends TestCase { diff --git a/tests/Composer/Test/IO/NullIOTest.php b/tests/Composer/Test/IO/NullIOTest.php index cb2023d49..feb586f95 100644 --- a/tests/Composer/Test/IO/NullIOTest.php +++ b/tests/Composer/Test/IO/NullIOTest.php @@ -13,7 +13,7 @@ namespace Composer\Test\IO; use Composer\IO\NullIO; -use Composer\Test\TestCase; +use Composer\TestCase; class NullIOTest extends TestCase { diff --git a/tests/Composer/Test/Installer/Fixtures/installer-v1/Installer/Custom.php b/tests/Composer/Test/Installer/Fixtures/installer-v1/Installer/Custom.php deleted file mode 100644 index bfad4a88a..000000000 --- a/tests/Composer/Test/Installer/Fixtures/installer-v1/Installer/Custom.php +++ /dev/null @@ -1,19 +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\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/Installer/InstallerInstallerTest.php b/tests/Composer/Test/Installer/InstallerInstallerTest.php deleted file mode 100644 index bfc641029..000000000 --- a/tests/Composer/Test/Installer/InstallerInstallerTest.php +++ /dev/null @@ -1,176 +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\Test\Installer; - -use Composer\Composer; -use Composer\Config; -use Composer\Installer\InstallerInstaller; -use Composer\Package\Loader\JsonLoader; -use Composer\Package\Loader\ArrayLoader; -use Composer\Package\PackageInterface; - -class InstallerInstallerTest extends \PHPUnit_Framework_TestCase -{ - protected $composer; - protected $packages; - protected $im; - protected $repository; - protected $io; - - protected function setUp() - { - $loader = new JsonLoader(new ArrayLoader()); - $this->packages = array(); - for ($i = 1; $i <= 4; $i++) { - $this->packages[] = $loader->load(__DIR__.'/Fixtures/installer-v'.$i.'/composer.json'); - } - - $dm = $this->getMockBuilder('Composer\Downloader\DownloadManager') - ->disableOriginalConstructor() - ->getMock(); - - $this->im = $this->getMockBuilder('Composer\Installer\InstallationManager') - ->disableOriginalConstructor() - ->getMock(); - - $this->repository = $this->getMock('Composer\Repository\InstalledRepositoryInterface'); - - $rm = $this->getMockBuilder('Composer\Repository\RepositoryManager') - ->disableOriginalConstructor() - ->getMock(); - $rm->expects($this->any()) - ->method('getLocalRepositories') - ->will($this->returnValue(array($this->repository))); - - $this->io = $this->getMock('Composer\IO\IOInterface'); - - $this->composer = new Composer(); - $config = new Config(); - $this->composer->setConfig($config); - $this->composer->setDownloadManager($dm); - $this->composer->setInstallationManager($this->im); - $this->composer->setRepositoryManager($rm); - - $config->merge(array( - 'config' => array( - 'vendor-dir' => __DIR__.'/Fixtures/', - 'bin-dir' => __DIR__.'/Fixtures/bin', - ), - )); - } - - public function testInstallNewInstaller() - { - $this->repository - ->expects($this->once()) - ->method('getPackages') - ->will($this->returnValue(array())); - $installer = new InstallerInstallerMock($this->io, $this->composer); - - $test = $this; - $this->im - ->expects($this->once()) - ->method('addInstaller') - ->will($this->returnCallback(function ($installer) use ($test) { - $test->assertEquals('installer-v1', $installer->version); - })); - - $installer->install($this->repository, $this->packages[0]); - } - - public function testInstallMultipleInstallers() - { - $this->repository - ->expects($this->once()) - ->method('getPackages') - ->will($this->returnValue(array())); - - $installer = new InstallerInstallerMock($this->io, $this->composer); - - $test = $this; - - $this->im - ->expects($this->at(0)) - ->method('addInstaller') - ->will($this->returnCallback(function ($installer) use ($test) { - $test->assertEquals('custom1', $installer->name); - $test->assertEquals('installer-v4', $installer->version); - })); - - $this->im - ->expects($this->at(1)) - ->method('addInstaller') - ->will($this->returnCallback(function ($installer) use ($test) { - $test->assertEquals('custom2', $installer->name); - $test->assertEquals('installer-v4', $installer->version); - })); - - $installer->install($this->repository, $this->packages[3]); - } - - public function testUpgradeWithNewClassName() - { - $this->repository - ->expects($this->once()) - ->method('getPackages') - ->will($this->returnValue(array($this->packages[0]))); - $this->repository - ->expects($this->exactly(2)) - ->method('hasPackage') - ->will($this->onConsecutiveCalls(true, false)); - $installer = new InstallerInstallerMock($this->io, $this->composer); - - $test = $this; - $this->im - ->expects($this->once()) - ->method('addInstaller') - ->will($this->returnCallback(function ($installer) use ($test) { - $test->assertEquals('installer-v2', $installer->version); - })); - - $installer->update($this->repository, $this->packages[0], $this->packages[1]); - } - - public function testUpgradeWithSameClassName() - { - $this->repository - ->expects($this->once()) - ->method('getPackages') - ->will($this->returnValue(array($this->packages[1]))); - $this->repository - ->expects($this->exactly(2)) - ->method('hasPackage') - ->will($this->onConsecutiveCalls(true, false)); - $installer = new InstallerInstallerMock($this->io, $this->composer); - - $test = $this; - $this->im - ->expects($this->once()) - ->method('addInstaller') - ->will($this->returnCallback(function ($installer) use ($test) { - $test->assertEquals('installer-v3', $installer->version); - })); - - $installer->update($this->repository, $this->packages[1], $this->packages[2]); - } -} - -class InstallerInstallerMock extends InstallerInstaller -{ - public function getInstallPath(PackageInterface $package) - { - $version = $package->getVersion(); - - return __DIR__.'/Fixtures/installer-v'.$version[0].'/'; - } -} diff --git a/tests/Composer/Test/Installer/LibraryInstallerTest.php b/tests/Composer/Test/Installer/LibraryInstallerTest.php index c76052e22..6230752e5 100644 --- a/tests/Composer/Test/Installer/LibraryInstallerTest.php +++ b/tests/Composer/Test/Installer/LibraryInstallerTest.php @@ -14,7 +14,7 @@ namespace Composer\Test\Installer; use Composer\Installer\LibraryInstaller; use Composer\Util\Filesystem; -use Composer\Test\TestCase; +use Composer\TestCase; use Composer\Composer; use Composer\Config; @@ -131,15 +131,36 @@ class LibraryInstallerTest extends TestCase */ public function testUpdate() { - $library = new LibraryInstaller($this->io, $this->composer); + $filesystem = $this->getMockBuilder('Composer\Util\Filesystem') + ->getMock(); + $filesystem + ->expects($this->once()) + ->method('rename') + ->with($this->vendorDir.'/package1/oldtarget', $this->vendorDir.'/package1/newtarget'); + $initial = $this->createPackageMock(); $target = $this->createPackageMock(); $initial - ->expects($this->any()) + ->expects($this->once()) + ->method('getPrettyName') + ->will($this->returnValue('package1')); + + $initial + ->expects($this->once()) + ->method('getTargetDir') + ->will($this->returnValue('oldtarget')); + + $target + ->expects($this->once()) ->method('getPrettyName') ->will($this->returnValue('package1')); + $target + ->expects($this->once()) + ->method('getTargetDir') + ->will($this->returnValue('newtarget')); + $this->repository ->expects($this->exactly(3)) ->method('hasPackage') @@ -148,7 +169,7 @@ class LibraryInstallerTest extends TestCase $this->dm ->expects($this->once()) ->method('update') - ->with($initial, $target, $this->vendorDir.'/package1'); + ->with($initial, $target, $this->vendorDir.'/package1/newtarget'); $this->repository ->expects($this->once()) @@ -160,6 +181,7 @@ class LibraryInstallerTest extends TestCase ->method('addPackage') ->with($target); + $library = new LibraryInstaller($this->io, $this->composer, 'library', $filesystem); $library->update($this->repository, $initial, $target); $this->assertFileExists($this->vendorDir, 'Vendor dir should be created'); $this->assertFileExists($this->binDir, 'Bin dir should be created'); @@ -197,8 +219,7 @@ class LibraryInstallerTest extends TestCase $library->uninstall($this->repository, $package); - // TODO re-enable once #125 is fixed and we throw exceptions again -// $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException('InvalidArgumentException'); $library->uninstall($this->repository, $package); } @@ -236,7 +257,7 @@ class LibraryInstallerTest extends TestCase protected function createPackageMock() { return $this->getMockBuilder('Composer\Package\Package') - ->setConstructorArgs(array(md5(rand()), '1.0.0.0', '1.0.0')) + ->setConstructorArgs(array(md5(mt_rand()), '1.0.0.0', '1.0.0')) ->getMock(); } } diff --git a/tests/Composer/Test/Installer/MetapackageInstallerTest.php b/tests/Composer/Test/Installer/MetapackageInstallerTest.php index a590274df..204e05265 100644 --- a/tests/Composer/Test/Installer/MetapackageInstallerTest.php +++ b/tests/Composer/Test/Installer/MetapackageInstallerTest.php @@ -86,8 +86,7 @@ class MetapackageInstallerTest extends \PHPUnit_Framework_TestCase $this->installer->uninstall($this->repository, $package); - // TODO re-enable once #125 is fixed and we throw exceptions again -// $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException('InvalidArgumentException'); $this->installer->uninstall($this->repository, $package); } @@ -95,7 +94,7 @@ class MetapackageInstallerTest extends \PHPUnit_Framework_TestCase private function createPackageMock() { return $this->getMockBuilder('Composer\Package\Package') - ->setConstructorArgs(array(md5(rand()), '1.0.0.0', '1.0.0')) + ->setConstructorArgs(array(md5(mt_rand()), '1.0.0.0', '1.0.0')) ->getMock(); } } diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index cca2fd95f..a88ce7c21 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -1,4 +1,5 @@ prevCwd = getcwd(); + chdir(__DIR__); + } + + public function tearDown() + { + chdir($this->prevCwd); + } + /** * @dataProvider provideInstaller */ @@ -36,12 +51,11 @@ 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); - $repositoryManager->setLocalRepository(new WritableRepositoryMock()); - $repositoryManager->setLocalDevRepository(new WritableRepositoryMock()); + $repositoryManager->setLocalRepository(new InstalledArrayRepository()); if (!is_array($repositories)) { $repositories = array($repositories); @@ -52,12 +66,13 @@ class InstallerTest extends TestCase $locker = $this->getMockBuilder('Composer\Package\Locker')->disableOriginalConstructor()->getMock(); $installationManager = new InstallationManagerMock(); - $eventDispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')->disableOriginalConstructor()->getMock(); - $autoloadGenerator = $this->getMock('Composer\Autoload\AutoloadGenerator'); + + $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); + $autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock(); $installer = new Installer($io, $config, clone $rootPackage, $downloadManager, $repositoryManager, $locker, $installationManager, $eventDispatcher, $autoloadGenerator); $result = $installer->run(); - $this->assertTrue($result); + $this->assertSame(0, $result); $expectedInstalled = isset($options['install']) ? $options['install'] : array(); $expectedUpdated = isset($options['update']) ? $options['update'] : array(); @@ -123,7 +138,7 @@ class InstallerTest extends TestCase /** * @dataProvider getIntegrationTests */ - public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $installedDev, $run, $expectLock, $expectOutput, $expect) + public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode) { if ($condition) { eval('$res = '.$condition.';'); @@ -150,17 +165,8 @@ class InstallerTest extends TestCase ->method('exists') ->will($this->returnValue(true)); - $devJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); - $devJsonMock->expects($this->any()) - ->method('read') - ->will($this->returnValue($installedDev)); - $devJsonMock->expects($this->any()) - ->method('exists') - ->will($this->returnValue(true)); - $repositoryManager = $composer->getRepositoryManager(); $repositoryManager->setLocalRepository(new InstalledFilesystemRepositoryMock($jsonMock)); - $repositoryManager->setLocalDevRepository(new InstalledFilesystemRepositoryMock($devJsonMock)); $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); $lockJsonMock->expects($this->any()) @@ -181,32 +187,37 @@ class InstallerTest extends TestCase })); } - $locker = new Locker($lockJsonMock, $repositoryManager, $composer->getInstallationManager(), md5(json_encode($composerConfig))); + $locker = new Locker($io, $lockJsonMock, $repositoryManager, $composer->getInstallationManager(), md5(json_encode($composerConfig))); $composer->setLocker($locker); - $autoloadGenerator = $this->getMock('Composer\Autoload\AutoloadGenerator'); + $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); + $autoloadGenerator = $this->getMock('Composer\Autoload\AutoloadGenerator', array(), array($eventDispatcher)); + $composer->setAutoloadGenerator($autoloadGenerator); + $composer->setEventDispatcher($eventDispatcher); $installer = Installer::create( $io, - $composer, - null, - $autoloadGenerator + $composer ); $application = new Application; $application->get('install')->setCode(function ($input, $output) use ($installer) { - $installer->setDevMode($input->getOption('dev')); + $installer + ->setDevMode(!$input->getOption('no-dev')) + ->setDryRun($input->getOption('dry-run')); - return $installer->run() ? 0 : 1; + return $installer->run(); }); $application->get('update')->setCode(function ($input, $output) use ($installer) { $installer - ->setDevMode($input->getOption('dev')) + ->setDevMode(!$input->getOption('no-dev')) ->setUpdate(true) - ->setUpdateWhitelist($input->getArgument('packages')); + ->setDryRun($input->getOption('dry-run')) + ->setUpdateWhitelist($input->getArgument('packages')) + ->setWhitelistDependencies($input->getOption('with-dependencies')); - return $installer->run() ? 0 : 1; + return $installer->run(); }); if (!preg_match('{^(install|update)\b}', $run)) { @@ -217,10 +228,11 @@ 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']); + unset($actualLock['_readme']); $this->assertEquals($expectLock, $actualLock); } @@ -251,10 +263,10 @@ class InstallerTest extends TestCase --COMPOSER--\s*(?P'.$content.')\s* (?:--LOCK--\s*(?P'.$content.'))?\s* (?:--INSTALLED--\s*(?P'.$content.'))?\s* - (?:--INSTALLED-DEV--\s*(?P'.$content.'))?\s* --RUN--\s*(?P.*?)\s* (?:--EXPECT-LOCK--\s*(?P'.$content.'))?\s* (?:--EXPECT-OUTPUT--\s*(?P'.$content.'))?\s* + (?:--EXPECT-EXIT-CODE--\s*(?P\d+))?\s* --EXPECT--\s*(?P.*?)\s* $}xs'; @@ -262,6 +274,7 @@ class InstallerTest extends TestCase $installedDev = array(); $lock = array(); $expectLock = array(); + $expectExitCode = 0; if (preg_match($pattern, $test, $match)) { try { @@ -277,15 +290,13 @@ class InstallerTest extends TestCase if (!empty($match['installed'])) { $installed = JsonFile::parseJson($match['installed']); } - if (!empty($match['installedDev'])) { - $installedDev = JsonFile::parseJson($match['installedDev']); - } $run = $match['run']; if (!empty($match['expectLock'])) { $expectLock = JsonFile::parseJson($match['expectLock']); } $expectOutput = $match['expectOutput']; $expect = $match['expect']; + $expectExitCode = (int) $match['expectExitCode']; } catch (\Exception $e) { die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file))); } @@ -293,7 +304,7 @@ class InstallerTest extends TestCase die(sprintf('Test "%s" is not valid, did not match the expected format.', str_replace($fixturesDir.'/', '', $file))); } - $tests[] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $installedDev, $run, $expectLock, $expectOutput, $expect); + $tests[] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode); } return $tests; diff --git a/tests/Composer/Test/Json/JsonFileTest.php b/tests/Composer/Test/Json/JsonFileTest.php index 159017aa6..cf89da35e 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); } @@ -179,12 +175,18 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase public function testEscapedSlashes() { - $data = "\\/foo"; $this->assertJsonFormat('"\\\\\\/foo"', $data, 0); } + public function testEscapedBackslashes() + { + $data = "a\\b"; + + $this->assertJsonFormat('"a\\\\b"', $data, 0); + } + public function testEscapedUnicode() { $data = "ƌ"; @@ -192,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 { diff --git a/tests/Composer/Test/Json/JsonFormatterTest.php b/tests/Composer/Test/Json/JsonFormatterTest.php new file mode 100644 index 000000000..eb6e87b00 --- /dev/null +++ b/tests/Composer/Test/Json/JsonFormatterTest.php @@ -0,0 +1,50 @@ + + * 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 8679a7508..1a0f976b5 100644 --- a/tests/Composer/Test/Json/JsonManipulatorTest.php +++ b/tests/Composer/Test/Json/JsonManipulatorTest.php @@ -30,8 +30,7 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase { return array( array( - '{ -}', + '{}', 'require', 'vendor/baz', 'qux', @@ -127,6 +126,156 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase "vendor/baz": "qux" } } +' + ), + array( + '{ + "require": { + "foo": "bar" + }, + "repositories": [{ + "type": "package", + "package": { + "require": { + "foo": "bar" + } + } + }] +}', + 'require', + 'foo', + 'qux', + '{ + "require": { + "foo": "qux" + }, + "repositories": [{ + "type": "package", + "package": { + "require": { + "foo": "bar" + } + } + }] +} +' + ), + array( + '{ + "repositories": [{ + "type": "package", + "package": { + "require": { + "foo": "bar" + } + } + }] +}', + 'require', + 'foo', + 'qux', + '{ + "repositories": [{ + "type": "package", + "package": { + "require": { + "foo": "bar" + } + } + }], + "require": { + "foo": "qux" + } +} +' + ), + array( + '{ + "require": { + "php": "5.*" + } +}', + 'require-dev', + 'foo', + 'qux', + '{ + "require": { + "php": "5.*" + }, + "require-dev": { + "foo": "qux" + } +} +' + ), + array( + '{ + "require": { + "php": "5.*" + }, + "require-dev": { + "foo": "bar" + } +}', + 'require-dev', + 'foo', + 'qux', + '{ + "require": { + "php": "5.*" + }, + "require-dev": { + "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" + } +} ' ), ); @@ -246,6 +395,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( @@ -302,6 +516,24 @@ 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 @@ -312,37 +544,37 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase public function testAddRepositoryCanInitializeEmptyRepositories() { $manipulator = new JsonManipulator('{ - "repositories": { - } + "repositories": { + } }'); $this->assertTrue($manipulator->addRepository('bar', array('type' => 'composer'))); $this->assertEquals('{ - "repositories": { - "bar": { - "type": "composer" - } + "repositories": { + "bar": { + "type": "composer" } + } } ', $manipulator->getContents()); } public function testAddRepositoryCanInitializeFromScratch() { - $manipulator = new JsonManipulator('{ - "a": "b" -}'); + $manipulator = new JsonManipulator("{ +\t\"a\": \"b\" +}"); $this->assertTrue($manipulator->addRepository('bar2', array('type' => 'composer'))); - $this->assertEquals('{ - "a": "b", - "repositories": { - "bar2": { - "type": "composer" - } - } + $this->assertEquals("{ +\t\"a\": \"b\", +\t\"repositories\": { +\t\t\"bar2\": { +\t\t\t\"type\": \"composer\" +\t\t} +\t} } -', $manipulator->getContents()); +", $manipulator->getContents()); } public function testAddRepositoryCanAdd() @@ -393,6 +625,24 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase ', $manipulator->getContents()); } + public function testAddConfigSettingEscapes() + { + $manipulator = new JsonManipulator('{ + "config": { + } +}'); + + $this->assertTrue($manipulator->addConfigSetting('test', 'a\b')); + $this->assertTrue($manipulator->addConfigSetting('test2', "a\nb\fa")); + $this->assertEquals('{ + "config": { + "test": "a\\\\b", + "test2": "a\nb\fa" + } +} +', $manipulator->getContents()); + } + public function testAddConfigSettingCanAdd() { $manipulator = new JsonManipulator('{ @@ -479,6 +729,213 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase "github-protocols": ["https", "http"] } } +', $manipulator->getContents()); + } + + public function testAddConfigSettingCanAddSubKeyInEmptyConfig() + { + $manipulator = new JsonManipulator('{ + "config": { + } +}'); + + $this->assertTrue($manipulator->addConfigSetting('github-oauth.bar', 'baz')); + $this->assertEquals('{ + "config": { + "github-oauth": { + "bar": "baz" + } + } +} +', $manipulator->getContents()); + } + + public function testAddConfigSettingCanAddSubKeyInEmptyVal() + { + $manipulator = new JsonManipulator('{ + "config": { + "github-oauth": {}, + "github-oauth2": { + } + } +}'); + + $this->assertTrue($manipulator->addConfigSetting('github-oauth.bar', 'baz')); + $this->assertTrue($manipulator->addConfigSetting('github-oauth2.a.bar', 'baz2')); + $this->assertTrue($manipulator->addConfigSetting('github-oauth3.b', 'c')); + $this->assertEquals('{ + "config": { + "github-oauth": { + "bar": "baz" + }, + "github-oauth2": { + "a.bar": "baz2" + }, + "github-oauth3": { + "b": "c" + } + } +} +', $manipulator->getContents()); + } + + public function testAddConfigSettingCanAddSubKeyInHash() + { + $manipulator = new JsonManipulator('{ + "config": { + "github-oauth": { + "github.com": "foo" + } + } +}'); + + $this->assertTrue($manipulator->addConfigSetting('github-oauth.bar', 'baz')); + $this->assertEquals('{ + "config": { + "github-oauth": { + "github.com": "foo", + "bar": "baz" + } + } +} +', $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('{ + "config": { + "github-oauth": { + "github.com": "foo", + "bar": "baz" + } + } +}'); + + $this->assertTrue($manipulator->removeConfigSetting('github-oauth.bar')); + $this->assertEquals('{ + "config": { + "github-oauth": { + "github.com": "foo" + } + } +} +', $manipulator->getContents()); + } + + public function testRemoveConfigSettingCanRemoveSubKeyInHashWithSiblings() + { + $manipulator = new JsonManipulator('{ + "config": { + "foo": "bar", + "github-oauth": { + "github.com": "foo", + "bar": "baz" + } + } +}'); + + $this->assertTrue($manipulator->removeConfigSetting('github-oauth.bar')); + $this->assertEquals('{ + "config": { + "foo": "bar", + "github-oauth": { + "github.com": "foo" + } + } +} +', $manipulator->getContents()); + } + + public function testAddMainKey() + { + $manipulator = new JsonManipulator('{ + "foo": "bar" +}'); + + $this->assertTrue($manipulator->addMainKey('bar', 'baz')); + $this->assertEquals('{ + "foo": "bar", + "bar": "baz" +} +', $manipulator->getContents()); + } + + public function testUpdateMainKey() + { + $manipulator = new JsonManipulator('{ + "foo": "bar" +}'); + + $this->assertTrue($manipulator->addMainKey('foo', 'baz')); + $this->assertEquals('{ + "foo": "baz" +} +', $manipulator->getContents()); + } + + public function testUpdateMainKey2() + { + $manipulator = new JsonManipulator('{ + "a": { + "foo": "bar", + "baz": "qux" + }, + "foo": "bar", + "baz": "bar" +}'); + + $this->assertTrue($manipulator->addMainKey('foo', 'baz')); + $this->assertTrue($manipulator->addMainKey('baz', 'quux')); + $this->assertEquals('{ + "a": { + "foo": "bar", + "baz": "qux" + }, + "foo": "baz", + "baz": "quux" +} +', $manipulator->getContents()); + } + + public function testUpdateMainKey3() + { + $manipulator = new JsonManipulator('{ + "require": { + "php": "5.*" + }, + "require-dev": { + "foo": "bar" + } +}'); + + $this->assertTrue($manipulator->addMainKey('require-dev', array('foo' => 'qux'))); + $this->assertEquals('{ + "require": { + "php": "5.*" + }, + "require-dev": { + "foo": "qux" + } +} ', $manipulator->getContents()); } } 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..75d2a23bb 100644 --- a/tests/Composer/Test/Mock/FactoryMock.php +++ b/tests/Composer/Test/Mock/FactoryMock.php @@ -21,7 +21,7 @@ use Composer\IO\IOInterface; class FactoryMock extends Factory { - public static function createConfig() + public static function createConfig(IOInterface $io = null) { $config = new Config(); diff --git a/tests/Composer/Test/Mock/InstallationManagerMock.php b/tests/Composer/Test/Mock/InstallationManagerMock.php index b643df728..e95229347 100644 --- a/tests/Composer/Test/Mock/InstallationManagerMock.php +++ b/tests/Composer/Test/Mock/InstallationManagerMock.php @@ -13,6 +13,7 @@ namespace Composer\Test\Mock; use Composer\Installer\InstallationManager; use Composer\Repository\RepositoryInterface; +use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UpdateOperation; @@ -32,6 +33,11 @@ class InstallationManagerMock extends InstallationManager return ''; } + public function isPackageInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) + { + return $repo->hasPackage($package); + } + public function install(RepositoryInterface $repo, InstallOperation $operation) { $this->installed[] = $operation->getPackage(); @@ -61,16 +67,15 @@ class InstallationManagerMock extends InstallationManager $this->installed[] = $package; $this->trace[] = (string) $operation; - if (!$repo->hasPackage($package)) { - $repo->addPackage($package); - } + parent::markAliasInstalled($repo, $operation); } public function markAliasUninstalled(RepositoryInterface $repo, MarkAliasUninstalledOperation $operation) { $this->uninstalled[] = $operation->getPackage(); $this->trace[] = (string) $operation; - $repo->removePackage($operation->getPackage()); + + parent::markAliasUninstalled($repo, $operation); } public function getTrace() diff --git a/tests/Composer/Test/Mock/WritableRepositoryMock.php b/tests/Composer/Test/Mock/WritableRepositoryMock.php deleted file mode 100644 index 59bb19f88..000000000 --- a/tests/Composer/Test/Mock/WritableRepositoryMock.php +++ /dev/null @@ -1,26 +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\Test\Mock; - -use Composer\Repository\ArrayRepository; -use Composer\Repository\WritableRepositoryInterface; - -class WritableRepositoryMock extends ArrayRepository implements WritableRepositoryInterface -{ - public function reload() - { - } - - public function write() - { - } -} diff --git a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php new file mode 100644 index 000000000..bc74be1e9 --- /dev/null +++ b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php @@ -0,0 +1,293 @@ + + * 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\ArchivableFilesFinder; +use Composer\Util\Filesystem; + +use Symfony\Component\Process\Process; +use Symfony\Component\Process\ExecutableFinder; + +class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase +{ + protected $sources; + protected $finder; + protected $fs; + + protected function setUp() + { + $fs = new Filesystem; + $this->fs = $fs; + + $this->sources = $fs->normalizePath( + realpath(sys_get_temp_dir()).'/composer_archiver_test'.uniqid(mt_rand(), true) + ); + + $fileTree = array( + 'A/prefixA.foo', + 'A/prefixB.foo', + 'A/prefixC.foo', + 'A/prefixD.foo', + 'A/prefixE.foo', + 'A/prefixF.foo', + 'B/sub/prefixA.foo', + 'B/sub/prefixB.foo', + 'B/sub/prefixC.foo', + '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', + 'prefixB.foo', + 'prefixC.foo', + 'prefixD.foo', + 'prefixE.foo', + 'prefixF.foo', + 'parameters.yml', + 'parameters.yml.dist', + '!important!.txt', + '!important_too!.txt' + ); + + foreach ($fileTree as $relativePath) { + $path = $this->sources.'/'.$relativePath; + $fs->ensureDirectoryExists(dirname($path)); + file_put_contents($path, ''); + } + } + + protected function tearDown() + { + $fs = new Filesystem; + $fs->removeDirectory($this->sources); + } + + public function testManualExcludes() + { + $excludes = array( + 'prefixB.foo', + '!/prefixB.foo', + '/prefixA.foo', + 'prefixC.*', + '!*/*/*/prefixC.foo' + ); + + $this->finder = new ArchivableFilesFinder($this->sources, $excludes); + + $this->assertArchivableFiles(array( + '/!important!.txt', + '/!important_too!.txt', + '/A/prefixA.foo', + '/A/prefixD.foo', + '/A/prefixE.foo', + '/A/prefixF.foo', + '/B/sub/prefixA.foo', + '/B/sub/prefixC.foo', + '/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', + '/prefixF.foo', + '/toplevelA.foo', + '/toplevelB.foo', + )); + } + + public function testGitExcludes() + { + // Ensure that git is available for testing. + if (!$this->isProcessAvailable('git')) { + return $this->markTestSkipped('git is not available.'); + } + + file_put_contents($this->sources.'/.gitignore', implode("\n", array( + '# gitignore rules with comments and blank lines', + '', + 'prefixE.foo', + '# and more', + '# comments', + '', + '!/prefixE.foo', + '/prefixD.foo', + 'prefixF.*', + '!/*/*/prefixF.foo', + '', + 'refixD.foo', + '/C', + 'D/prefixA', + 'E', + 'F/', + 'G/*', + 'H/**', + 'J/', + 'parameters.yml', + '\!important!.txt' + ))); + + // git does not currently support negative git attributes + file_put_contents($this->sources.'/.gitattributes', implode("\n", array( + '', + '# gitattributes rules with comments and blank lines', + 'prefixB.foo export-ignore', + //'!/prefixB.foo export-ignore', + '/prefixA.foo export-ignore', + 'prefixC.* export-ignore', + //'!/*/*/prefixC.foo export-ignore' + ))); + + $this->finder = new ArchivableFilesFinder($this->sources, array()); + + $this->assertArchivableFiles($this->getArchivedFiles('git init && '. + 'git add .git* && '. + 'git commit -m "ignore rules" && '. + 'git add . && '. + 'git commit -m "init" && '. + 'git archive --format=zip --prefix=archive/ -o archive.zip HEAD' + )); + } + + public function testHgExcludes() + { + // Ensure that Mercurial is available for testing. + if (!$this->isProcessAvailable('hg')) { + return $this->markTestSkipped('Mercurial is not available.'); + } + + file_put_contents($this->sources.'/.hgignore', implode("\n", array( + '# hgignore rules with comments, blank lines and syntax changes', + '', + 'pre*A.foo', + 'prefixE.foo', + '# and more', + '# comments', + '', + '^prefixD.foo', + 'D/prefixA', + 'parameters.yml', + '\!important!.txt', + 'E', + 'F/', + 'syntax: glob', + 'prefixF.*', + 'B/*', + 'H/**', + ))); + + $this->finder = new ArchivableFilesFinder($this->sources, array()); + + $expectedFiles = $this->getArchivedFiles('hg init && '. + 'hg add && '. + 'hg commit -m "init" && '. + 'hg archive archive.zip' + ); + + // Remove .hg_archival.txt from the expectedFiles + $archiveKey = array_search('/.hg_archival.txt', $expectedFiles); + array_splice($expectedFiles, $archiveKey, 1); + + $this->assertArchivableFiles($expectedFiles); + } + + protected function getArchivableFiles() + { + $files = array(); + foreach ($this->finder as $file) { + if (!$file->isDir()) { + $files[] = preg_replace('#^'.preg_quote($this->sources, '#').'#', '', $this->fs->normalizePath($file->getRealPath())); + } + } + + sort($files); + + return $files; + } + + protected function getArchivedFiles($command) + { + $process = new Process($command, $this->sources); + $process->run(); + + $archive = new \PharData($this->sources.'/archive.zip'); + $iterator = new \RecursiveIteratorIterator($archive); + + $files = array(); + foreach ($iterator as $file) { + $files[] = preg_replace('#^phar://'.preg_quote($this->sources, '#').'/archive\.zip/archive#', '', $this->fs->normalizePath($file)); + } + + unset($archive, $iterator, $file); + unlink($this->sources.'/archive.zip'); + + return $files; + } + + protected function assertArchivableFiles($expectedFiles) + { + $actualFiles = $this->getArchivableFiles(); + + $this->assertEquals($expectedFiles, $actualFiles); + } + + /** + * Check whether or not the given process is available. + * + * @param string $process The name of the binary to test. + * + * @return boolean True if the process is available, false otherwise. + */ + protected function isProcessAvailable($process) + { + $finder = new ExecutableFinder(); + + return (bool) $finder->find($process); + } +} diff --git a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php new file mode 100644 index 000000000..5a9f8c2d8 --- /dev/null +++ b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php @@ -0,0 +1,96 @@ + + * 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\Factory; +use Composer\Package\Archiver; +use Composer\Package\PackageInterface; + +class ArchiveManagerTest extends ArchiverTest +{ + protected $manager; + protected $targetDir; + + public function setUp() + { + parent::setUp(); + + $factory = new Factory(); + $this->manager = $factory->createArchiveManager($factory->createConfig()); + $this->targetDir = $this->testDir.'/composer_archiver_tests'; + } + + public function testUnknownFormat() + { + $this->setExpectedException('RuntimeException'); + + $package = $this->setupPackage(); + + $this->manager->archive($package, '__unknown_format__', $this->targetDir); + } + + public function testArchiveTar() + { + $this->setupGitRepo(); + + $package = $this->setupPackage(); + + $this->manager->archive($package, 'tar', $this->targetDir); + + $target = $this->getTargetName($package, 'tar'); + $this->assertFileExists($target); + + $tmppath = sys_get_temp_dir().'/composer_archiver/'.$this->manager->getPackageFilename($package); + $this->assertFileNotExists($tmppath); + + unlink($target); + } + + protected function getTargetName(PackageInterface $package, $format) + { + $packageName = $this->manager->getPackageFilename($package); + $target = $this->targetDir.'/'.$packageName.'.'.$format; + + return $target; + } + + /** + * Create local git repository to run tests against! + */ + protected function setupGitRepo() + { + $currentWorkDir = getcwd(); + chdir($this->testDir); + + $output = null; + $result = $this->process->execute('git init -q', $output, $this->testDir); + if ($result > 0) { + chdir($currentWorkDir); + throw new \RuntimeException('Could not init: '.$this->process->getErrorOutput()); + } + + $result = file_put_contents('composer.json', '{"name":"faker/faker", "description": "description", "license": "MIT"}'); + if (false === $result) { + chdir($currentWorkDir); + throw new \RuntimeException('Could not save file.'); + } + + $result = $this->process->execute('git add composer.json && git commit -m "commit composer.json" -q', $output, $this->testDir); + if ($result > 0) { + chdir($currentWorkDir); + throw new \RuntimeException('Could not commit: '.$this->process->getErrorOutput()); + } + + chdir($currentWorkDir); + } +} diff --git a/tests/Composer/Test/Package/Archiver/ArchiverTest.php b/tests/Composer/Test/Package/Archiver/ArchiverTest.php new file mode 100644 index 000000000..a3c73fa7a --- /dev/null +++ b/tests/Composer/Test/Package/Archiver/ArchiverTest.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\Test\Package\Archiver; + +use Composer\Util\Filesystem; +use Composer\Util\ProcessExecutor; +use Composer\Package\Package; + +abstract class ArchiverTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Composer\Util\Filesystem + */ + protected $filesystem; + + /** + * @var \Composer\Util\ProcessExecutor + */ + protected $process; + + /** + * @var string + */ + protected $testDir; + + public function setUp() + { + $this->filesystem = new Filesystem(); + $this->process = new ProcessExecutor(); + $this->testDir = sys_get_temp_dir().'/composer_archiver_test_'.mt_rand(); + $this->filesystem->ensureDirectoryExists($this->testDir); + } + + public function tearDown() + { + $this->filesystem->removeDirectory($this->testDir); + } + + /** + * Util method to quickly setup a package using the source path built. + * + * @return \Composer\Package\Package + */ + protected function setupPackage() + { + $package = new Package('archivertest/archivertest', 'master', 'master'); + $package->setSourceUrl(realpath($this->testDir)); + $package->setSourceReference('master'); + $package->setSourceType('git'); + + return $package; + } +} 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/Archiver/HgExcludeFilterTest.php b/tests/Composer/Test/Package/Archiver/HgExcludeFilterTest.php new file mode 100644 index 000000000..c7eae2b56 --- /dev/null +++ b/tests/Composer/Test/Package/Archiver/HgExcludeFilterTest.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\Package\Archiver; + +use Composer\Package\Archiver\HgExcludeFilter; + +class HgExcludeFilterTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider patterns + */ + public function testPatternEscape($ignore, $expected) + { + $filter = new HgExcludeFilter('/'); + + $this->assertEquals($expected, $filter->patternFromRegex($ignore)); + } + + public function patterns() + { + return array( + array('.#', array('#.\\##', false, true)), + array('.\\#', array('#.\\\\\\##', false, true)), + array('\\.#', array('#\\.\\##', false, true)), + array('\\\\.\\\\\\\\#', array('#\\\\.\\\\\\\\\\##', false, true)), + array('.\\\\\\\\\\#', array('#.\\\\\\\\\\\\\\##', false, true)), + ); + } +} diff --git a/tests/Composer/Test/Package/Archiver/PharArchiverTest.php b/tests/Composer/Test/Package/Archiver/PharArchiverTest.php new file mode 100644 index 000000000..d6e783c91 --- /dev/null +++ b/tests/Composer/Test/Package/Archiver/PharArchiverTest.php @@ -0,0 +1,78 @@ + + * 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\PharArchiver; + +class PharArchiverTest extends ArchiverTest +{ + public function testTarArchive() + { + // Set up repository + $this->setupDummyRepo(); + $package = $this->setupPackage(); + $target = sys_get_temp_dir().'/composer_archiver_test.tar'; + + // Test archive + $archiver = new PharArchiver(); + $archiver->archive($package->getSourceUrl(), $target, 'tar', array('foo/bar', 'baz', '!/foo/bar/baz')); + $this->assertFileExists($target); + + unlink($target); + } + + public function testZipArchive() + { + // Set up repository + $this->setupDummyRepo(); + $package = $this->setupPackage(); + $target = sys_get_temp_dir().'/composer_archiver_test.zip'; + + // Test archive + $archiver = new PharArchiver(); + $archiver->archive($package->getSourceUrl(), $target, 'zip'); + $this->assertFileExists($target); + + unlink($target); + } + + /** + * Create a local dummy repository to run tests against! + */ + protected function setupDummyRepo() + { + $currentWorkDir = getcwd(); + chdir($this->testDir); + + $this->writeFile('file.txt', 'content', $currentWorkDir); + $this->writeFile('foo/bar/baz', 'content', $currentWorkDir); + $this->writeFile('foo/bar/ignoreme', 'content', $currentWorkDir); + $this->writeFile('x/baz', 'content', $currentWorkDir); + $this->writeFile('x/includeme', 'content', $currentWorkDir); + + chdir($currentWorkDir); + } + + protected function writeFile($path, $content, $currentWorkDir) + { + if (!file_exists(dirname($path))) { + mkdir(dirname($path), 0777, true); + } + + $result = file_put_contents($path, 'a'); + if (false === $result) { + chdir($currentWorkDir); + throw new \RuntimeException('Could not save file.'); + } + } +} diff --git a/tests/Composer/Test/Package/BasePackageTest.php b/tests/Composer/Test/Package/BasePackageTest.php new file mode 100644 index 000000000..1fe0ece84 --- /dev/null +++ b/tests/Composer/Test/Package/BasePackageTest.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\Package; + +use Composer\Package\BasePackage; + +class BasePackageTest extends \PHPUnit_Framework_TestCase +{ + public function testSetSameRepository() + { + $package = $this->getMockForAbstractClass('Composer\Package\BasePackage', array('foo')); + $repository = $this->getMock('Composer\Repository\RepositoryInterface'); + + $package->setRepository($repository); + try { + $package->setRepository($repository); + } catch (\Exception $e) { + $this->fail('Set against the same repository is allowed.'); + } + } + + /** + * @expectedException LogicException + */ + public function testSetAnotherRepository() + { + $package = $this->getMockForAbstractClass('Composer\Package\BasePackage', array('foo')); + + $package->setRepository($this->getMock('Composer\Repository\RepositoryInterface')); + $package->setRepository($this->getMock('Composer\Repository\RepositoryInterface')); + } +} diff --git a/tests/Composer/Test/Package/CompletePackageTest.php b/tests/Composer/Test/Package/CompletePackageTest.php index b6f90928c..de119ebaa 100644 --- a/tests/Composer/Test/Package/CompletePackageTest.php +++ b/tests/Composer/Test/Package/CompletePackageTest.php @@ -14,7 +14,7 @@ namespace Composer\Test\Package; use Composer\Package\Package; use Composer\Package\Version\VersionParser; -use Composer\Test\TestCase; +use Composer\TestCase; class CompletePackageTest extends TestCase { diff --git a/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php b/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php index e21726b09..5576e3d05 100644 --- a/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php +++ b/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php @@ -101,7 +101,9 @@ class ArrayDumperTest extends \PHPUnit_Framework_TestCase ), array( 'keywords', - array('package', 'dependency', 'autoload') + array('package', 'dependency', 'autoload'), + null, + array('autoload', 'dependency', 'package') ), array( 'bin', @@ -128,6 +130,14 @@ class ArrayDumperTest extends \PHPUnit_Framework_TestCase 'extra', array('class' => 'MyVendor\\Installer') ), + array( + 'archive', + array('/foo/bar', 'baz', '!/foo/bar/baz'), + 'archiveExcludes', + array( + 'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'), + ), + ), array( 'require', array(new Link('foo', 'foo/bar', new VersionConstraint('=', '1.0.0.0'), 'requires', '1.0.0')), @@ -148,6 +158,47 @@ class ArrayDumperTest extends \PHPUnit_Framework_TestCase array( 'support', array('foo' => 'bar'), + ), + array( + 'require', + 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')), + 'requires', + array('bar/baz' => '1.0.0', 'foo/bar' => '1.0.0') + ), + array( + 'require-dev', + 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')), + 'devRequires', + array('bar/baz' => '1.0.0', 'foo/bar' => '1.0.0') + ), + array( + 'suggest', + array('foo/bar' => 'very useful package', 'bar/baz' => 'another useful package'), + 'suggests', + array('bar/baz' => 'another useful package', 'foo/bar' => 'very useful package') + ), + array( + 'provide', + 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')), + 'provides', + array('bar/baz' => '1.0.0', 'foo/bar' => '1.0.0') + ), + array( + 'replace', + 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')), + 'replaces', + array('bar/baz' => '1.0.0', 'foo/bar' => '1.0.0') + ), + array( + 'conflict', + 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/LinkConstraint/VersionConstraintTest.php b/tests/Composer/Test/Package/LinkConstraint/VersionConstraintTest.php index eb6663822..e2adda282 100644 --- a/tests/Composer/Test/Package/LinkConstraint/VersionConstraintTest.php +++ b/tests/Composer/Test/Package/LinkConstraint/VersionConstraintTest.php @@ -72,6 +72,8 @@ class VersionConstraintTest extends \PHPUnit_Framework_TestCase array('==', 'dev-foo-bist', '==', 'dev-foo-aist'), array('<=', 'dev-foo-bist', '>=', 'dev-foo-aist'), array('>=', 'dev-foo-bist', '<', 'dev-foo-aist'), + array('<', '0.12', '==', 'dev-foo'), // branches are not comparable + array('>', '0.12', '==', 'dev-foo'), // branches are not comparable ); } @@ -85,4 +87,19 @@ class VersionConstraintTest extends \PHPUnit_Framework_TestCase $this->assertFalse($versionRequire->matches($versionProvide)); } + + public function testComparableBranches() + { + $versionRequire = new VersionConstraint('>', '0.12'); + $versionProvide = new VersionConstraint('==', 'dev-foo'); + + $this->assertFalse($versionRequire->matches($versionProvide)); + $this->assertFalse($versionRequire->matchSpecific($versionProvide, true)); + + $versionRequire = new VersionConstraint('<', '0.12'); + $versionProvide = new VersionConstraint('==', 'dev-foo'); + + $this->assertFalse($versionRequire->matches($versionProvide)); + $this->assertTrue($versionRequire->matchSpecific($versionProvide, true)); + } } diff --git a/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php b/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php index ef1295ff8..04a537abd 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() @@ -114,10 +114,28 @@ class ArrayLoaderTest extends \PHPUnit_Framework_TestCase 'target-dir' => 'some/prefix', 'extra' => array('random' => array('things' => 'of', 'any' => 'shape')), 'bin' => array('bin1', 'bin/foo'), + 'archive' => array( + 'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'), + ), + 'transport-options' => array('ssl' => array('local_cert' => '/opt/certs/test.pem')) ); $package = $this->loader->load($config); $dumper = new ArrayDumper; $this->assertEquals($config, $dumper->dump($package)); } + + public function testPackageWithBranchAlias() + { + $config = array( + 'name' => 'A', + 'version' => 'dev-master', + 'extra' => array('branch-alias' => array('dev-master' => '1.0.x-dev')), + ); + + $package = $this->loader->load($config); + + $this->assertInstanceOf('Composer\Package\AliasPackage', $package); + $this->assertEquals('1.0.x-dev', $package->getPrettyVersion()); + } } diff --git a/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php b/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php index 6f4f53e19..51799d053 100644 --- a/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php @@ -14,6 +14,7 @@ namespace Composer\Test\Package\Loader; use Composer\Config; use Composer\Package\Loader\RootPackageLoader; +use Composer\Package\BasePackage; use Composer\Test\Mock\ProcessExecutorMock; use Composer\Repository\RepositoryManager; @@ -34,7 +35,12 @@ 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; + } + $self->assertStringStartsWith('git branch', $command); $output = "* (no branch) $commitHash Commit message\n"; @@ -49,4 +55,102 @@ class RootPackageLoaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals("dev-$commitHash", $package->getVersion()); } + + public function testTagBecomesVersion() + { + if (!function_exists('proc_open')) { + $this->markTestSkipped('proc_open() is not available'); + } + + $manager = $this->getMockBuilder('\\Composer\\Repository\\RepositoryManager') + ->disableOriginalConstructor() + ->getMock(); + + $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) { + $self->assertEquals('git describe --exact-match --tags', $command); + + $output = "v2.0.5-alpha2"; + + return 0; + }); + + $config = new Config; + $config->merge(array('repositories' => array('packagist' => false))); + $loader = new RootPackageLoader($manager, $config, null, $processExecutor); + $package = $loader->load(array()); + + $this->assertEquals("2.0.5.0-alpha2", $package->getVersion()); + } + + public function testInvalidTagBecomesVersion() + { + if (!function_exists('proc_open')) { + $this->markTestSkipped('proc_open() is not available'); + } + + $manager = $this->getMockBuilder('\\Composer\\Repository\\RepositoryManager') + ->disableOriginalConstructor() + ->getMock(); + + $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) { + if ('git describe --exact-match --tags' === $command) { + $output = "foo-bar"; + + return 0; + } + + $output = "* foo 03a15d220da53c52eddd5f32ffca64a7b3801bea Commit message\n"; + + return 0; + }); + + $config = new Config; + $config->merge(array('repositories' => array('packagist' => false))); + $loader = new RootPackageLoader($manager, $config, null, $processExecutor); + $package = $loader->load(array()); + + $this->assertEquals("dev-foo", $package->getVersion()); + } + + protected function loadPackage($data) + { + $manager = $this->getMockBuilder('\\Composer\\Repository\\RepositoryManager') + ->disableOriginalConstructor() + ->getMock(); + + $processExecutor = new ProcessExecutorMock(function ($command, &$output = null, $cwd = null) { + return 1; + }); + + $config = new Config; + $config->merge(array('repositories' => array('packagist' => false))); + + $loader = new RootPackageLoader($manager, $config); + + return $loader->load($data); + } + + public function testStabilityFlagsParsing() + { + $package = $this->loadPackage(array( + 'require' => array( + 'foo/bar' => '~2.1.0-beta2', + 'bar/baz' => '1.0.x-dev as 1.2.0', + 'qux/quux' => '1.0.*@rc', + ), + 'minimum-stability' => 'alpha', + )); + + $this->assertEquals('alpha', $package->getMinimumStability()); + $this->assertEquals(array( + 'bar/baz' => BasePackage::STABILITY_DEV, + 'qux/quux' => BasePackage::STABILITY_RC, + ), $package->getStabilityFlags()); + } } diff --git a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php index e095f6e3d..23c47c3c7 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( @@ -123,6 +126,9 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase 'vendor-dir' => 'vendor', 'process-timeout' => 10000, ), + 'archive' => array( + 'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'), + ), 'scripts' => array( 'post-update-cmd' => 'Foo\\Bar\\Baz::doSomething', 'post-install-cmd' => array( @@ -140,6 +146,7 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase 'bin/foo', 'bin/bar', ), + 'transport-options' => array('ssl' => array('local_cert' => '/opt/certs/test.pem')) ), ), array( // test as array @@ -157,7 +164,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'); @@ -175,7 +182,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(); @@ -187,15 +194,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); } @@ -231,6 +243,37 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase 'support.source : invalid value, must be a string', ) ), + array( + array( + 'name' => 'foo/bar', + 'autoload' => 'strings', + ), + array( + 'autoload : should be an array, string given' + ) + ), + array( + array( + 'name' => 'foo/bar', + 'autoload' => array( + 'psr0' => array( + 'foo' => 'src', + ), + ), + ), + array( + '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' + ) + ), ); } @@ -263,6 +306,24 @@ 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 + ), ); } } diff --git a/tests/Composer/Test/Package/LockerTest.php b/tests/Composer/Test/Package/LockerTest.php index b9b1950c9..7e1d2d3b1 100644 --- a/tests/Composer/Test/Package/LockerTest.php +++ b/tests/Composer/Test/Package/LockerTest.php @@ -13,13 +13,14 @@ namespace Composer\Test\Package; use Composer\Package\Locker; +use Composer\IO\NullIO; class LockerTest extends \PHPUnit_Framework_TestCase { public function testIsLocked() { $json = $this->createJsonFileMock(); - $locker = new Locker($json, $this->createRepositoryManagerMock(), $this->createInstallationManagerMock(), 'md5'); + $locker = new Locker(new NullIO, $json, $this->createRepositoryManagerMock(), $this->createInstallationManagerMock(), 'md5'); $json ->expects($this->any()) @@ -39,7 +40,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker($json, $repo, $inst, 'md5'); + $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5'); $json ->expects($this->once()) @@ -57,7 +58,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker($json, $repo, $inst, 'md5'); + $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5'); $json ->expects($this->once()) @@ -68,57 +69,14 @@ class LockerTest extends \PHPUnit_Framework_TestCase ->method('read') ->will($this->returnValue(array( 'packages' => array( - array('package' => 'pkg1', 'version' => '1.0.0-beta'), - array('package' => 'pkg2', 'version' => '0.1.10') - ) - ))); - - $package1 = $this->createPackageMock(); - $package2 = $this->createPackageMock(); - - $repo->getLocalRepository() - ->expects($this->exactly(2)) - ->method('findPackage') - ->with($this->logicalOr('pkg1', 'pkg2'), $this->logicalOr('1.0.0-beta', '0.1.10')) - ->will($this->onConsecutiveCalls($package1, $package2)); - - $this->assertEquals(array($package1, $package2), $locker->getLockedRepository()->getPackages()); - } - - public function testGetPackagesWithoutRepo() - { - $json = $this->createJsonFileMock(); - $repo = $this->createRepositoryManagerMock(); - $inst = $this->createInstallationManagerMock(); - - $locker = new Locker($json, $repo, $inst, 'md5'); - - $json - ->expects($this->once()) - ->method('exists') - ->will($this->returnValue(true)); - $json - ->expects($this->once()) - ->method('read') - ->will($this->returnValue(array( - 'packages' => array( - array('package' => 'pkg1', 'version' => '1.0.0-beta'), - array('package' => 'pkg2', 'version' => '0.1.10') + array('name' => 'pkg1', 'version' => '1.0.0-beta'), + array('name' => 'pkg2', 'version' => '0.1.10') ) ))); - $package1 = $this->createPackageMock(); - $package2 = $this->createPackageMock(); - - $repo->getLocalRepository() - ->expects($this->exactly(2)) - ->method('findPackage') - ->with($this->logicalOr('pkg1', 'pkg2'), $this->logicalOr('1.0.0-beta', '0.1.10')) - ->will($this->onConsecutiveCalls($package1, null)); - - $this->setExpectedException('LogicException'); - - $locker->getLockedRepository(); + $repo = $locker->getLockedRepository(); + $this->assertNotNull($repo->findPackage('pkg1', '1.0.0-beta')); + $this->assertNotNull($repo->findPackage('pkg2', '0.1.10')); } public function testSetLockData() @@ -127,7 +85,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker($json, $repo, $inst, 'md5'); + $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5'); $package1 = $this->createPackageMock(); $package2 = $this->createPackageMock(); @@ -162,6 +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', + 'This file is @gener'.'ated automatically'), 'hash' => 'md5', 'packages' => array( array('name' => 'pkg1', 'version' => '1.0.0-beta'), @@ -171,9 +132,12 @@ class LockerTest extends \PHPUnit_Framework_TestCase 'aliases' => array(), 'minimum-stability' => 'dev', 'stability-flags' => array(), + 'platform' => array(), + 'platform-dev' => array(), + 'prefer-stable' => false, )); - $locker->setLockData(array($package1, $package2), array(), array(), 'dev', array()); + $locker->setLockData(array($package1, $package2), array(), array(), array(), array(), 'dev', array(), false); } public function testLockBadPackages() @@ -182,7 +146,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker($json, $repo, $inst, 'md5'); + $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5'); $package1 = $this->createPackageMock(); $package1 @@ -192,7 +156,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase $this->setExpectedException('LogicException'); - $locker->setLockData(array($package1), array(), array(), 'dev', array()); + $locker->setLockData(array($package1), array(), array(), array(), array(), 'dev', array(), false); } public function testIsFresh() @@ -201,7 +165,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker($json, $repo, $inst, 'md5'); + $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5'); $json ->expects($this->once()) @@ -217,7 +181,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase $repo = $this->createRepositoryManagerMock(); $inst = $this->createInstallationManagerMock(); - $locker = new Locker($json, $repo, $inst, 'md5'); + $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5'); $json ->expects($this->once()) diff --git a/tests/Composer/Test/Package/Version/VersionParserTest.php b/tests/Composer/Test/Package/Version/VersionParserTest.php index 99a4237da..25c2b9898 100644 --- a/tests/Composer/Test/Package/Version/VersionParserTest.php +++ b/tests/Composer/Test/Package/Version/VersionParserTest.php @@ -15,6 +15,7 @@ namespace Composer\Test\Package\Version; use Composer\Package\Version\VersionParser; use Composer\Package\LinkConstraint\MultiConstraint; use Composer\Package\LinkConstraint\VersionConstraint; +use Composer\Package\LinkConstraint\EmptyConstraint; use Composer\Package\PackageInterface; class VersionParserTest extends \PHPUnit_Framework_TestCase @@ -53,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')); @@ -78,32 +79,36 @@ 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'), - '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'), + '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'), ); } @@ -165,6 +170,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase { $parser = new VersionParser; $this->assertSame((string) new VersionConstraint('=', '1.0.9999999.9999999-dev'), (string) $parser->parseConstraints('1.0.x-dev#abcd123')); + $this->assertSame((string) new VersionConstraint('=', '1.0.9999999.9999999-dev'), (string) $parser->parseConstraints('1.0.x-dev#trunk/@123')); } /** @@ -174,6 +180,17 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase { $parser = new VersionParser; $this->assertSame((string) new VersionConstraint('=', '1.0.0.0'), (string) $parser->parseConstraints('1.0#abcd123')); + $this->assertSame((string) new VersionConstraint('=', '1.0.0.0'), (string) $parser->parseConstraints('1.0#trunk/@123')); + } + + /** + * @expectedException UnexpectedValueException + * @expectedExceptionMessage Invalid operator "~>", you probably meant to use the "~" operator + */ + public function testParseConstraintsNudgesRubyDevsTowardsThePathOfRighteousness() + { + $parser = new VersionParser; + $parser->parseConstraints('~>1.2'); } /** @@ -188,10 +205,10 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase public function simpleConstraints() { return array( - 'match any' => array('*', new MultiConstraint(array())), - 'match any/2' => array('*.*', new MultiConstraint(array())), - 'match any/3' => array('*.x.*', new MultiConstraint(array())), - 'match any/4' => array('x.x.x.*', new MultiConstraint(array())), + '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()), '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')), @@ -202,6 +219,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase 'double equals' => array('==1.2.3', new VersionConstraint('=', '1.2.3.0')), 'no op means eq' => array('1.2.3', new VersionConstraint('=', '1.2.3.0')), '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 master' => array('>=dev-master', new VersionConstraint('>=', '9999999-dev')), 'accepts master/2' => array('dev-master', new VersionConstraint('=', '9999999-dev')), @@ -231,13 +249,13 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase public function wildcardConstraints() { return array( - array('2.*', new VersionConstraint('>', '1.9999999.9999999.9999999'), new VersionConstraint('<', '2.9999999.9999999.9999999')), - array('20.*', new VersionConstraint('>', '19.9999999.9999999.9999999'), new VersionConstraint('<', '20.9999999.9999999.9999999')), - array('2.0.*', new VersionConstraint('>', '1.9999999.9999999.9999999'), new VersionConstraint('<', '2.0.9999999.9999999')), - array('2.2.x', new VersionConstraint('>', '2.1.9999999.9999999'), new VersionConstraint('<', '2.2.9999999.9999999')), - array('2.10.x', new VersionConstraint('>', '2.9.9999999.9999999'), new VersionConstraint('<', '2.10.9999999.9999999')), - array('2.1.3.*', new VersionConstraint('>', '2.1.2.9999999'), new VersionConstraint('<', '2.1.3.9999999')), - array('0.*', null, new VersionConstraint('<', '0.9999999.9999999.9999999')), + array('2.*', new VersionConstraint('>=', '2.0.0.0-dev'), new VersionConstraint('<', '3.0.0.0-dev')), + 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.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')), ); } @@ -259,10 +277,17 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase public function tildeConstraints() { return array( - array('~1', new VersionConstraint('>=', '1.0.0.0'), new VersionConstraint('<', '2.0.0.0-dev')), - array('~1.2', new VersionConstraint('>=', '1.2.0.0'), new VersionConstraint('<', '2.0.0.0-dev')), - array('~1.2.3', new VersionConstraint('>=', '1.2.3.0'), new VersionConstraint('<', '1.3.0.0-dev')), - array('~1.2.3.4', new VersionConstraint('>=', '1.2.3.4'), new VersionConstraint('<', '1.2.4.0-dev')), + array('~1', new VersionConstraint('>=', '1.0.0.0-dev'), new VersionConstraint('<', '2.0.0.0-dev')), + array('~1.0', new VersionConstraint('>=', '1.0.0.0-dev'), new VersionConstraint('<', '2.0.0.0-dev')), + array('~1.0.0', new VersionConstraint('>=', '1.0.0.0-dev'), new VersionConstraint('<', '1.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', new VersionConstraint('>=', '1.2.3.0-dev'), new VersionConstraint('<', '1.3.0.0-dev')), + array('~1.2.3.4', new VersionConstraint('>=', '1.2.3.4-dev'), new VersionConstraint('<', '1.2.4.0-dev')), + array('~1.2-beta',new VersionConstraint('>=', '1.2.0.0-beta'), new VersionConstraint('<', '2.0.0.0-dev')), + array('~1.2-b2', new VersionConstraint('>=', '1.2.0.0-beta2'), new VersionConstraint('<', '2.0.0.0-dev')), + array('~1.2-BETA2', new VersionConstraint('>=', '1.2.0.0-beta2'), new VersionConstraint('<', '2.0.0.0-dev')), + array('~1.2.2-dev', new VersionConstraint('>=', '1.2.2.0-dev'), new VersionConstraint('<', '1.3.0.0-dev')), + array('~1.2.2-stable', new VersionConstraint('>=', '1.2.2.0-stable'), new VersionConstraint('<', '1.3.0.0-dev')), ); } @@ -275,6 +300,17 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase $this->assertSame((string) $multi, (string) $parser->parseConstraints('>2.0,<=3.0')); } + public function testParseConstraintsMultiDisjunctiveHasPrioOverConjuctive() + { + $parser = new VersionParser; + $first = new VersionConstraint('>', '2.0.0.0'); + $second = new VersionConstraint('<', '2.0.5.0-dev'); + $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')); + } + public function testParseConstraintsMultiWithStabilities() { $parser = new VersionParser; @@ -313,9 +349,13 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase public function stabilityProvider() { return array( + array('stable', '1'), array('stable', '1.0'), + array('stable', '3.2.1'), + array('stable', 'v3.2.1'), array('dev', 'v2.0.x-dev'), array('dev', 'v2.0.x-dev#abc123'), + array('dev', 'v2.0.x-dev#trunk/@123'), array('RC', '3.0-RC2'), array('dev', 'dev-master'), array('dev', '3.1.2-dev'), diff --git a/tests/Composer/Test/Package/Version/VersionSelectorTest.php b/tests/Composer/Test/Package/Version/VersionSelectorTest.php new file mode 100644 index 000000000..2a9cb45a0 --- /dev/null +++ b/tests/Composer/Test/Package/Version/VersionSelectorTest.php @@ -0,0 +1,132 @@ + + * 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'), + // for non-stable versions, we add ~, but don't try the (1.2.1 -> 1.2) transformation + 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.3.x-dev'), + array('dev-master', true, 'dev', '~2.0@dev', '2.x-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/Fixtures/plugin-v1/Installer/Plugin.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php new file mode 100644 index 000000000..f80acd325 --- /dev/null +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php @@ -0,0 +1,16 @@ + + * 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\Composer; +use Composer\Config; +use Composer\Installer\PluginInstaller; +use Composer\Package\Loader\JsonLoader; +use Composer\Package\Loader\ArrayLoader; +use Composer\Plugin\PluginManager; +use Composer\Autoload\AutoloadGenerator; +use Composer\Util\Filesystem; + +class PluginInstallerTest extends \PHPUnit_Framework_TestCase +{ + protected $composer; + protected $packages; + protected $im; + protected $pm; + protected $repository; + protected $io; + protected $autoloadGenerator; + protected $directory; + + protected function setUp() + { + $loader = new JsonLoader(new ArrayLoader()); + $this->packages = array(); + $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); + $this->packages[] = $loader->load(__DIR__ . $filename); + } + + $dm = $this->getMockBuilder('Composer\Downloader\DownloadManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->repository = $this->getMock('Composer\Repository\InstalledRepositoryInterface'); + + $rm = $this->getMockBuilder('Composer\Repository\RepositoryManager') + ->disableOriginalConstructor() + ->getMock(); + $rm->expects($this->any()) + ->method('getLocalRepository') + ->will($this->returnValue($this->repository)); + + $im = $this->getMock('Composer\Installer\InstallationManager'); + $im->expects($this->any()) + ->method('getInstallPath') + ->will($this->returnCallback(function ($package) { + return __DIR__.'/Fixtures/'.$package->getPrettyName(); + })); + + $this->io = $this->getMock('Composer\IO\IOInterface'); + + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); + $this->autoloadGenerator = new AutoloadGenerator($dispatcher); + + $this->composer = new Composer(); + $config = new Config(); + $this->composer->setConfig($config); + $this->composer->setDownloadManager($dm); + $this->composer->setRepositoryManager($rm); + $this->composer->setInstallationManager($im); + $this->composer->setAutoloadGenerator($this->autoloadGenerator); + + $this->pm = new PluginManager($this->composer, $this->io); + $this->composer->setPluginManager($this->pm); + + $config->merge(array( + 'config' => array( + 'vendor-dir' => $this->directory.'/Fixtures/', + 'home' => $this->directory.'/Fixtures', + 'bin-dir' => $this->directory.'/Fixtures/bin', + ), + )); + } + + protected function tearDown() + { + $filesystem = new Filesystem(); + $filesystem->removeDirectory($this->directory); + } + + public function testInstallNewPlugin() + { + $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]); + + $plugins = $this->pm->getPlugins(); + $this->assertEquals('installer-v1', $plugins[0]->version); + } + + public function testInstallMultiplePlugins() + { + $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[3]); + + $plugins = $this->pm->getPlugins(); + $this->assertEquals('plugin1', $plugins[0]->name); + $this->assertEquals('installer-v4', $plugins[0]->version); + $this->assertEquals('plugin2', $plugins[1]->name); + $this->assertEquals('installer-v4', $plugins[1]->version); + } + + public function testUpgradeWithNewClassName() + { + $this->repository + ->expects($this->exactly(3)) + ->method('getPackages') + ->will($this->returnValue(array($this->packages[0]))); + $this->repository + ->expects($this->exactly(2)) + ->method('hasPackage') + ->will($this->onConsecutiveCalls(true, false)); + $installer = new PluginInstaller($this->io, $this->composer); + $this->pm->loadInstalledPlugins(); + + $installer->update($this->repository, $this->packages[0], $this->packages[1]); + + $plugins = $this->pm->getPlugins(); + $this->assertEquals('installer-v2', $plugins[1]->version); + } + + public function testUpgradeWithSameClassName() + { + $this->repository + ->expects($this->exactly(3)) + ->method('getPackages') + ->will($this->returnValue(array($this->packages[1]))); + $this->repository + ->expects($this->exactly(2)) + ->method('hasPackage') + ->will($this->onConsecutiveCalls(true, false)); + $installer = new PluginInstaller($this->io, $this->composer); + $this->pm->loadInstalledPlugins(); + + $installer->update($this->repository, $this->packages[1], $this->packages[2]); + + $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/ArrayRepositoryTest.php b/tests/Composer/Test/Repository/ArrayRepositoryTest.php index ed05819b6..434b3da99 100644 --- a/tests/Composer/Test/Repository/ArrayRepositoryTest.php +++ b/tests/Composer/Test/Repository/ArrayRepositoryTest.php @@ -13,7 +13,7 @@ namespace Composer\Test\Repository; use Composer\Repository\ArrayRepository; -use Composer\Test\TestCase; +use Composer\TestCase; class ArrayRepositoryTest extends TestCase { @@ -66,4 +66,18 @@ class ArrayRepositoryTest extends TestCase $this->assertCount(2, $bar); $this->assertEquals('bar', $bar[0]->getName()); } + + public function testAutomaticallyAddAliasedPackage() + { + $repo = new ArrayRepository(); + + $package = $this->getPackage('foo', '1'); + $alias = $this->getAliasPackage($package, '2'); + + $repo->addPackage($alias); + + $this->assertEquals(2, count($repo)); + $this->assertTrue($repo->hasPackage($this->getPackage('foo', '1'))); + $this->assertTrue($repo->hasPackage($this->getPackage('foo', '2'))); + } } diff --git a/tests/Composer/Test/Repository/ArtifactRepositoryTest.php b/tests/Composer/Test/Repository/ArtifactRepositoryTest.php new file mode 100644 index 000000000..db7ca42ba --- /dev/null +++ b/tests/Composer/Test/Repository/ArtifactRepositoryTest.php @@ -0,0 +1,109 @@ + + * 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\TestCase; +use Composer\IO\NullIO; +use Composer\Config; +use Composer\Package\BasePackage; +use Composer\Util\Filesystem; + +class ArtifactRepositoryTest extends TestCase +{ + public function testExtractsConfigsFromZipArchives() + { + $expectedPackages = array( + 'vendor0/package0-0.0.1', + '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) { + return "{$package->getPrettyName()}-{$package->getPrettyVersion()}"; + }, $repo->getPackages()); + + sort($expectedPackages); + sort($foundPackages); + + $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/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 9a2d340af..5109ee41f 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -15,7 +15,9 @@ namespace Composer\Test\Repository; use Composer\Repository\ComposerRepository; use Composer\IO\NullIO; use Composer\Test\Mock\FactoryMock; -use Composer\Test\TestCase; +use Composer\TestCase; +use Composer\Package\Loader\ArrayLoader; +use Composer\Package\Version\VersionParser; class ComposerRepositoryTest extends TestCase { @@ -42,7 +44,7 @@ class ComposerRepositoryTest extends TestCase ); $repository - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('loadRootServerFile') ->will($this->returnValue($repoPackages)); @@ -50,7 +52,7 @@ class ComposerRepositoryTest extends TestCase $stubPackage = $this->getPackage('stub/stub', '1.0.0'); $repository - ->expects($this->at($at + 1)) + ->expects($this->at($at + 2)) ->method('createPackage') ->with($this->identicalTo($arg), $this->equalTo('Composer\Package\CompletePackage')) ->will($this->returnValue($stubPackage)); @@ -93,4 +95,74 @@ class ComposerRepositoryTest extends TestCase ), ); } + + public function testWhatProvides() + { + $repo = $this->getMockBuilder('Composer\Repository\ComposerRepository') + ->disableOriginalConstructor() + ->setMethods(array('fetchFile')) + ->getMock(); + + $cache = $this->getMockBuilder('Composer\Cache')->disableOriginalConstructor()->getMock(); + $cache->expects($this->any()) + ->method('sha256') + ->will($this->returnValue(false)); + + $properties = array( + 'cache' => $cache, + 'loader' => new ArrayLoader(), + 'providerListing' => array('p/a.json' => array('sha256' => 'xxx')) + ); + + foreach ($properties as $property => $value) { + $ref = new \ReflectionProperty($repo, $property); + $ref->setAccessible(true); + $ref->setValue($repo, $value); + } + + $repo->expects($this->any()) + ->method('fetchFile') + ->will($this->returnValue(array( + 'packages' => array( + array(array( + 'uid' => 1, + 'name' => 'a', + 'version' => 'dev-master', + 'extra' => array('branch-alias' => array('dev-master' => '1.0.x-dev')), + )), + array(array( + 'uid' => 2, + 'name' => 'a', + 'version' => 'dev-develop', + 'extra' => array('branch-alias' => array('dev-develop' => '1.1.x-dev')), + )), + array(array( + 'uid' => 3, + 'name' => 'a', + 'version' => '0.6', + )), + ) + ))); + + $pool = $this->getMock('Composer\DependencyResolver\Pool'); + $pool->expects($this->any()) + ->method('isPackageAcceptable') + ->will($this->returnValue(true)); + + $versionParser = new VersionParser(); + $repo->setRootAliases(array( + 'a' => array( + $versionParser->normalize('0.6') => array('alias' => 'dev-feature', 'alias_normalized' => $versionParser->normalize('dev-feature')), + $versionParser->normalize('1.1.x-dev') => array('alias' => '1.0', 'alias_normalized' => $versionParser->normalize('1.0')), + ), + )); + + $packages = $repo->whatProvides($pool, 'a'); + + $this->assertCount(7, $packages); + $this->assertEquals(array('1', '1-alias', '2', '2-alias', '2-root', '3', '3-root'), array_keys($packages)); + $this->assertInstanceOf('Composer\Package\AliasPackage', $packages['2-root']); + $this->assertSame($packages['2'], $packages['2-root']->getAliasOf()); + $this->assertSame($packages['2'], $packages['2-alias']->getAliasOf()); + } } diff --git a/tests/Composer/Test/Repository/CompositeRepositoryTest.php b/tests/Composer/Test/Repository/CompositeRepositoryTest.php index c71196729..d9f8b70e3 100644 --- a/tests/Composer/Test/Repository/CompositeRepositoryTest.php +++ b/tests/Composer/Test/Repository/CompositeRepositoryTest.php @@ -14,7 +14,7 @@ namespace Composer\Test\Repository; use Composer\Repository\CompositeRepository; use Composer\Repository\ArrayRepository; -use Composer\Test\TestCase; +use Composer\TestCase; class CompositeRepositoryTest extends TestCase { @@ -125,4 +125,22 @@ class CompositeRepositoryTest extends TestCase $this->assertEquals(2, count($repo), "Should return '2' for count(\$repo)"); } + + /** + * @dataProvider provideMethodCalls + */ + public function testNoRepositories($method, $args) + { + $repo = new CompositeRepository(array()); + $this->assertEquals(array(), call_user_func_array(array($repo, $method), $args)); + } + + public function provideMethodCalls() + { + return array( + array('findPackages', array('foo')), + array('search', array('foo')), + array('getPackages', array()), + ); + } } diff --git a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php index f80a91889..fa1ec6d5b 100644 --- a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php +++ b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php @@ -12,8 +12,7 @@ namespace Composer\Repository; -use Composer\Repository\FilesystemRepository; -use Composer\Test\TestCase; +use Composer\TestCase; class FilesystemRepositoryTest extends TestCase { diff --git a/tests/Composer/Test/Repository/Fixtures/artifacts/composer-1.0.0-alpha6.zip b/tests/Composer/Test/Repository/Fixtures/artifacts/composer-1.0.0-alpha6.zip new file mode 100644 index 000000000..e94843eb6 Binary files /dev/null and b/tests/Composer/Test/Repository/Fixtures/artifacts/composer-1.0.0-alpha6.zip differ 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/Fixtures/artifacts/not-an-artifact.zip b/tests/Composer/Test/Repository/Fixtures/artifacts/not-an-artifact.zip new file mode 100644 index 000000000..3e788dcc2 Binary files /dev/null and b/tests/Composer/Test/Repository/Fixtures/artifacts/not-an-artifact.zip differ diff --git a/tests/Composer/Test/Repository/Fixtures/artifacts/package0.zip b/tests/Composer/Test/Repository/Fixtures/artifacts/package0.zip new file mode 100644 index 000000000..855c6a64d Binary files /dev/null and b/tests/Composer/Test/Repository/Fixtures/artifacts/package0.zip differ diff --git a/tests/Composer/Test/Repository/Fixtures/artifacts/package2.zip b/tests/Composer/Test/Repository/Fixtures/artifacts/package2.zip new file mode 100644 index 000000000..671058059 Binary files /dev/null and b/tests/Composer/Test/Repository/Fixtures/artifacts/package2.zip differ diff --git a/tests/Composer/Test/Repository/Fixtures/artifacts/subfolder/not-an-artifact.zip b/tests/Composer/Test/Repository/Fixtures/artifacts/subfolder/not-an-artifact.zip new file mode 100644 index 000000000..3e788dcc2 Binary files /dev/null and b/tests/Composer/Test/Repository/Fixtures/artifacts/subfolder/not-an-artifact.zip differ diff --git a/tests/Composer/Test/Repository/Fixtures/artifacts/subfolder/package1.zip b/tests/Composer/Test/Repository/Fixtures/artifacts/subfolder/package1.zip new file mode 100644 index 000000000..a2d96c387 Binary files /dev/null and b/tests/Composer/Test/Repository/Fixtures/artifacts/subfolder/package1.zip differ diff --git a/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php b/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php index 127c5689c..214d7b702 100644 --- a/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php +++ b/tests/Composer/Test/Repository/Pear/ChannelReaderTest.php @@ -12,7 +12,7 @@ namespace Composer\Repository\Pear; -use Composer\Test\TestCase; +use Composer\TestCase; use Composer\Package\Version\VersionParser; use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\Link; diff --git a/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php b/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php index ac4f377be..299eae37b 100644 --- a/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php +++ b/tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php @@ -12,7 +12,7 @@ namespace Composer\Repository\Pear; -use Composer\Test\TestCase; +use Composer\TestCase; use Composer\Test\Mock\RemoteFilesystemMock; class ChannelRest10ReaderTest extends TestCase diff --git a/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php b/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php index 58105a5eb..08420786e 100644 --- a/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php +++ b/tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php @@ -12,7 +12,7 @@ namespace Composer\Repository\Pear; -use Composer\Test\TestCase; +use Composer\TestCase; use Composer\Test\Mock\RemoteFilesystemMock; class ChannelRest11ReaderTest extends TestCase diff --git a/tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php b/tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php index 959fe2a9e..4f43d39f0 100644 --- a/tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php +++ b/tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php @@ -12,14 +12,15 @@ namespace Composer\Repository\Pear; -use Composer\Test\TestCase; +use Composer\TestCase; class PackageDependencyParserTest extends TestCase { /** * @dataProvider dataProvider10 * @param $expected - * @param $data + * @param $data10 + * @param $data20 */ public function testShouldParseDependencies($expected, $data10, $data20) { diff --git a/tests/Composer/Test/Repository/PearRepositoryTest.php b/tests/Composer/Test/Repository/PearRepositoryTest.php index c14682553..a42c8e0b3 100644 --- a/tests/Composer/Test/Repository/PearRepositoryTest.php +++ b/tests/Composer/Test/Repository/PearRepositoryTest.php @@ -12,7 +12,7 @@ namespace Composer\Repository; -use Composer\Test\TestCase; +use Composer\TestCase; /** * @group slow @@ -32,6 +32,9 @@ class PearRepositoryTest extends TestCase public function testComposerShouldSetIncludePath() { $url = 'pear.phpmd.org'; + if (!@file_get_contents('http://'.$url)) { + $this->markTestSkipped('Repository '.$url.' appears to be unreachable'); + } $expectedPackages = array( array('name' => 'pear-pear.phpmd.org/PHP_PMD', 'version' => '1.3.3'), ); @@ -64,8 +67,11 @@ class PearRepositoryTest extends TestCase 'url' => $url ); - $this->createRepository($repoConfig); + if (!@file_get_contents('http://'.$url)) { + $this->markTestSkipped('Repository '.$url.' appears to be unreachable'); + } + $this->createRepository($repoConfig); foreach ($expectedPackages as $expectedPackage) { $this->assertInstanceOf('Composer\Package\PackageInterface', $this->repository->findPackage($expectedPackage['name'], $expectedPackage['version']), diff --git a/tests/Composer/Test/Repository/RepositoryManagerTest.php b/tests/Composer/Test/Repository/RepositoryManagerTest.php new file mode 100644 index 000000000..94acc8bad --- /dev/null +++ b/tests/Composer/Test/Repository/RepositoryManagerTest.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\Repository; + +use Composer\TestCase; + +class RepositoryManagerTest extends TestCase +{ + /** + * @dataProvider creationCases + */ + public function testRepoCreation($type, $config) + { + $rm = new RepositoryManager( + $this->getMock('Composer\IO\IOInterface'), + $this->getMock('Composer\Config'), + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() + ); + $rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository'); + $rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository'); + $rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository'); + $rm->setRepositoryClass('pear', 'Composer\Repository\PearRepository'); + $rm->setRepositoryClass('git', 'Composer\Repository\VcsRepository'); + $rm->setRepositoryClass('svn', 'Composer\Repository\VcsRepository'); + $rm->setRepositoryClass('perforce', 'Composer\Repository\VcsRepository'); + $rm->setRepositoryClass('hg', 'Composer\Repository\VcsRepository'); + $rm->setRepositoryClass('artifact', 'Composer\Repository\ArtifactRepository'); + + $rm->createRepository('composer', array('url' => 'http://example.org')); + $rm->createRepository('composer', array('url' => 'http://example.org')); + $rm->createRepository('composer', array('url' => 'http://example.org')); + } + + public function creationCases() + { + return array( + array('composer', array('url' => 'http://example.org')), + array('vcs', array('url' => 'http://github.com/foo/bar')), + array('git', array('url' => 'http://github.com/foo/bar')), + array('git', array('url' => 'git@example.org:foo/bar.git')), + array('svn', array('url' => 'svn://example.org/foo/bar')), + array('pear', array('url' => 'http://pear.example.org/foo')), + array('artifact', array('url' => '/path/to/zips')), + array('package', array()), + ); + } +} diff --git a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php index c8122c7da..d20da87dc 100644 --- a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php @@ -81,15 +81,22 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase $remoteFilesystem->expects($this->at(1)) ->method('getContents') ->with($this->equalTo('github.com'), $this->equalTo('https://api.github.com/authorizations'), $this->equalTo(false)) - ->will($this->returnValue('{"token": "abcdef"}')); + ->will($this->returnValue('[]')); $remoteFilesystem->expects($this->at(2)) + ->method('getContents') + ->with($this->equalTo('github.com'), $this->equalTo('https://api.github.com/authorizations'), $this->equalTo(false)) + ->will($this->returnValue('{"token": "abcdef"}')); + + $remoteFilesystem->expects($this->at(3)) ->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, @@ -101,25 +108,15 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase $this->assertEquals('test_master', $gitHubDriver->getRootIdentifier()); - $dist = $gitHubDriver->getDist($identifier); - $this->assertEquals('zip', $dist['type']); - $this->assertEquals('https://api.github.com/repos/composer/packagist/zipball/v0.0.0', $dist['url']); - $this->assertEquals('v0.0.0', $dist['reference']); - - $source = $gitHubDriver->getSource($identifier); - $this->assertEquals('git', $source['type']); - $this->assertEquals($repoSshUrl, $source['url']); - $this->assertEquals('v0.0.0', $source['reference']); - $dist = $gitHubDriver->getDist($sha); $this->assertEquals('zip', $dist['type']); - $this->assertEquals('https://api.github.com/repos/composer/packagist/zipball/v0.0.0', $dist['url']); - $this->assertEquals('v0.0.0', $dist['reference']); + $this->assertEquals('https://api.github.com/repos/composer/packagist/zipball/SOMESHA', $dist['url']); + $this->assertEquals('SOMESHA', $dist['reference']); $source = $gitHubDriver->getSource($sha); $this->assertEquals('git', $source['type']); $this->assertEquals($repoSshUrl, $source['url']); - $this->assertEquals('v0.0.0', $source['reference']); + $this->assertEquals('SOMESHA', $source['reference']); } public function testPublicRepository() @@ -141,11 +138,12 @@ 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, ); + $repoUrl = 'https://github.com/composer/packagist.git'; $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, null, $remoteFilesystem); $gitHubDriver->initialize(); @@ -153,25 +151,15 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase $this->assertEquals('test_master', $gitHubDriver->getRootIdentifier()); - $dist = $gitHubDriver->getDist($identifier); - $this->assertEquals('zip', $dist['type']); - $this->assertEquals('https://api.github.com/repos/composer/packagist/zipball/v0.0.0', $dist['url']); - $this->assertEquals($identifier, $dist['reference']); - - $source = $gitHubDriver->getSource($identifier); - $this->assertEquals('git', $source['type']); - $this->assertEquals($repoUrl, $source['url']); - $this->assertEquals($identifier, $source['reference']); - $dist = $gitHubDriver->getDist($sha); $this->assertEquals('zip', $dist['type']); - $this->assertEquals('https://api.github.com/repos/composer/packagist/zipball/v0.0.0', $dist['url']); - $this->assertEquals($identifier, $dist['reference']); + $this->assertEquals('https://api.github.com/repos/composer/packagist/zipball/SOMESHA', $dist['url']); + $this->assertEquals($sha, $dist['reference']); $source = $gitHubDriver->getSource($sha); $this->assertEquals('git', $source['type']); $this->assertEquals($repoUrl, $source['url']); - $this->assertEquals($identifier, $source['reference']); + $this->assertEquals($sha, $source['reference']); } public function testPublicRepository2() @@ -193,12 +181,12 @@ 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') - ->with($this->equalTo('github.com'), $this->equalTo('https://raw.github.com/composer/packagist/feature%2F3.2-foo/composer.json'), $this->equalTo(false)) - ->will($this->returnValue('{"support": {"source": "'.$repoUrl.'" }}')); + ->with($this->equalTo('github.com'), $this->equalTo('https://api.github.com/repos/composer/packagist/contents/composer.json?ref=feature%2F3.2-foo'), $this->equalTo(false)) + ->will($this->returnValue('{"encoding":"base64","content":"'.base64_encode('{"support": {"source": "'.$repoUrl.'" }}').'"}')); $remoteFilesystem->expects($this->at(2)) ->method('getContents') @@ -208,6 +196,7 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase $repoConfig = array( 'url' => $repoUrl, ); + $repoUrl = 'https://github.com/composer/packagist.git'; $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, null, $remoteFilesystem); $gitHubDriver->initialize(); @@ -215,25 +204,15 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase $this->assertEquals('test_master', $gitHubDriver->getRootIdentifier()); - $dist = $gitHubDriver->getDist($identifier); - $this->assertEquals('zip', $dist['type']); - $this->assertEquals('https://api.github.com/repos/composer/packagist/zipball/feature/3.2-foo', $dist['url']); - $this->assertEquals($identifier, $dist['reference']); - - $source = $gitHubDriver->getSource($identifier); - $this->assertEquals('git', $source['type']); - $this->assertEquals($repoUrl, $source['url']); - $this->assertEquals($identifier, $source['reference']); - $dist = $gitHubDriver->getDist($sha); $this->assertEquals('zip', $dist['type']); - $this->assertEquals('https://api.github.com/repos/composer/packagist/zipball/feature/3.2-foo', $dist['url']); - $this->assertEquals($identifier, $dist['reference']); + $this->assertEquals('https://api.github.com/repos/composer/packagist/zipball/SOMESHA', $dist['url']); + $this->assertEquals($sha, $dist['reference']); $source = $gitHubDriver->getSource($sha); $this->assertEquals('git', $source['type']); $this->assertEquals($repoUrl, $source['url']); - $this->assertEquals($identifier, $source['reference']); + $this->assertEquals($sha, $source['reference']); $gitHubDriver->getComposerInformation($identifier); } @@ -280,11 +259,11 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase $process->expects($this->at(2)) ->method('execute') - ->with($this->stringContains('git tag')); + ->with($this->stringContains('git show-ref --tags')); $process->expects($this->at(3)) ->method('splitLines') - ->will($this->returnValue(array($identifier))); + ->will($this->returnValue(array($sha.' refs/tags/'.$identifier))); $process->expects($this->at(4)) ->method('execute') @@ -311,19 +290,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 new file mode 100644 index 000000000..09762f26e --- /dev/null +++ b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php @@ -0,0 +1,177 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +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 +{ + protected $config; + protected $io; + protected $process; + protected $remoteFileSystem; + protected $testPath; + protected $driver; + protected $repoConfig; + + const TEST_URL = 'TEST_PERFORCE_URL'; + const TEST_DEPOT = 'TEST_DEPOT_CONFIG'; + const TEST_BRANCH = 'TEST_BRANCH_CONFIG'; + + 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); + } + + 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; + } + + protected function overrideDriverInternalPerforce(Perforce $perforce) + { + $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, + ); + } + + 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(self::TEST_URL, $driver->getUrl()); + $this->assertEquals(self::TEST_DEPOT, $driver->getDepot()); + $this->assertEquals(self::TEST_BRANCH, $driver->getBranch()); + } + + public function testInitializeLogsInAndConnectsClient() + { + $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(); + } + + /** + * @depends testInitializeCapturesVariablesFromRepoConfig + * @depends testInitializeLogsInAndConnectsClient + */ + public function testHasComposerFileReturnsFalseOnNoComposerFile() + { + $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->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); + } + + /** + * Test that supports() simply return false. + * + * @covers \Composer\Repository\Vcs\PerforceDriver::supports + * + * @return void + */ + public function testSupportsReturnsFalseNoDeepCheck() + { + $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 2a4ab00c3..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->once()) - ->method('isInteractive') - ->will($this->returnValue(true)); $output = "svn: OPTIONS of 'http://corp.svn.local/repo':"; $output .= " authorization failed: Could not authenticate to server:"; @@ -35,7 +32,7 @@ class SvnDriverTest extends \PHPUnit_Framework_TestCase $process->expects($this->at(1)) ->method('execute') ->will($this->returnValue(1)); - $process->expects($this->once()) + $process->expects($this->exactly(7)) ->method('getErrorOutput') ->will($this->returnValue($output)); $process->expects($this->at(2)) @@ -80,10 +77,8 @@ class SvnDriverTest extends \PHPUnit_Framework_TestCase */ public function testSupport($url, $assertion) { - if ($assertion === true) { - $this->assertTrue(SvnDriver::supports($this->getMock('Composer\IO\IOInterface'), $url)); - } else { - $this->assertFalse(SvnDriver::supports($this->getMock('Composer\IO\IOInterface'), $url)); - } + $config = new Config(); + $result = SvnDriver::supports($this->getMock('Composer\IO\IOInterface'), $config, $url); + $this->assertEquals($assertion, $result); } } diff --git a/tests/Composer/Test/Repository/VcsRepositoryTest.php b/tests/Composer/Test/Repository/VcsRepositoryTest.php index c79b2807b..eaedc82a9 100644 --- a/tests/Composer/Test/Repository/VcsRepositoryTest.php +++ b/tests/Composer/Test/Repository/VcsRepositoryTest.php @@ -25,13 +25,15 @@ use Composer\Config; */ class VcsRepositoryTest extends \PHPUnit_Framework_TestCase { + private static $composerHome; private static $gitRepo; private $skipped; protected function initialize() { $oldCwd = getcwd(); - self::$gitRepo = sys_get_temp_dir() . '/composer-git-'.rand().'/'; + self::$composerHome = sys_get_temp_dir() . '/composer-home-'.mt_rand().'/'; + self::$gitRepo = sys_get_temp_dir() . '/composer-git-'.mt_rand().'/'; $locator = new ExecutableFinder(); if (!$locator->find('git')) { @@ -39,7 +41,7 @@ class VcsRepositoryTest extends \PHPUnit_Framework_TestCase return; } - if (!mkdir(self::$gitRepo) || !chdir(self::$gitRepo)) { + if (!@mkdir(self::$gitRepo) || !@chdir(self::$gitRepo)) { $this->skipped = 'Could not create and move into the temp git repo '.self::$gitRepo; return; @@ -47,58 +49,67 @@ class VcsRepositoryTest extends \PHPUnit_Framework_TestCase // init $process = new ProcessExecutor; - $process->execute('git init', $null); + $exec = function ($command) use ($process) { + $cwd = getcwd(); + if ($process->execute($command, $output, $cwd) !== 0) { + throw new \RuntimeException('Failed to execute '.$command.': '.$process->getErrorOutput()); + } + }; + + $exec('git init'); + $exec('git config user.email composertest@example.org'); + $exec('git config user.name ComposerTest'); touch('foo'); - $process->execute('git add foo', $null); - $process->execute('git commit -m init', $null); + $exec('git add foo'); + $exec('git commit -m init'); // non-composed tag & branch - $process->execute('git tag 0.5.0', $null); - $process->execute('git branch oldbranch', $null); + $exec('git tag 0.5.0'); + $exec('git branch oldbranch'); // add composed tag & master branch $composer = array('name' => 'a/b'); file_put_contents('composer.json', json_encode($composer)); - $process->execute('git add composer.json', $null); - $process->execute('git commit -m addcomposer', $null); - $process->execute('git tag 0.6.0', $null); + $exec('git add composer.json'); + $exec('git commit -m addcomposer'); + $exec('git tag 0.6.0'); // add feature-a branch - $process->execute('git checkout -b feature/a-1.0-B', $null); + $exec('git checkout -b feature/a-1.0-B'); file_put_contents('foo', 'bar feature'); - $process->execute('git add foo', $null); - $process->execute('git commit -m change-a', $null); + $exec('git add foo'); + $exec('git commit -m change-a'); // add version to composer.json - $process->execute('git checkout master', $null); + $exec('git checkout master'); $composer['version'] = '1.0.0'; file_put_contents('composer.json', json_encode($composer)); - $process->execute('git add composer.json', $null); - $process->execute('git commit -m addversion', $null); + $exec('git add composer.json'); + $exec('git commit -m addversion'); // create tag with wrong version in it - $process->execute('git tag 0.9.0', $null); + $exec('git tag 0.9.0'); // create tag with correct version in it - $process->execute('git tag 1.0.0', $null); + $exec('git tag 1.0.0'); // add feature-b branch - $process->execute('git checkout -b feature-b', $null); + $exec('git checkout -b feature-b'); file_put_contents('foo', 'baz feature'); - $process->execute('git add foo', $null); - $process->execute('git commit -m change-b', $null); + $exec('git add foo'); + $exec('git commit -m change-b'); // add 1.0 branch - $process->execute('git checkout master', $null); - $process->execute('git branch 1.0', $null); + $exec('git checkout master'); + $exec('git branch 1.0'); // add 1.0.x branch - $process->execute('git branch 1.1.x', $null); + $exec('git branch 1.1.x'); // update master to 2.0 $composer['version'] = '2.0.0'; file_put_contents('composer.json', json_encode($composer)); - $process->execute('git add composer.json', $null); - $process->execute('git commit -m bump-version', $null); + $exec('git add composer.json'); + $exec('git commit -m bump-version'); chdir($oldCwd); } @@ -116,6 +127,7 @@ class VcsRepositoryTest extends \PHPUnit_Framework_TestCase public static function tearDownAfterClass() { $fs = new Filesystem; + $fs->removeDirectory(self::$composerHome); $fs->removeDirectory(self::$gitRepo); } @@ -131,7 +143,13 @@ class VcsRepositoryTest extends \PHPUnit_Framework_TestCase 'dev-master' => true, ); - $repo = new VcsRepository(array('url' => self::$gitRepo, 'type' => 'vcs'), new NullIO, new Config()); + $config = new Config(); + $config->merge(array( + 'config' => array( + 'home' => self::$composerHome, + ), + )); + $repo = new VcsRepository(array('url' => self::$gitRepo, 'type' => 'vcs'), new NullIO, $config); $packages = $repo->getPackages(); $dumper = new ArrayDumper(); diff --git a/tests/Composer/Test/Util/ErrorHandlerTest.php b/tests/Composer/Test/Util/ErrorHandlerTest.php index e24fe3f39..cb16a1e13 100644 --- a/tests/Composer/Test/Util/ErrorHandlerTest.php +++ b/tests/Composer/Test/Util/ErrorHandlerTest.php @@ -13,7 +13,7 @@ namespace Composer\Test\Util; use Composer\Util\ErrorHandler; -use Composer\Test\TestCase; +use Composer\TestCase; /** * ErrorHandler test case @@ -38,7 +38,7 @@ class ErrorHandlerTest extends TestCase */ public function testErrorHandlerCaptureWarning() { - $this->setExpectedException('\ErrorException', 'array_merge(): Argument #2 is not an array'); + $this->setExpectedException('\ErrorException', 'array_merge'); ErrorHandler::register(); diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index 586e48568..a4d1255f9 100644 --- a/tests/Composer/Test/Util/FilesystemTest.php +++ b/tests/Composer/Test/Util/FilesystemTest.php @@ -13,7 +13,7 @@ namespace Composer\Test\Util; use Composer\Util\Filesystem; -use Composer\Test\TestCase; +use Composer\TestCase; class FilesystemTest extends TestCase { @@ -38,6 +38,7 @@ class FilesystemTest extends TestCase array('c:/bin/run', 'd:/vendor/acme/bin/run', false, "'d:/vendor/acme/bin/run'"), array('c:\\bin\\run', 'd:/vendor/acme/bin/run', false, "'d:/vendor/acme/bin/run'"), array('/foo/bar', '/foo/bar', true, "__DIR__"), + array('/foo/bar/', '/foo/bar', true, "__DIR__"), array('/foo/bar', '/foo/baz', true, "dirname(__DIR__).'/baz'"), array('/foo/bin/run', '/foo/vendor/acme/bin/run', true, "dirname(dirname(__DIR__)).'/vendor/acme/bin/run'"), array('/foo/bin/run', '/bar/bin/run', true, "'/bar/bin/run'"), @@ -52,6 +53,15 @@ class FilesystemTest extends TestCase array('/tmp/test', '/tmp', true, "dirname(__DIR__)"), array('/tmp', '/tmp/test', true, "__DIR__ . '/test'"), array('C:/Temp', 'c:\Temp\test', true, "__DIR__ . '/test'"), + array('/tmp/test/./', '/tmp/test/', true, '__DIR__'), + array('/tmp/test/../vendor', '/tmp/test', true, "dirname(__DIR__).'/test'"), + array('/tmp/test/.././vendor', '/tmp/test', true, "dirname(__DIR__).'/test'"), + array('C:/Temp', 'c:\Temp\..\..\test', true, "dirname(__DIR__).'/test'"), + array('C:/Temp/../..', 'd:\Temp\..\..\test', true, "'d:/test'"), + array('/foo/bar', '/foo/bar_vendor', true, "dirname(__DIR__).'/bar_vendor'"), + array('/foo/bar_vendor', '/foo/bar', true, "dirname(__DIR__).'/bar'"), + array('/foo/bar_vendor', '/foo/bar/src', true, "dirname(__DIR__).'/bar/src'"), + array('/foo/bar_vendor/src2', '/foo/bar/src/lib', true, "dirname(dirname(__DIR__)).'/bar/src/lib'"), ); } @@ -91,6 +101,17 @@ class FilesystemTest extends TestCase array('/tmp', '/tmp/test', "test"), array('C:/Temp', 'C:\Temp\test', "test"), array('C:/Temp', 'c:\Temp\test', "test"), + array('/tmp/test/./', '/tmp/test', './', true), + array('/tmp/test/../vendor', '/tmp/test', '../test', true), + array('/tmp/test/.././vendor', '/tmp/test', '../test', true), + array('C:/Temp', 'c:\Temp\..\..\test', "../test", true), + array('C:/Temp/../..', 'c:\Temp\..\..\test', "./test", true), + array('C:/Temp/../..', 'D:\Temp\..\..\test', "d:/test", true), + array('/tmp', '/tmp/../../test', '/test', true), + array('/foo/bar', '/foo/bar_vendor', '../bar_vendor', true), + array('/foo/bar_vendor', '/foo/bar', '../bar', true), + array('/foo/bar_vendor', '/foo/bar/src', '../bar/src', true), + array('/foo/bar_vendor/src2', '/foo/bar/src/lib', '../../bar/src/lib', true), ); } @@ -107,5 +128,113 @@ class FilesystemTest extends TestCase $this->assertTrue($fs->removeDirectoryPhp($tmp . "/composer_testdir")); $this->assertFalse(file_exists($tmp . "/composer_testdir/level1/level2/hello.txt")); } -} + public function testFileSize() + { + $tmp = sys_get_temp_dir(); + file_put_contents("$tmp/composer_test_file", 'Hello'); + + $fs = new Filesystem; + $this->assertGreaterThanOrEqual(5, $fs->size("$tmp/composer_test_file")); + } + + public function testDirectorySize() + { + $tmp = sys_get_temp_dir(); + @mkdir("$tmp/composer_testdir", 0777, true); + file_put_contents("$tmp/composer_testdir/file1.txt", 'Hello'); + file_put_contents("$tmp/composer_testdir/file2.txt", 'World'); + + $fs = new Filesystem; + $this->assertGreaterThanOrEqual(10, $fs->size("$tmp/composer_testdir")); + } + + /** + * @dataProvider provideNormalizedPaths + */ + public function testNormalizePath($expected, $actual) + { + $fs = new Filesystem; + $this->assertEquals($expected, $fs->normalizePath($actual)); + } + + public function provideNormalizedPaths() + { + return array( + array('../foo', '../foo'), + array('c:/foo/bar', 'c:/foo//bar'), + array('C:/foo/bar', 'C:/foo/./bar'), + array('C:/bar', 'C:/foo/../bar'), + array('/bar', '/foo/../bar/'), + array('phar://c:/Foo', 'phar://c:/Foo/Bar/..'), + array('phar://c:/', 'phar://c:/Foo/Bar/../../../..'), + array('/', '/Foo/Bar/../../../..'), + array('/', '/'), + array('c:/', 'c:\\'), + array('../src', 'Foo/Bar/../../../src'), + array('c:../b', 'c:.\\..\\a\\..\\b'), + 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/PerforceTest.php b/tests/Composer/Test/Util/PerforceTest.php new file mode 100644 index 000000000..91575b06d --- /dev/null +++ b/tests/Composer/Test/Util/PerforceTest.php @@ -0,0 +1,722 @@ + + * 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\Util\Perforce; +use Composer\Util\ProcessExecutor; + +/** + * @author Matt Whittom + */ +class PerforceTest extends \PHPUnit_Framework_TestCase +{ + protected $perforce; + protected $processExecutor; + protected $io; + + 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'); + $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 + ); + } + + 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() + { + $client = $this->perforce->getClient(); + $hostname = gethostname(); + $timestamp = time(); + + $expected = 'composer_perforce_TEST_depot'; + $this->assertEquals($expected, $client); + } + + public function testGetClientFromStream() + { + $this->setPerforceToStream(); + + $client = $this->perforce->getClient(); + + $expected = 'composer_perforce_TEST_depot_branch'; + $this->assertEquals($expected, $client); + } + + public function testGetStreamWithoutStream() + { + $stream = $this->perforce->getStream(); + $this->assertEquals("//depot", $stream); + } + + public function testGetStreamWithStream() + { + $this->setPerforceToStream(); + + $stream = $this->perforce->getStream(); + $this->assertEquals('//depot/branch', $stream); + } + + public function testGetStreamWithoutLabelWithStreamWithoutLabel() + { + $stream = $this->perforce->getStreamWithoutLabel('//depot/branch'); + $this->assertEquals('//depot/branch', $stream); + } + + public function testGetStreamWithoutLabelWithStreamWithLabel() + { + $stream = $this->perforce->getStreamWithoutLabel('//depot/branching@label'); + $this->assertEquals('//depot/branching', $stream); + } + + public function testGetClientSpec() + { + $clientSpec = $this->perforce->getP4ClientSpec(); + $expected = 'path/composer_perforce_TEST_depot.p4.spec'; + $this->assertEquals($expected, $clientSpec); + } + + public function testGenerateP4Command() + { + $command = 'do something'; + $p4Command = $this->perforce->generateP4Command($command); + $expected = 'p4 -u user -c composer_perforce_TEST_depot -p port do something'; + $this->assertEquals($expected, $p4Command); + } + + public function testQueryP4UserWithUserAlreadySet() + { + $this->perforce->queryP4user(); + $this->assertEquals(self::TEST_P4USER, $this->perforce->getUser()); + } + + public function testQueryP4UserWithUserSetInP4VariablesWithWindowsOS() + { + $this->createNewPerforceWithWindowsFlag(true); + $this->perforce->setUser(null); + $expectedCommand = 'p4 set'; + $callback = function ($command, &$output) { + $output = 'P4USER=TEST_P4VARIABLE_USER' . PHP_EOL; + + 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() + { + $this->createNewPerforceWithWindowsFlag(false); + $this->perforce->setUser(null); + $expectedCommand = 'echo $P4USER'; + $callback = function ($command, &$output) { + $output = 'TEST_P4VARIABLE_USER' . PHP_EOL; + + 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() + { + $this->perforce->setUser(null); + $expectedQuestion = 'Enter P4 User:'; + $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() + { + $this->createNewPerforceWithWindowsFlag(true); + $this->perforce->setUser(null); + $expectedQuestion = 'Enter P4 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(); + } + + public function testQueryP4UserStoresResponseToQueryForUserWithoutWindows() + { + $this->createNewPerforceWithWindowsFlag(false); + $this->perforce->setUser(null); + $expectedQuestion = 'Enter P4 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(); + } + + public function testQueryP4PasswordWithPasswordAlreadySet() + { + $repoConfig = array( + 'depot' => 'depot', + 'branch' => 'branch', + 'p4user' => 'user', + 'p4password' => 'TEST_PASSWORD' + ); + $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() + { + $this->createNewPerforceWithWindowsFlag(true); + $expectedCommand = 'p4 set'; + $callback = function ($command, &$output) { + $output = 'P4PASSWD=TEST_P4VARIABLE_PASSWORD' . PHP_EOL; + + 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() + { + $this->createNewPerforceWithWindowsFlag(false); + $expectedCommand = 'echo $P4PASSWD'; + $callback = function ($command, &$output) { + $output = 'TEST_P4VARIABLE_PASSWORD' . PHP_EOL; + + 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 testQueryP4PasswordQueriesForPassword() + { + $expectedQuestion = 'Enter password for Perforce user user: '; + $this->io->expects($this->at(0)) + ->method('askAndHideAnswer') + ->with($this->equalTo($expectedQuestion)) + ->will($this->returnValue('TEST_QUERY_PASSWORD')); + + $password = $this->perforce->queryP4Password(); + $this->assertEquals('TEST_QUERY_PASSWORD', $password); + } + + public function testWriteP4ClientSpecWithoutStream() + { + $stream = fopen('php://memory', 'w+'); + $this->perforce->writeClientSpecToFile($stream); + + rewind($stream); + + $expectedArray = $this->getExpectedClientSpec(false); + try { + foreach ($expectedArray as $expected) { + $this->assertStringStartsWith($expected, fgets($stream)); + } + $this->assertFalse(fgets($stream)); + } catch (Exception $e) { + fclose($stream); + throw $e; + } + fclose($stream); + } + + public function testWriteP4ClientSpecWithStream() + { + $this->setPerforceToStream(); + $stream = fopen('php://memory', 'w+'); + + $this->perforce->writeClientSpecToFile($stream); + rewind($stream); + + $expectedArray = $this->getExpectedClientSpec(true); + try { + foreach ($expectedArray as $expected) { + $this->assertStringStartsWith($expected, fgets($stream)); + } + $this->assertFalse(fgets($stream)); + } catch (Exception $e) { + fclose($stream); + throw $e; + } + fclose($stream); + } + + public function testIsLoggedIn() + { + $expectedCommand = 'p4 -u user -p port login -s'; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand), $this->equalTo(null)) + ->will($this->returnValue(0)); + + $this->perforce->isLoggedIn(); + } + + public function testConnectClient() + { + $expectedCommand = 'p4 -u user -c composer_perforce_TEST_depot -p port client -i < path/composer_perforce_TEST_depot.p4.spec'; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand), $this->equalTo(null)) + ->will($this->returnValue(0)); + + $this->perforce->connectClient(); + } + + public function testGetBranchesWithStream() + { + $this->setPerforceToStream(); + + $expectedCommand = 'p4 -u user -c composer_perforce_TEST_depot_branch -p port streams //depot/...'; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will( + $this->returnCallback( + function ($command, &$output) { + $output = 'Stream //depot/branch mainline none \'branch\'' . PHP_EOL; + + return true; + } + ) + ); + $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@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@5678', $branches['master']); + } + + public function testGetTagsWithoutStream() + { + $expectedCommand = 'p4 -u user -c composer_perforce_TEST_depot -p port labels'; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will( + $this->returnCallback( + function ($command, &$output) { + $output = 'Label 0.0.1 2013/07/31 \'First Label!\'' . PHP_EOL . 'Label 0.0.2 2013/08/01 \'Second Label!\'' . PHP_EOL; + + return true; + } + ) + ); + + $tags = $this->perforce->getTags(); + $this->assertEquals('//depot@0.0.1', $tags['0.0.1']); + $this->assertEquals('//depot@0.0.2', $tags['0.0.2']); + } + + public function testGetTagsWithStream() + { + $this->setPerforceToStream(); + + $expectedCommand = 'p4 -u user -c composer_perforce_TEST_depot_branch -p port labels'; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will( + $this->returnCallback( + function ($command, &$output) { + $output = 'Label 0.0.1 2013/07/31 \'First Label!\'' . PHP_EOL . 'Label 0.0.2 2013/08/01 \'Second Label!\'' . PHP_EOL; + + return true; + } + ) + ); + + $tags = $this->perforce->getTags(); + $this->assertEquals('//depot/branch@0.0.1', $tags['0.0.1']); + $this->assertEquals('//depot/branch@0.0.2', $tags['0.0.2']); + } + + public function testCheckStreamWithoutStream() + { + $result = $this->perforce->checkStream('depot'); + $this->assertFalse($result); + $this->assertFalse($this->perforce->isStream()); + } + + public function testCheckStreamWithStream() + { + $this->processExecutor->expects($this->any())->method('execute') + ->will( + $this->returnCallback( + function ($command, &$output) { + $output = 'Depot depot 2013/06/25 stream /p4/1/depots/depot/... \'Created by Me\''; + + return true; + } + ) + ); + $result = $this->perforce->checkStream('depot'); + $this->assertTrue($result); + $this->assertTrue($this->perforce->isStream()); + } + + public function testGetComposerInformationWithoutLabelWithoutStream() + { + $expectedCommand = 'p4 -u user -c composer_perforce_TEST_depot -p port print //depot/composer.json'; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will( + $this->returnCallback( + function ($command, &$output) { + $output = PerforceTest::getComposerJson(); + + return true; + } + ) + ); + + $result = $this->perforce->getComposerInformation('//depot'); + $expected = array( + 'name' => 'test/perforce', + 'description' => 'Basic project for testing', + 'minimum-stability' => 'dev', + 'autoload' => array('psr-0' => array()) + ); + $this->assertEquals($expected, $result); + } + + public function testGetComposerInformationWithLabelWithoutStream() + { + $expectedCommand = 'p4 -u user -p port files //depot/composer.json@0.0.1'; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will( + $this->returnCallback( + function ($command, &$output) { + $output = '//depot/composer.json#1 - branch change 10001 (text)'; + + return true; + } + ) + ); + + $expectedCommand = 'p4 -u user -c composer_perforce_TEST_depot -p port print //depot/composer.json@10001'; + $this->processExecutor->expects($this->at(1)) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will( + $this->returnCallback( + function ($command, &$output) { + $output = PerforceTest::getComposerJson(); + + return true; + } + ) + ); + + $result = $this->perforce->getComposerInformation('//depot@0.0.1'); + + $expected = array( + 'name' => 'test/perforce', + 'description' => 'Basic project for testing', + 'minimum-stability' => 'dev', + 'autoload' => array('psr-0' => array()) + ); + $this->assertEquals($expected, $result); + } + + public function testGetComposerInformationWithoutLabelWithStream() + { + $this->setPerforceToStream(); + + $expectedCommand = 'p4 -u user -c composer_perforce_TEST_depot_branch -p port print //depot/branch/composer.json'; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will( + $this->returnCallback( + function ($command, &$output) { + $output = PerforceTest::getComposerJson(); + + return true; + } + ) + ); + + $result = $this->perforce->getComposerInformation('//depot/branch'); + + $expected = array( + 'name' => 'test/perforce', + 'description' => 'Basic project for testing', + 'minimum-stability' => 'dev', + 'autoload' => array('psr-0' => array()) + ); + $this->assertEquals($expected, $result); + } + + public function testGetComposerInformationWithLabelWithStream() + { + $this->setPerforceToStream(); + $expectedCommand = 'p4 -u user -p port files //depot/branch/composer.json@0.0.1'; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will( + $this->returnCallback( + function ($command, &$output) { + $output = '//depot/composer.json#1 - branch change 10001 (text)'; + + return true; + } + ) + ); + + $expectedCommand = 'p4 -u user -c composer_perforce_TEST_depot_branch -p port print //depot/branch/composer.json@10001'; + $this->processExecutor->expects($this->at(1)) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will( + $this->returnCallback( + function ($command, &$output) { + $output = PerforceTest::getComposerJson(); + + return true; + } + ) + ); + + $result = $this->perforce->getComposerInformation('//depot/branch@0.0.1'); + + $expected = array( + 'name' => 'test/perforce', + 'description' => 'Basic project for testing', + 'minimum-stability' => 'dev', + 'autoload' => array('psr-0' => array()) + ); + $this->assertEquals($expected, $result); + } + + public function testSyncCodeBaseWithoutStream() + { + $expectedCommand = 'p4 -u user -c composer_perforce_TEST_depot -p port sync -f @label'; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand), $this->equalTo(null)) + ->will($this->returnValue(0)); + + $this->perforce->syncCodeBase('label'); + } + + public function testSyncCodeBaseWithStream() + { + $this->setPerforceToStream(); + $expectedCommand = 'p4 -u user -c composer_perforce_TEST_depot_branch -p port sync -f @label'; + $this->processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand)) + ->will($this->returnValue(0)); + + $this->perforce->syncCodeBase('label'); + } + + public function testCheckServerExists() + { + $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); + + $expectedCommand = 'p4 -p perforce.does.exist:port info -s'; + $processExecutor->expects($this->at(0)) + ->method('execute') + ->with($this->equalTo($expectedCommand), $this->equalTo(null)) + ->will($this->returnValue(0)); + + $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() + { + $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); + + $expectedCommand = 'p4 -p perforce.does.exist:port info -s'; + $processExecutor->expects($this->at(0)) + ->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); + } + + public static function getComposerJson() + { + $composer_json = array( + '{', + '"name": "test/perforce",', + '"description": "Basic project for testing",', + '"minimum-stability": "dev",', + '"autoload": {', + '"psr-0" : {', + '}', + '}', + '}' + ); + + return implode($composer_json); + } + + private function getExpectedClientSpec($withStream) + { + $expectedArray = array( + 'Client: composer_perforce_TEST_depot', + PHP_EOL, + 'Update:', + PHP_EOL, + 'Access:', + 'Owner: user', + PHP_EOL, + 'Description:', + ' Created by user from composer.', + PHP_EOL, + 'Root: path', + PHP_EOL, + 'Options: noallwrite noclobber nocompress unlocked modtime rmdir', + PHP_EOL, + 'SubmitOptions: revertunchanged', + PHP_EOL, + 'LineEnd: local', + PHP_EOL + ); + if ($withStream) { + $expectedArray[] = 'Stream:'; + $expectedArray[] = ' //depot/branch'; + } else { + $expectedArray[] = 'View: //depot/... //composer_perforce_TEST_depot/...'; + } + + return $expectedArray; + } + + private function setPerforceToStream() + { + $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/ProcessExecutorTest.php b/tests/Composer/Test/Util/ProcessExecutorTest.php index f2b394d9f..b15a2763f 100644 --- a/tests/Composer/Test/Util/ProcessExecutorTest.php +++ b/tests/Composer/Test/Util/ProcessExecutorTest.php @@ -13,7 +13,7 @@ namespace Composer\Test\Util; use Composer\Util\ProcessExecutor; -use Composer\Test\TestCase; +use Composer\TestCase; class ProcessExecutorTest extends TestCase { @@ -56,6 +56,6 @@ class ProcessExecutorTest extends TestCase $this->assertEquals(array('foo'), $process->splitLines('foo')); $this->assertEquals(array('foo', 'bar'), $process->splitLines("foo\nbar")); $this->assertEquals(array('foo', 'bar'), $process->splitLines("foo\r\nbar")); - $this->assertEquals(array('foo', 'bar', ''), $process->splitLines("foo\r\nbar\n")); + $this->assertEquals(array('foo', 'bar'), $process->splitLines("foo\r\nbar\n")); } } diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php index f9c1d398b..bbeba908e 100644 --- a/tests/Composer/Test/Util/RemoteFilesystemTest.php +++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php @@ -13,6 +13,7 @@ namespace Composer\Test\Util; use Composer\Util\RemoteFilesystem; +use Installer\Exception; class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase { @@ -129,17 +130,26 @@ 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')); + $this->assertNull($this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, 'HTTP/1.1 404 Not Found', 404, 0, 0)); + } + + public function testCaptureAuthenticationParamsFromUrl() + { + $io = $this->getMock('Composer\IO\IOInterface'); + $io->expects($this->once()) + ->method('setAuthentication') + ->with($this->equalTo('example.com'), $this->equalTo('user'), $this->equalTo('pass')); + + $fs = new RemoteFilesystem($io); try { - $this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, 'HTTP/1.1 404 Not Found', 404, 0, 0); - $this->fail(); + $fs->getContents('example.com', 'http://user:pass@www.example.com/something'); } catch (\Exception $e) { $this->assertInstanceOf('Composer\Downloader\TransportException', $e); $this->assertEquals(404, $e->getCode()); - $this->assertContains('HTTP/1.1 404 Not Found', $e->getMessage()); } } @@ -163,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/SpdxLicenseIdentifierTest.php b/tests/Composer/Test/Util/SpdxLicenseIdentifierTest.php index 2ed7c1819..b6cee4ec5 100644 --- a/tests/Composer/Test/Util/SpdxLicenseIdentifierTest.php +++ b/tests/Composer/Test/Util/SpdxLicenseIdentifierTest.php @@ -1,7 +1,7 @@ array()), array() + $a = array('http' => array('follow_location' => 1, 'max_redirects' => 20)), array(), + array('options' => $a), array() ), array( - $a = array('http' => array('method' => 'GET')), $a, - array('options' => $a, 'notification' => $f = function() {}), array('notification' => $f) + $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) ), ); } public function testHttpProxy() { - $_SERVER['http_proxy'] = 'http://username:password@proxyserver.net:3128/'; + $_SERVER['http_proxy'] = 'http://username:p%40ssword@proxyserver.net:3128/'; $_SERVER['HTTP_PROXY'] = 'http://proxyserver/'; - $context = StreamContextFactory::getContext(array('http' => array('method' => 'GET'))); + $context = StreamContextFactory::getContext('http://example.org', array('http' => array('method' => 'GET'))); $options = stream_context_get_options($context); $this->assertEquals(array('http' => array( 'proxy' => 'tcp://proxyserver.net:3128', 'request_fulluri' => true, 'method' => 'GET', - 'header' => array("Proxy-Authorization: Basic " . base64_encode('username:password')) + 'header' => array("Proxy-Authorization: Basic " . base64_encode('username:p@ssword')), + 'max_redirects' => 20, + 'follow_location' => 1, + )), $options); + } + + public function testHttpProxyWithNoProxy() + { + $_SERVER['http_proxy'] = 'http://username:password@proxyserver.net:3128/'; + $_SERVER['no_proxy'] = 'foo,example.org'; + + $context = StreamContextFactory::getContext('http://example.org', array('http' => array('method' => 'GET'))); + $options = stream_context_get_options($context); + + $this->assertEquals(array('http' => array( + 'method' => 'GET', + 'max_redirects' => 20, + 'follow_location' => 1, + )), $options); + } + + public function testHttpProxyWithNoProxyWildcard() + { + $_SERVER['http_proxy'] = 'http://username:password@proxyserver.net:3128/'; + $_SERVER['no_proxy'] = '*'; + + $context = StreamContextFactory::getContext('http://example.org', array('http' => array('method' => 'GET'))); + $options = stream_context_get_options($context); + + $this->assertEquals(array('http' => array( + 'method' => 'GET', + 'max_redirects' => 20, + 'follow_location' => 1, )), $options); } @@ -75,14 +109,16 @@ class StreamContextFactoryTest extends \PHPUnit_Framework_TestCase { $_SERVER['http_proxy'] = 'http://username:password@proxyserver.net:3128/'; - $context = StreamContextFactory::getContext(array('http' => array('method' => 'GET', 'header' => array("X-Foo: bar"), 'request_fulluri' => false))); + $context = StreamContextFactory::getContext('http://example.org', array('http' => array('method' => 'GET', 'header' => array("X-Foo: bar"), 'request_fulluri' => false))); $options = stream_context_get_options($context); $this->assertEquals(array('http' => array( 'proxy' => 'tcp://proxyserver.net:3128', 'request_fulluri' => false, 'method' => 'GET', - 'header' => array("X-Foo: bar", "Proxy-Authorization: Basic " . base64_encode('username:password')) + 'header' => array("X-Foo: bar", "Proxy-Authorization: Basic " . base64_encode('username:password')), + 'max_redirects' => 20, + 'follow_location' => 1, )), $options); } @@ -90,14 +126,16 @@ class StreamContextFactoryTest extends \PHPUnit_Framework_TestCase { $_SERVER['http_proxy'] = 'http://username:password@proxyserver.net'; - $context = StreamContextFactory::getContext(array('http' => array('method' => 'GET'))); + $context = StreamContextFactory::getContext('http://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')) + 'header' => array("Proxy-Authorization: Basic " . base64_encode('username:password')), + 'max_redirects' => 20, + 'follow_location' => 1, )), $options); } @@ -109,18 +147,20 @@ class StreamContextFactoryTest extends \PHPUnit_Framework_TestCase $_SERVER['http_proxy'] = $proxy; if (extension_loaded('openssl')) { - $context = StreamContextFactory::getContext(); + $context = StreamContextFactory::getContext('http://example.org'); $options = stream_context_get_options($context); $this->assertEquals(array('http' => array( 'proxy' => $expected, 'request_fulluri' => true, + 'max_redirects' => 20, + 'follow_location' => 1, )), $options); } else { try { - StreamContextFactory::getContext(); + StreamContextFactory::getContext('http://example.org'); $this->fail(); - } catch (\Exception $e) { + } catch (\RuntimeException $e) { $this->assertInstanceOf('RuntimeException', $e); } } @@ -133,4 +173,25 @@ class StreamContextFactoryTest extends \PHPUnit_Framework_TestCase array('ssl://proxyserver:8443', 'https://proxyserver:8443'), ); } + + public function testEnsureThatfixHttpHeaderFieldMovesContentTypeToEndOfOptions() + { + $options = array( + 'http' => array( + 'header' => "X-Foo: bar\r\nContent-Type: application/json\r\nAuthorization: Basic aW52YWxpZA==" + ) + ); + $expectedOptions = array( + 'http' => array( + 'header' => array( + "X-Foo: bar", + "Authorization: Basic aW52YWxpZA==", + "Content-Type: application/json" + ) + ) + ); + $context = StreamContextFactory::getContext('http://example.org', $options); + $ctxoptions = stream_context_get_options($context); + $this->assertEquals(end($ctxoptions['http']['header']), end($expectedOptions['http']['header'])); + } } diff --git a/tests/Composer/Test/Util/SvnTest.php b/tests/Composer/Test/Util/SvnTest.php index a29db7cee..b1f19ca1a 100644 --- a/tests/Composer/Test/Util/SvnTest.php +++ b/tests/Composer/Test/Util/SvnTest.php @@ -1,25 +1,12 @@ getCmd(" --no-auth-cache --username 'till' --password 'test' ")), - array('http://svn.apache.org/', ''), - array('svn://johndoe@example.org', $this->getCmd(" --no-auth-cache --username 'johndoe' --password '' ")), - ); - } - /** * Test the credential string. * @@ -30,20 +17,113 @@ class SvnTest */ public function testCredentials($url, $expect) { - $svn = new Svn($url, new NullIO); + $svn = new Svn($url, new NullIO, new Config()); + $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialString'); + $reflMethod->setAccessible(true); - $this->assertEquals($expect, $svn->getCredentialString()); + $this->assertEquals($expect, $reflMethod->invoke($svn)); + } + + /** + * Provide some examples for {@self::testCredentials()}. + * + * @return array + */ + public function urlProvider() + { + return array( + array('http://till:test@svn.example.org/', $this->getCmd(" --username 'till' --password 'test' ")), + array('http://svn.apache.org/', ''), + array('svn://johndoe@example.org', $this->getCmd(" --username 'johndoe' --password '' ")), + ); } public function testInteractiveString() { $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); $this->assertEquals( - "svn ls --non-interactive 'http://svn.example.org'", - $svn->getCommand('svn ls', $url) + $this->getCmd("svn ls --non-interactive 'http://svn.example.org'"), + $reflMethod->invokeArgs($svn, array('svn ls', $url)) ); } + + 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')) { + return strtr($cmd, "'", '"'); + } + + return $cmd; + } } diff --git a/tests/Composer/Test/TestCase.php b/tests/Composer/TestCase.php similarity index 98% rename from tests/Composer/Test/TestCase.php rename to tests/Composer/TestCase.php index bb4e0f14b..760b57291 100644 --- a/tests/Composer/Test/TestCase.php +++ b/tests/Composer/TestCase.php @@ -10,7 +10,7 @@ * file that was distributed with this source code. */ -namespace Composer\Test; +namespace Composer; use Composer\Package\Version\VersionParser; use Composer\Package\Package; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 1974415d3..908861cf5 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -12,5 +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';