diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index eee01015b..8f525a97f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,7 +2,7 @@ Contributing to Composer ======================== Please note that this project is released with a -[Contributor Code of Conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct/). +[Contributor Code of Conduct](https://github.com/composer/composer/blob/master/CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. Reporting Issues diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 000000000..30c035bd4 --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,158 @@ +name: "Continuous Integration" + +on: + push: + paths-ignore: + - 'doc/**' + pull_request: + paths-ignore: + - 'doc/**' + +env: + COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist" + COMPOSER_UPDATE_FLAGS: "" + SYMFONY_PHPUNIT_VERSION: "8.3" + SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT: "1" + +jobs: + tests: + name: "CI" + + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.experimental }} + + strategy: + matrix: + php-version: + - "5.3" + - "5.4" + - "5.5" + - "5.6" + - "7.0" + - "7.1" + - "7.2" + - "7.3" + - "7.4" + dependencies: [locked] + os: [ubuntu-latest] + experimental: [false] + include: + - php-version: 5.3 + dependencies: highest + os: ubuntu-latest + experimental: false + - php-version: 5.3 + dependencies: lowest + os: ubuntu-latest + experimental: false + - php-version: 7.4 + dependencies: highest + os: ubuntu-latest + experimental: false + - php-version: 7.4 + os: windows-latest + dependencies: locked + experimental: false + - php-version: 7.4 + os: macos-latest + dependencies: locked + experimental: false + - php-version: 8.0 + dependencies: lowest-ignore + os: ubuntu-latest + experimental: true + - php-version: 8.0 + dependencies: highest-ignore + os: ubuntu-latest + experimental: true + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Choose PHPUnit version" + if: "!startsWith(matrix.os, 'windows')" + run: | + if [ "${{ matrix.php-version }}" = "5.3" ] || [ "${{ matrix.php-version }}" = "5.4" ] || [ "${{ matrix.php-version }}" = "5.5" ]; then + echo "::set-env name=SYMFONY_PHPUNIT_VERSION::4.8"; + elif [ "${{ matrix.php-version }}" = "5.6" ]; then + echo "::set-env name=SYMFONY_PHPUNIT_VERSION::5.7"; + elif [ "${{ matrix.php-version }}" = "7.0" ]; then + echo "::set-env name=SYMFONY_PHPUNIT_VERSION::6.5"; + elif [ "${{ matrix.php-version }}" = "7.1" ]; then + echo "::set-env name=SYMFONY_PHPUNIT_VERSION::7.5"; + else + echo "::set-env name=SYMFONY_PHPUNIT_VERSION::8.3"; + fi + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + extensions: "intl" + ini-values: "memory_limit=-1, phar.readonly=0" + php-version: "${{ matrix.php-version }}" + + - name: "Update to latest Composer snapshot" + if: "!startsWith(matrix.os, 'windows')" + run: "sudo -i composer self-update --snapshot" + + - name: "Update to latest Composer snapshot on Windows" + if: "startsWith(matrix.os, 'windows')" + run: "composer self-update --snapshot" + + - name: "Determine composer cache directory" + id: "determine-composer-cache-directory" + run: "echo \"::set-output name=directory::$(composer config cache-dir)\"" + + - name: "Cache dependencies installed with composer" + uses: "actions/cache@v1" + with: + path: "${{ steps.determine-composer-cache-directory.outputs.directory }}" + key: "php-${{ matrix.php-version }}-symfony-php-unit-version-${{ env.SYMFONY_PHPUNIT_VERSION }}-${{ hashFiles('**/composer.lock') }}" + restore-keys: "php-${{ matrix.php-version }}-symfony-php-unit-version-${{ env.SYMFONY_PHPUNIT_VERSION }}" + + - name: "Handle lowest dependencies update" + if: "contains(matrix.dependencies, 'lowest')" + run: "echo \"::set-env name=COMPOSER_UPDATE_FLAGS::$COMPOSER_UPDATE_FLAGS --prefer-lowest\"" + + - name: "Handle ignore-platform-reqs dependencies update" + if: "contains(matrix.dependencies, 'ignore')" + run: "echo \"::set-env name=COMPOSER_FLAGS::$COMPOSER_FLAGS --ignore-platform-req=php\"" + + - name: "Remove platform config to get latest dependencies for current PHP version when build is not locked" + run: "composer config platform --unset" + + - name: "Update dependencies from composer.json using composer binary provided by system" + if: "contains(matrix.dependencies, 'highest') || contains(matrix.dependencies, 'lowest')" + run: "composer update ${{ env.COMPOSER_UPDATE_FLAGS }} ${{ env.COMPOSER_FLAGS }}" + + - name: "Install dependencies from composer.lock using composer binary provided by system" + if: "matrix.dependencies == 'locked'" + run: "composer install ${{ env.COMPOSER_FLAGS }}" + + - name: "Require latest PHPUnitBridge for PHP 8" + if: "matrix.php-version == '8.0'" + run: | + composer require --no-update --dev symfony/phpunit-bridge:^5.1 + composer config -g platform-check false + + - name: "Update Symfony's PHPUnitBridge to latest available for the current PHP always as it is not really a dependency of the project" + run: "composer update ${{ env.COMPOSER_FLAGS }} symfony/phpunit-bridge" + + - name: "Run install again using composer binary from source" + run: "bin/composer install ${{ env.COMPOSER_FLAGS }}" + + - name: "Validate composer.json" + run: "bin/composer validate" + + - name: "Prepare git environment" + run: "git config --global user.name composer && git config --global user.email composer@example.com" + + - name: "Run tests" + if: "matrix.php-version != '7.3'" + run: "vendor/bin/simple-phpunit" + + - name: "Run complete test suite on 7.3" + if: "matrix.php-version == '7.3'" + run: "vendor/bin/simple-phpunit --configuration tests/complete.phpunit.xml" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..0db4f34cb --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,36 @@ +name: "PHP Lint" + +on: + push: + paths-ignore: + - 'doc/**' + pull_request: + paths-ignore: + - 'doc/**' + +jobs: + tests: + name: "Lint" + + runs-on: ubuntu-latest + + strategy: + matrix: + php-version: + - "5.3" + - "7.4" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + extensions: "intl" + ini-values: "memory_limit=-1" + php-version: "${{ matrix.php-version }}" + + - name: "Lint PHP files" + run: "find src/ -type f -name '*.php' -print0 | xargs -0 -L1 -P4 -- php -l -f" diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 000000000..967f79dcd --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,56 @@ +name: "PHPStan" + +on: + push: + paths-ignore: + - 'doc/**' + pull_request: + paths-ignore: + - 'doc/**' + +env: + COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --no-suggest --prefer-dist" + SYMFONY_PHPUNIT_VERSION: "" + +jobs: + tests: + name: "PHPStan" + + runs-on: ubuntu-latest + + strategy: + matrix: + php-version: + - "7.4" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + extensions: "intl" + ini-values: "memory_limit=-1" + php-version: "${{ matrix.php-version }}" + tools: "cs2pr" + + - name: "Determine composer cache directory" + id: "determine-composer-cache-directory" + run: "echo \"::set-output name=directory::$(composer config cache-dir)\"" + + - name: "Cache dependencies installed with composer" + uses: "actions/cache@v1" + with: + path: "${{ steps.determine-composer-cache-directory.outputs.directory }}" + key: "php-${{ matrix.php-version }}-symfony-php-unit-version-${{ env.SYMFONY_PHPUNIT_VERSION }}-${{ hashFiles('**/composer.lock') }}" + restore-keys: "php-${{ matrix.php-version }}-symfony-php-unit-version-${{ env.SYMFONY_PHPUNIT_VERSION }}" + + - name: "Install highest dependencies from composer.json using composer binary provided by system" + run: "composer config platform --unset && composer update ${{ env.COMPOSER_FLAGS }}" + + - name: Run PHPStan + run: | + bin/composer require --dev phpstan/phpstan:^0.12 phpunit/phpunit:^7.5 --with-all-dependencies + vendor/bin/phpstan analyse --configuration=phpstan/config.neon --error-format=checkstyle | cs2pr diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..a67b4e6e5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,58 @@ +name: "Release" + +on: + push: + tags: + - "*" + +env: + COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --no-suggest --prefer-dist" + +jobs: + build: + name: Upload Release Asset + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + extensions: "intl" + ini-values: "memory_limit=-1" + php-version: "7.4" + + - name: "Install dependencies from composer.lock using composer binary provided by system" + run: "composer install ${{ env.COMPOSER_FLAGS }}" + + - name: "Run install again using composer binary from source" + run: "bin/composer install ${{ env.COMPOSER_FLAGS }}" + + - name: "Validate composer.json" + run: "bin/composer validate" + + - name: Build phar file + run: "php -d phar.readonly=0 bin/compile" + + - name: Create release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + draft: true + body: TODO + + - name: Upload phar + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./composer.phar + asset_name: composer.phar + asset_content_type: application/octet-stream diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0cf228882..000000000 --- a/.travis.yml +++ /dev/null @@ -1,93 +0,0 @@ -language: php - -dist: bionic - -git: - depth: 5 - -cache: - directories: - - $HOME/.composer/cache - -matrix: - include: - - php: 5.3 - dist: precise - - php: 5.4 - dist: trusty - - php: 5.5 - dist: trusty - - php: 5.6 - dist: xenial - - php: 7.0 - dist: xenial - - php: 7.1 - dist: xenial - - php: 7.2 - dist: xenial - - php: 7.3 - dist: xenial - # Regular 7.4 build with locked deps - - php: 7.4 - env: - - SYMFONY_PHPUNIT_VERSION=7.5 - # High deps check - - php: 7.4 - env: - - deps=high - - SYMFONY_PHPUNIT_VERSION=7.5 - # PHPStan checks - - php: 7.4 - env: - - deps=high - - PHPSTAN=1 - - SYMFONY_PHPUNIT_VERSION=7.5 - - php: nightly - fast_finish: true - allow_failures: - - php: nightly - -before_install: - # disable Xdebug if available - - phpenv config-rm xdebug.ini || echo "xdebug not available" - # disable default memory limit - - export INI=~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini - - echo memory_limit = -1 >> $INI - - composer validate - -install: - # flags to pass to install - - flags="--ansi --prefer-dist --no-interaction --optimize-autoloader --no-progress" - # update deps to latest in case of high deps build - - if [ "$deps" == "high" ]; then composer config platform.php 7.4.0; composer update $flags; fi - # install dependencies using system provided composer binary - - composer install $flags - # install dependencies using composer from source - - bin/composer install $flags - -before_script: - # make sure git tests do not complain about user/email not being set - - git config --global user.name travis-ci - - git config --global user.email travis@example.com - -script: - - if [[ $PHPSTAN == "1" ]]; then - bin/composer require --dev phpstan/phpstan:^0.12 phpunit/phpunit:^7.5 --no-update && - bin/composer update phpstan/* phpunit/* sebastian/* --with-all-dependencies && - vendor/bin/phpstan analyse --configuration=phpstan/config.neon; - else - vendor/bin/simple-phpunit; - fi - -before_deploy: - - php -d phar.readonly=0 bin/compile - -deploy: - provider: releases - api_key: $GITHUB_TOKEN - file: composer.phar - skip_cleanup: true - on: - tags: true - repo: composer/composer - php: '7.3' diff --git a/CHANGELOG.md b/CHANGELOG.md index 47211a0a6..2e2f52236 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,54 @@ -### [2.0.0-?] 2020-?? +### [2.0.0-alpha1] 2020-06-03 * Breaking: This is a major release and while we tried to keep things compatible for most users, you might want to have a look at the [UPGRADE](UPGRADE-2.0.md) guides * Many CPU and memory performance improvements * The update command is now much more deterministic as it does not take the already installed packages into account * Package installation now performs all network operations first before doing any changes on disk, to reduce the chances of ending up with a partially updated vendor dir * Partial updates and require/remove are now much faster as they only load the metadata required for the updated packages - * Added support for parallel downloads of package metadata and zip files, this requires that the curl extension is present + * Added a [platform-check step](doc/07-runtime.md#platform-check) when vendor/autoload.php gets initialized which checks the current PHP version/extensions match what is expected and fails hard otherwise. Can be disabled with the platform-check config option + * Added a [`Composer\InstalledVersions`](doc/07-runtime.md#installed-versions) class which is autoloaded in every project and lets you check which packages/versions are present at runtime + * Added a `composer-runtime-api` virtual package which you can require (as e.g. `^2.0`) to ensure things like the InstalledVersions class above are present. It will effectively force people to use Composer 2.x to install your project + * Added support for parallel downloads of package metadata and zip files, this requires that the curl extension is present and we thus strongly recommend enabling curl * Added much clearer dependency resolution error reporting for common error cases + * Added support for updating to a specific version with partial updates, as well as a [--with flag](doc/03-cli.md#update--u) to pass in temporary constraint overrides * Added support for TTY mode on Linux/OSX/WSL so that script handlers now run in interactive mode * Added `only`, `exclude` and `canonical` options to all repositories, see [repository priorities](https://getcomposer.org/repoprio) for details * Added support for lib-zip platform package * Added `pre-operations-exec` event to be fired before the packages get installed/upgraded/removed * Added `pre-pool-create` event to be fired before the package pool for the dependency solver is created, which lets you modify the list of packages going in + * Added `post-file-download` event to be fired after package dist files are downloaded, which lets you do additional checks on the files + * Added --locked flag to `show` command to see the packages from the composer.lock file + * Added --unused flag to `remove` command to make sure any packages which are not needed anymore get removed * Added --dry-run flag to `require` and `remove` commands + * Added --no-install flag to `update`, `require` and `remove` commands to disable the install step and only do the update step (composer.lock file update) * Added --with-dependencies and --with-all-dependencies flag aliases to `require` and `remove` commands for consistency with `update` * Added more info to `vendor/composer/installed.json`, a dev key stores whether dev requirements were installed, and every package now has an install-path key with its install location * Added COMPOSER_DISABLE_NETWORK which if set makes Composer do its best to run offline. This can be useful when you have poor connectivity or to do benchmarking without network jitter + * Added --json and --merge flags to `config` command to allow editing complex `extra.*` values by using json as input * Added confirmation prompt when running Composer as superuser in interactive mode + * Added --no-check-version to `validate` command to remove the warning in case the version is defined + * Added --ignore-platform-req (without s) to all commands supporting --ignore-platform-reqs, which accepts a package name so you can ignore only specific platform requirements + * Added support for wildcards (`*`) in classmap autoloader paths + * Added support for configuring GitLab deploy tokens in addition to private tokens, see [gitlab-token](doc/06-config.md#gitlab-token) + * Added support for package version guessing for require and init command to take all platform packages into account, not just php version + * Fixed package ordering when autoloading and especially when loading plugins, to make sure dependencies are loaded before their dependents * Fixed suggest output being very spammy, it now is only one line long and shows more rarely * Fixed conflict rules like e.g. >=5 from matching dev-master, as it is not normalized to 9999999-dev internally anymore +### [1.10.7] 2020-06-03 + + * Fix PHP 8 deprecations + * Fixed detection of pcntl_signal being in disabled_functions when pcntl_async_signal is allowed + +### [1.10.6] 2020-05-06 + + * Fixed version guessing to take composer-runtime-api and composer-plugin-api requirements into account to avoid selecting packages which require Composer 2 + * Fixed package name validation to allow several dashes following each other + * Fixed post-status-cmd script not firing when there were no changes to be displayed + * Fixed composer-runtime-api support on Composer 1.x, the package is now present as 1.0.0 + * Fixed support for composer show --name-only --self + * Fixed detection of GitLab URLs when handling authentication in some cases + ### [1.10.5] 2020-04-10 * Fixed self-update on PHP <5.6, seriously please upgrade people, it's time @@ -868,6 +897,9 @@ * Initial release +[2.0.0-alpha1]: https://github.com/composer/composer/compare/1.10.7...2.0.0-alpha1 +[1.10.7]: https://github.com/composer/composer/compare/1.10.6...1.10.7 +[1.10.6]: https://github.com/composer/composer/compare/1.10.5...1.10.6 [1.10.5]: https://github.com/composer/composer/compare/1.10.4...1.10.5 [1.10.4]: https://github.com/composer/composer/compare/1.10.3...1.10.4 [1.10.3]: https://github.com/composer/composer/compare/1.10.2...1.10.3 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..c93e4a6e0 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at contact@packagist.org. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/README.md b/README.md index 5423f90ed..a2a8cea69 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Composer helps you declare, manage, and install dependencies of PHP projects. See [https://getcomposer.org/](https://getcomposer.org/) for more information and documentation. -[![Build Status](https://travis-ci.org/composer/composer.svg?branch=master)](https://travis-ci.org/composer/composer) +[![Continuous Integration](https://github.com/composer/composer/workflows/Continuous%20Integration/badge.svg?branch=master)](https://github.com/composer/composer/actions) Installation / Usage -------------------- diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md index eb4acc53f..33729371a 100644 --- a/UPGRADE-2.0.md +++ b/UPGRADE-2.0.md @@ -2,11 +2,13 @@ ## For composer CLI users +- The new platform-check feature means that Composer checks the runtime PHP version and available extensions to ensure they match the project dependencies. If a mismatch is found, it exits with error details to make sure problems are not overlooked. To avoid issues when deploying to production it is recommended to run `composer check-platform-reqs` with the production PHP process as part of your build or deployment process. - If a packages exists in a higher priority repository, it will now be entirely ignored in lower priority repositories. See [repository priorities](https://getcomposer.org/repoprio) for details. - Invalid PSR-0 / PSR-4 class configurations will not autoload anymore in optimized-autoloader mode, as per the warnings introduced in 1.10 -- Package names now must comply to our naming guidelines or Composer will abort, as per the warnings introduced in 1.8.1 -- Removed --no-suggest flag as it is not needed anymore -- `update` now lists changes to the lock file first, and then the changes applied when installing the lock file to the vendor dir +- Package names now must comply to our [naming guidelines](doc/04-schema.md#name) or Composer will abort, as per the warnings introduced in 1.8.1 +- Deprecated --no-suggest flag as it is not needed anymore +- PEAR support (repository, downloader, etc.) has been removed +- `update` now lists changes to the lock file first (update step), and then the changes applied when installing the lock file to the vendor dir (install step) - `HTTPS_PROXY_REQUEST_FULLURI` if not specified will now default to false as this seems to work better in most environments ## For integrators and plugin authors @@ -24,7 +26,9 @@ - packages are now wrapped into a `"packages"` top level key instead of the whole file being the package array - packages now contain an `"installed-path"` key which lists where they were installed - there is a top level `"dev"` key which stores whether dev requirements were installed or not -- `PreFileDownloadEvent` now receives an `HttpDownloader` instance instead of `RemoteFilesystem`, and that instance can not be overriden by listeners anymore +- `PreFileDownloadEvent` now receives an `HttpDownloader` instance instead of `RemoteFilesystem`, and that instance can not be overridden by listeners anymore +- `VersionSelector::findBestCandidate`'s third argument (phpVersion) was removed in favor of passing in a complete PlatformRepository instance into the constructor +- `InitCommand::determineRequirements`'s fourth argument (phpVersion) should now receive a complete PlatformRepository instance or null if platform requirements are to be ignored - `IOInterface` now extends PSR-3's `LoggerInterface`, and has new `writeRaw` + `writeErrorRaw` methods - `RepositoryInterface` changes: - A new `loadPackages(array $packageNameMap, array $acceptableStabilities, array $stabilityFlags)` function was added for use during pool building @@ -39,6 +43,7 @@ - All packages get first downloaded, then all together prepared, then all together installed/updated/uninstalled, then finally cleanup is called for all. Therefore for error recovery it is important to avoid failing during install/update/uninstall as much as possible, and risky things or user prompts should happen in the prepare step rather. In case of failure, cleanup() will be called so that changes can be undone as much as possible. - If you used `RemoteFilesystem` you probably should use `HttpDownloader` instead now - `PRE_DEPENDENCIES_SOLVING` and `POST_DEPENDENCIES_SOLVING` events have been removed, use the new `PRE_OPERATIONS_EXEC` or other existing events instead or talk to us if you think you really need this +- The bundled composer/semver is now the 3.x range, see release notes for [2.0](https://github.com/composer/semver/releases/tag/2.0.0) and [3.0](https://github.com/composer/semver/releases/tag/3.0.0) for the minor breaking changes there ## For Composer repository implementors diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 15950da41..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,42 +0,0 @@ -build: false -clone_depth: 5 - -environment: - # This sets the PHP version (from Chocolatey) - PHPCI_CHOCO_VERSION: 7.3.14 - PHPCI_CACHE: C:\tools\phpci - PHPCI_PHP: C:\tools\phpci\php - PHPCI_COMPOSER: C:\tools\phpci\composer - -cache: - - '%PHPCI_CACHE% -> appveyor.yml' - -init: - - SET PATH=%PHPCI_PHP%;%PHPCI_COMPOSER%;%PATH% - - SET COMPOSER_HOME=%PHPCI_COMPOSER%\home - - SET COMPOSER_CACHE_DIR=%PHPCI_COMPOSER%\cache - - SET COMPOSER_NO_INTERACTION=1 - - SET PHP=0 - - SET ANSICON=121x90 (121x90) - -install: - - IF EXIST %PHPCI_CACHE% (SET PHP=1) - - IF %PHP%==0 cinst php -i -y --version %PHPCI_CHOCO_VERSION% --params "/InstallDir:%PHPCI_PHP%" - - IF %PHP%==0 cinst composer -i -y --ia "/DEV=%PHPCI_COMPOSER%" - - php -v - - IF %PHP%==0 (composer --version) ELSE (composer self-update) - - IF %PHP%==0 cd %PHPCI_PHP% - - IF %PHP%==0 copy php.ini-production php.ini /Y - - IF %PHP%==0 echo date.timezone="UTC" >> php.ini - - IF %PHP%==0 echo extension_dir=ext >> php.ini - - IF %PHP%==0 echo extension=php_openssl.dll >> php.ini - - IF %PHP%==0 echo extension=php_mbstring.dll >> php.ini - - IF %PHP%==0 echo extension=php_fileinfo.dll >> php.ini - - IF %PHP%==0 echo extension=php_intl.dll >> php.ini - - IF %PHP%==0 echo extension=php_curl.dll >> php.ini - - cd %APPVEYOR_BUILD_FOLDER% - - composer install --prefer-dist --no-progress - -test_script: - - cd %APPVEYOR_BUILD_FOLDER% - - vendor\bin\simple-phpunit --colors=always diff --git a/composer.json b/composer.json index e0ced56f3..024dc454a 100644 --- a/composer.json +++ b/composer.json @@ -24,24 +24,21 @@ "require": { "php": "^5.3.2 || ^7.0", "composer/ca-bundle": "^1.0", - "composer/semver": "^2.0@dev", + "composer/semver": "^3.0", "composer/spdx-licenses": "^1.2", "composer/xdebug-handler": "^1.1", - "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0", + "justinrainbow/json-schema": "^5.2.10", "psr/log": "^1.0", "seld/jsonlint": "^1.4", "seld/phar-utils": "^1.0", - "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0", - "symfony/filesystem": "^2.7 || ^3.0 || ^4.0 || ^5.0", - "symfony/finder": "^2.7 || ^3.0 || ^4.0 || ^5.0", - "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0", + "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0", + "symfony/filesystem": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0", + "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0", + "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0", "react/promise": "^1.2 || ^2.7" }, - "conflict": { - "symfony/console": "2.8.38" - }, "require-dev": { - "symfony/phpunit-bridge": "^3.4", + "symfony/phpunit-bridge": "^4.2 || ^5.0", "phpspec/prophecy": "^1.10" }, "suggest": { @@ -52,7 +49,8 @@ "config": { "platform": { "php": "5.3.9" - } + }, + "platform-check": false }, "extra": { "branch-alias": { diff --git a/composer.lock b/composer.lock index 69e9fb589..a958c8a20 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a0a9399315ac0b612d4296b8df745112", + "content-hash": "b39e04ca4a44810c96876dae02629707", "packages": [ { "name": "composer/ca-bundle", @@ -79,28 +79,29 @@ }, { "name": "composer/semver", - "version": "2.0.x-dev", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "4df5ff3249f01018504939d66040d8d2b783d820" + "reference": "3426bd5efa8a12d230824536c42a8a4ad30b7940" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/4df5ff3249f01018504939d66040d8d2b783d820", - "reference": "4df5ff3249f01018504939d66040d8d2b783d820", + "url": "https://api.github.com/repos/composer/semver/zipball/3426bd5efa8a12d230824536c42a8a4ad30b7940", + "reference": "3426bd5efa8a12d230824536c42a8a4ad30b7940", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.5 || ^5.0.5" + "phpstan/phpstan": "^0.12.19", + "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-master": "3.x-dev" } }, "autoload": { @@ -139,19 +140,23 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/2.0" + "source": "https://github.com/composer/semver/tree/3.0.0" }, "funding": [ { "url": "https://packagist.com", "type": "custom" }, + { + "url": "https://github.com/composer", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2020-03-11T13:41:23+00:00" + "time": "2020-05-26T18:22:04+00:00" }, { "name": "composer/spdx-licenses", @@ -220,16 +225,16 @@ }, { "name": "composer/xdebug-handler", - "version": "1.4.1", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7" + "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/1ab9842d69e64fb3a01be6b656501032d1b78cb7", - "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", + "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", "shasum": "" }, "require": { @@ -269,22 +274,30 @@ { "url": "https://packagist.com", "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" } ], - "time": "2020-03-01T12:26:26+00:00" + "time": "2020-06-04T11:16:35+00:00" }, { "name": "justinrainbow/json-schema", - "version": "5.2.9", + "version": "5.2.10", "source": { "type": "git", "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "44c6787311242a979fa15c704327c20e7221a0e4" + "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/44c6787311242a979fa15c704327c20e7221a0e4", - "reference": "44c6787311242a979fa15c704327c20e7221a0e4", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b", + "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b", "shasum": "" }, "require": { @@ -339,9 +352,9 @@ ], "support": { "issues": "https://github.com/justinrainbow/json-schema/issues", - "source": "https://github.com/justinrainbow/json-schema/tree/5.2.9" + "source": "https://github.com/justinrainbow/json-schema/tree/5.2.10" }, - "time": "2019-09-25T14:49:45+00:00" + "time": "2020-05-27T16:41:55+00:00" }, { "name": "psr/log", @@ -428,25 +441,35 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com" + } + ], "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/1.0" + }, "time": "2016-03-07T13:46:50+00:00" }, { "name": "seld/jsonlint", - "version": "1.7.2", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "e2e5d290e4d2a4f0eb449f510071392e00e10d19" + "reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/e2e5d290e4d2a4f0eb449f510071392e00e10d19", - "reference": "e2e5d290e4d2a4f0eb449f510071392e00e10d19", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1", + "reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1", "shasum": "" }, "require": { - "php": "^5.3 || ^7.0" + "php": "^5.3 || ^7.0 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" @@ -480,9 +503,19 @@ ], "support": { "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.7.2" + "source": "https://github.com/Seldaek/jsonlint/tree/master" }, - "time": "2019-10-24T14:27:39+00:00" + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2020-04-30T19:05:18+00:00" }, { "name": "seld/phar-utils", @@ -763,16 +796,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.15.0", + "version": "v1.17.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14" + "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14", - "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e94c8b1bbe2bc77507a1056cdb06451c75b427f9", + "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9", "shasum": "" }, "require": { @@ -784,7 +817,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" } }, "autoload": { @@ -818,7 +851,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.15.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.17.0" }, "funding": [ { @@ -834,20 +867,20 @@ "type": "tidelift" } ], - "time": "2020-02-27T09:26:54+00:00" + "time": "2020-05-12T16:14:59+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.15.0", + "version": "v1.17.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac" + "reference": "fa79b11539418b02fc5e1897267673ba2c19419c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac", - "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fa79b11539418b02fc5e1897267673ba2c19419c", + "reference": "fa79b11539418b02fc5e1897267673ba2c19419c", "shasum": "" }, "require": { @@ -859,7 +892,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" } }, "autoload": { @@ -894,7 +927,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.15.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.17.0" }, "funding": [ { @@ -910,7 +943,7 @@ "type": "tidelift" } ], - "time": "2020-03-09T19:04:49+00:00" + "time": "2020-05-12T16:47:27+00:00" }, { "name": "symfony/process", @@ -1398,23 +1431,23 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v3.4.39", + "version": "v4.2.12", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "c02893ae43532b46a4f0e0f207d088b939f278d9" + "reference": "80f9ffa6afcc27c7b00e8b8446b1d5d48d94bae7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/c02893ae43532b46a4f0e0f207d088b939f278d9", - "reference": "c02893ae43532b46a4f0e0f207d088b939f278d9", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/80f9ffa6afcc27c7b00e8b8446b1d5d48d94bae7", + "reference": "80f9ffa6afcc27c7b00e8b8446b1d5d48d94bae7", "shasum": "" }, "require": { "php": ">=5.3.3" }, "conflict": { - "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0|<6.4,>=6.0" + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" }, "suggest": { "symfony/debug": "For tracking deprecated interfaces usages at runtime with DebugClassLoader" @@ -1425,7 +1458,7 @@ "type": "symfony-bridge", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.2-dev" }, "thanks": { "name": "phpunit/phpunit", @@ -1460,30 +1493,14 @@ "description": "Symfony PHPUnit Bridge", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v3.4.38" + "source": "https://github.com/symfony/phpunit-bridge/tree/4.2" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-02-21T08:01:47+00:00" + "time": "2019-07-05T06:33:37+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "composer/semver": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/doc/00-intro.md b/doc/00-intro.md index 897d23609..ea88808a0 100644 --- a/doc/00-intro.md +++ b/doc/00-intro.md @@ -10,7 +10,7 @@ Composer is **not** a package manager in the same sense as Yum or Apt are. Yes, it deals with "packages" or libraries, but it manages them on a per-project basis, installing them in a directory (e.g. `vendor`) inside your project. By default it does not install anything globally. Thus, it is a dependency -manager. It does however support a "global" project for convenience via the +manager. It does however support a "global" project for convenience via the [global](03-cli.md#global) command. This idea is not new and Composer is strongly inspired by node's @@ -58,7 +58,7 @@ project, or globally as a system wide executable. #### Locally -To install Composer locally, run the installer in your project directory. See +To install Composer locally, run the installer in your project directory. See [the Download page](https://getcomposer.org/download/) for instructions. The installer will check a few PHP settings and then download `composer.phar` @@ -134,8 +134,16 @@ to download `composer.phar`. Create a new `composer.bat` file alongside `composer.phar`: +Using cmd.exe: + ```sh -C:\bin>echo @php "%~dp0composer.phar" %*>composer.bat +C:\bin> echo @php "%~dp0composer.phar" %*>composer.bat +``` + +Using PowerShell: + +```sh +PS C:\bin> Set-Content composer.bat '@php "%~dp0composer.phar" %*' ``` Add the directory to your PATH environment variable if it isn't already. diff --git a/doc/03-cli.md b/doc/03-cli.md index 3a964c390..374ef952e 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -112,9 +112,13 @@ resolution. * **--classmap-authoritative (-a):** Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`. * **--apcu-autoloader:** Use APCu to cache found/not-found classes. -* **--ignore-platform-reqs:** ignore `php`, `hhvm`, `lib-*` and `ext-*` - requirements and force the installation even if the local machine does not - fulfill these. See also the [`platform`](06-config.md#platform) config option. +* **--ignore-platform-reqs:** ignore all platform requirements (`php`, `hhvm`, + `lib-*` and `ext-*`) and force the installation even if the local machine does + not fulfill these. + See also the [`platform`](06-config.md#platform) config option. +* **--ignore-platform-req:** ignore a specific platform requirement(`php`, + `hhvm`, `lib-*` and `ext-*`) and force the installation even if the local machine + does not fulfill it. ## update / u @@ -142,6 +146,26 @@ You can also use wildcards to update a bunch of packages at once: php composer.phar update "vendor/*" ``` + +If you want to downgrade a package to a specific version without changing your +composer.json you can use `--with` and provide a custom version constraint: + +```sh +php composer.phar update --with vendor/package:2.0.1 +``` + +The custom constraint has to be a subset of the existing constraint you have, +and this feature is only available for your root package dependencies. + +If you only want to update the package(s) for which you provide custom constraints +using `--with`, you can skip `--with` and just use constraints with the partial +update syntax: + +```sh +php composer.phar update vendor/package:2.0.1 vendor/package2:3.0.* +``` + + ### Options * **--prefer-source:** Install packages from `source` when available. @@ -149,8 +173,10 @@ php composer.phar update "vendor/*" * **--dry-run:** Simulate the command without actually doing anything. * **--dev:** Install packages listed in `require-dev` (this is the default behavior). * **--no-dev:** Skip installing packages listed in `require-dev`. The autoloader generation skips the `autoload-dev` rules. +* **--no-install:** Does not run the install step after updating the composer.lock file. * **--lock:** Only updates the lock file hash to suppress warning about the lock file being out of date. +* **--with:** Temporary version constraint to add, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 * **--no-autoloader:** Skips autoloader generation. * **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--no-progress:** Removes the progress display that can mess with some @@ -163,15 +189,21 @@ php composer.phar update "vendor/*" * **--classmap-authoritative (-a):** Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`. * **--apcu-autoloader:** Use APCu to cache found/not-found classes. -* **--ignore-platform-reqs:** ignore `php`, `hhvm`, `lib-*` and `ext-*` - requirements and force the installation even if the local machine does not - fulfill these. See also the [`platform`](06-config.md#platform) config option. +* **--ignore-platform-reqs:** ignore all platform requirements (`php`, `hhvm`, + `lib-*` and `ext-*`) and force the installation even if the local machine does + not fulfill these. + See also the [`platform`](06-config.md#platform) config option. +* **--ignore-platform-req:** ignore a specific platform requirement(`php`, + `hhvm`, `lib-*` and `ext-*`) and force the installation even if the local machine + does not fulfill it. * **--prefer-stable:** Prefer stable versions of dependencies. * **--prefer-lowest:** Prefer lowest versions of dependencies. Useful for testing minimal versions of requirements, generally used with `--prefer-stable`. * **--interactive:** Interactive interface with autocompletion to select the packages to update. * **--root-reqs:** Restricts the update to your first degree dependencies. +Specifying one of the words `mirrors`, `lock`, or `nothing` as an argument has the same effect as specifying the option `--lock`, for example `composer update mirrors` is exactly the same as `composer update --lock`. + ## require The `require` command adds new packages to the `composer.json` file from @@ -201,14 +233,19 @@ If you do not specify a package, composer will prompt you to search for a packag * **--prefer-dist:** Install packages from `dist` when available. * **--no-progress:** Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters. -* **--no-update:** Disables the automatic update of the dependencies. +* **--no-update:** Disables the automatic update of the dependencies (implies --no-install). +* **--no-install:** Does not run the install step after updating the composer.lock file. * **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--update-no-dev:** Run the dependency update with the `--no-dev` option. * **--update-with-dependencies:** Also update dependencies of the newly required packages, except those that are root requirements. * **--update-with-all-dependencies:** Also update dependencies of the newly required packages, including those that are root requirements. -* **--ignore-platform-reqs:** ignore `php`, `hhvm`, `lib-*` and `ext-*` - requirements and force the installation even if the local machine does not - fulfill these. See also the [`platform`](06-config.md#platform) config option. +* **--ignore-platform-reqs:** ignore all platform requirements (`php`, `hhvm`, + `lib-*` and `ext-*`) and force the installation even if the local machine does + not fulfill these. + See also the [`platform`](06-config.md#platform) config option. +* **--ignore-platform-req:** ignore a specific platform requirement(`php`, + `hhvm`, `lib-*` and `ext-*`) and force the installation even if the local machine + does not fulfill it. * **--prefer-stable:** Prefer stable versions of dependencies. * **--prefer-lowest:** Prefer lowest versions of dependencies. Useful for testing minimal versions of requirements, generally used with `--prefer-stable`. @@ -237,13 +274,18 @@ uninstalled. * **--dry-run:** Simulate the command without actually doing anything. * **--no-progress:** Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters. -* **--no-update:** Disables the automatic update of the dependencies. +* **--no-update:** Disables the automatic update of the dependencies (implies --no-install). +* **--no-install:** Does not run the install step after updating the composer.lock file. * **--no-scripts:** Skips execution of scripts defined in `composer.json`. * **--update-no-dev:** Run the dependency update with the --no-dev option. * **--update-with-dependencies:** Also update dependencies of the removed packages. -* **--ignore-platform-reqs:** ignore `php`, `hhvm`, `lib-*` and `ext-*` - requirements and force the installation even if the local machine does not - fulfill these. See also the [`platform`](06-config.md#platform) config option. +* **--ignore-platform-reqs:** ignore all platform requirements (`php`, `hhvm`, + `lib-*` and `ext-*`) and force the installation even if the local machine does + not fulfill these. + See also the [`platform`](06-config.md#platform) config option. +* **--ignore-platform-req:** ignore a specific platform requirement(`php`, + `hhvm`, `lib-*` and `ext-*`) and force the installation even if the local machine + does not fulfill it. * **--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. @@ -357,6 +399,7 @@ php composer.phar show monolog/monolog 1.0.2 * **--all :** List all packages available in all your repositories. * **--installed (-i):** List the packages that are installed (this is enabled by default, and deprecated). +* **--locked:** List the locked packages from composer.lock. * **--platform (-p):** List only platform packages (php & extensions). * **--available (-a):** List available packages only. * **--self (-s):** List the root package info. @@ -605,6 +648,8 @@ See the [Config](06-config.md) chapter for valid configuration options. that this cannot be used in conjunction with the `--global` option. * **--absolute:** Returns absolute paths when fetching *-dir config values instead of relative. +* **--json:** JSON decode the setting value, to be used with `extra.*` keys. +* **--merge:** Merge the setting value with the current value, to be used with `extra.*` keys in combination with `--json`. ### Modifying Repositories @@ -633,6 +678,13 @@ php composer.phar config extra.foo.bar value The dots indicate array nesting, a max depth of 3 levels is allowed though. The above would set `"extra": { "foo": { "bar": "value" } }`. +If you have a complex value to add/modify, you can use the `--json` and `--merge` flags +to edit extra fields as json: + +```sh +php composer.phar config --json extra.foo.bar '{"baz": true, "qux": []}' +``` + ## create-project You can use Composer to create new projects from an existing package. This is @@ -686,9 +738,13 @@ By default the command checks for the packages on packagist.org. mode. * **--remove-vcs:** Force-remove the VCS metadata without prompting. * **--no-install:** Disables installation of the vendors. -* **--ignore-platform-reqs:** ignore `php`, `hhvm`, `lib-*` and `ext-*` - requirements and force the installation even if the local machine does not - fulfill these. +* **--ignore-platform-reqs:** ignore all platform requirements (`php`, `hhvm`, + `lib-*` and `ext-*`) and force the installation even if the local machine does + not fulfill these. + See also the [`platform`](06-config.md#platform) config option. +* **--ignore-platform-req:** ignore a specific platform requirement(`php`, + `hhvm`, `lib-*` and `ext-*`) and force the installation even if the local machine + does not fulfill it. ## dump-autoload (dumpautoload) @@ -712,6 +768,11 @@ performance. Implicitly enables `--optimize`. * **--apcu:** Use APCu to cache found/not-found classes. * **--no-dev:** Disables autoload-dev rules. +* **--ignore-platform-reqs:** ignore all `php`, `hhvm`, `lib-*` and `ext-*` + requirements and skip the [platform check](07-runtime.md#platform-check) for these. + See also the [`platform`](06-config.md#platform) config option. +* **--ignore-platform-req:** ignore a specific platform requirement (`php`, `hhvm`, + `lib-*` and `ext-*`) and skip the [platform check](07-runtime.md#platform-check) for it. ## clear-cache / clearcache / cc diff --git a/doc/04-schema.md b/doc/04-schema.md index 7b67f4114..8bdad86ee 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -1,4 +1,4 @@ -# The composer.json Schema +# The composer.json schema This chapter will explain all of the fields available in `composer.json`. @@ -34,12 +34,12 @@ separated by `/`. Examples: * monolog/monolog * igorw/event-source -The name can contain any character, including white spaces, and it's case -insensitive (`foo/bar` and `Foo/Bar` are considered the same package). In order -to simplify its installation, it's recommended to define a short and lowercase -name that doesn't include non-alphanumeric characters or white spaces. +The name must be lowercased and consist of words separated by `-`, `.` or `_`. +The complete name should match `^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$`. -Required for published packages (libraries). +The `name` property is required for published packages (libraries). + +> **Note:** Before Composer version 2.0, a name could contain any character, including white spaces. ### description @@ -610,6 +610,18 @@ Example: } ``` +Wildcards (`*`) are also supported in a classmap paths, and expand to match any directory name: + +Example: + +```json +{ + "autoload": { + "classmap": ["src/addons/*/lib/", "3rd-party/*", "Something.php"] + } +} +``` + #### Files If you want to require certain files explicitly on every request then you can use @@ -877,6 +889,21 @@ A set of options for creating package archives. The following options are supported: +* **name:** Allows configuring base name for archive. + By default (if not configured, and `--file` is not passed as command-line argument), + `preg_replace('#[^a-z0-9-_]#i', '-', name)` is used. + +Example: + +```json +{ + "name": "org/strangeName", + "archive": { + "name": "Strange_name" + } +} +``` + * **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 diff --git a/doc/06-config.md b/doc/06-config.md index 603de014a..30f31b98e 100644 --- a/doc/06-config.md +++ b/doc/06-config.md @@ -85,9 +85,13 @@ gitlab.com the domain names must be also specified with the ## gitlab-token -A list of domain names and private tokens. For example using `{"gitlab.com": +A list of domain names and private tokens. Private token can be either simple +string, or array with username and token. For example using `{"gitlab.com": "privatetoken"}` as the value of this option will use `privatetoken` to access -private repositories on gitlab. Please note: If the package is not hosted at +private repositories on gitlab. Using `{"gitlab.com": {"username": "gitlabuser", + "token": "privatetoken"}}` will use both username and token for gitlab deploy +token functionality (https://docs.gitlab.com/ee/user/project/deploy_tokens/) +Please note: If the package is not hosted at gitlab.com the domain names must be also specified with the [`gitlab-domains`](06-config.md#gitlab-domains) option. @@ -307,4 +311,9 @@ in the composer home, cache, and data directories. Defaults to `true`. If set to `false`, Composer will not create a `composer.lock` file. -← [Repositories](05-repositories.md) | [Community](07-community.md) → +## platform-check + +Defaults to `true`. If set to `false`, Composer will not create and require a +`platform_check.php` file as part of the autoloader bootstrap. + +← [Repositories](05-repositories.md) | [Runtime](07-runtime.md) → diff --git a/doc/07-runtime.md b/doc/07-runtime.md new file mode 100644 index 000000000..4faef5e07 --- /dev/null +++ b/doc/07-runtime.md @@ -0,0 +1,109 @@ +# Runtime Composer utilities + +While Composer is mostly used around your project to install its dependencies, +there are a few things which are made available to you at runtime. + +If you need to rely on some of these in a specific version, you can require +the `composer-runtime-api` package. + +## Autoload + +The autoloader is the most used one, and is already covered in our +[basic usage guide](#01-basic-usage.md#autoloading). It is available in all +Composer versions. + +## Installed versions + +composer-runtime-api 2.0 introduced a new `Composer\InstalledVersions` class which offers +a few static methods to inspect which versions are currently installed. This is +automatically available to your code as long as you include the Composer autoloader. + +The main use cases for this class are the following: + +### Knowing whether package X (or virtual package) is present + +```php +\Composer\InstalledVersions::isInstalled('vendor/package'); // returns bool +\Composer\InstalledVersions::isInstalled('psr/log-implementation'); // returns bool +``` + +Note that this can not be used to check whether platform packages are installed. + +### Knowing whether package X is installed in version Y + +> **Note:** To use this, your package must require `"composer/semver": "^2.0"`. + +```php +use Composer\Semver\VersionParser; + +\Composer\InstalledVersions::satisfies(new VersionParser, 'vendor/package', '2.0.*'); +\Composer\InstalledVersions::satisfies(new VersionParser, 'psr/log-implementation', '^1.0'); +``` + +This will return true if e.g. vendor/package is installed in a version matching +`2.0.*`, but also if the given package name is replaced or provided by some other +package. + +### Knowing the version of package X + +> **Note:** This will return `null` if the package name you ask for is not itself installed +> but merely provided or replaced by another package. We therefore recommend using satisfies() +> in library code at least. In application code you have a bit more control and it is less +> important. + +```php +// returns a normalized version (e.g. 1.2.3.0) if vendor/package is installed, +// or null if it is provided/replaced, +// or throws OutOfBoundsException if the package is not installed at all +\Composer\InstalledVersions::getVersion('vendor/package'); +``` + +```php +// returns the original version (e.g. v1.2.3) if vendor/package is installed, +// or null if it is provided/replaced, +// or throws OutOfBoundsException if the package is not installed at all +\Composer\InstalledVersions::getPrettyVersion('vendor/package'); +``` + +```php +// returns the package dist or source reference (e.g. a git commit hash) if vendor/package is installed, +// or null if it is provided/replaced, +// or throws OutOfBoundsException if the package is not installed at all +\Composer\InstalledVersions::getReference('vendor/package'); +``` + +### Knowing a package's own installed version + +If you are only interested in getting a package's own version, e.g. in the source of acme/foo you want +to know which version acme/foo is currently running to display that to the user, then it is +acceptable to use getVersion/getPrettyVersion/getReference. + +The warning in the section above does not apply in this case as you are sure the package is present +and not being replaced if your code is running. + +It is nonetheless a good idea to make sure you handle the `null` return value as gracefully as +possible for safety. + +---- + +A few other methods are available for more complex usages, please refer to the +source/docblocks of the class itself. + +## Platform check + +composer-runtime-api 2.0 introduced a new `vendor/composer/platform_check.php` file, which +is included automatically when you include the Composer autoloader. + +It verifies that platform requirements (i.e. php and php extensions) are fulfilled +by the PHP process currently running. If the requirements are not met, the script +prints a warning with the missing requirements and exits with code 104. + +To avoid an unexpected white page of death with some obscure PHP extension warning in +production, you can run `composer check-platform-reqs --no-dev` as part of your +deployment/build and if that returns a non-0 code you should abort. + +If you for some reason do not want to use this safety check, and would rather +risk runtime errors when your code executes, you can disable this by setting the +[`platform-check`](06-config.md#platform-check) config option to `false`. + +← [Config](06-config.md) | [Community](08-community.md) → diff --git a/doc/07-community.md b/doc/08-community.md similarity index 97% rename from doc/07-community.md rename to doc/08-community.md index 0a45827ec..fc140e0c7 100644 --- a/doc/07-community.md +++ b/doc/08-community.md @@ -32,4 +32,4 @@ for users and [#composer-dev](irc://irc.freenode.org/composer-dev) for developme Stack Overflow has a growing collection of [Composer related questions](https://stackoverflow.com/questions/tagged/composer-php). -← [Config](06-config.md) +← [Config](07-runtime.md) diff --git a/doc/articles/autoloader-optimization.md b/doc/articles/autoloader-optimization.md index 41bd4a6cb..11faac8a7 100644 --- a/doc/articles/autoloader-optimization.md +++ b/doc/articles/autoloader-optimization.md @@ -2,7 +2,7 @@ tagline: How to reduce the performance impact of the autoloader --> -# Autoloader Optimization +# Autoloader optimization By default, the Composer autoloader runs relatively fast. However, due to the way PSR-4 and PSR-0 autoloading rules are set up, it needs to check the filesystem diff --git a/doc/articles/scripts.md b/doc/articles/scripts.md index fae33f614..bbaea9c89 100644 --- a/doc/articles/scripts.md +++ b/doc/articles/scripts.md @@ -375,4 +375,7 @@ You can set custom script descriptions with the following in your `composer.json } ``` +The descriptions are used in `composer list` or `composer run -l` commands to +describe what the scripts do when the command is run. + > **Note:** You can only set custom descriptions of custom commands. diff --git a/phpstan/autoload.php b/phpstan/autoload.php deleted file mode 100644 index 7d1ed7671..000000000 --- a/phpstan/autoload.php +++ /dev/null @@ -1,5 +0,0 @@ -eventDispatcher = $eventDispatcher; @@ -101,6 +109,26 @@ class AutoloadGenerator $this->runScripts = (bool) $runScripts; } + /** + * Sets whether platform requirements should be ignored + * + * If this is set to true, the platform check file will not be generated + * If this is set to false, the platform check file will be generated with all requirements + * If this is set to string[], those packages will be ignored from the platform check file + * + * @param array|bool $ignorePlatformReqs + */ + public function setIgnorePlatformRequirements($ignorePlatformReqs) + { + if (is_array($ignorePlatformReqs)) { + $this->ignorePlatformReqs = array_filter($ignorePlatformReqs, function ($req) { + return (bool) preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); + }); + } else { + $this->ignorePlatformReqs = (bool) $ignorePlatformReqs; + } + } + public function dump(Config $config, InstalledRepositoryInterface $localRepo, PackageInterface $mainPackage, InstallationManager $installationManager, $targetDir, $scanPsrPackages = false, $suffix = '') { if ($this->classMapAuthoritative) { @@ -276,6 +304,7 @@ EOF; ); } + $classMap['Composer\\InstalledVersions'] = "\$vendorDir . '/composer/InstalledVersions.php',\n"; ksort($classMap); foreach ($classMap as $class => $code) { $classmapFile .= ' '.var_export($class, true).' => '.$code; @@ -295,27 +324,40 @@ EOF; } } - $this->filePutContentsIfModified($targetDir.'/autoload_namespaces.php', $namespacesFile); - $this->filePutContentsIfModified($targetDir.'/autoload_psr4.php', $psr4File); - $this->filePutContentsIfModified($targetDir.'/autoload_classmap.php', $classmapFile); + $filesystem->filePutContentsIfModified($targetDir.'/autoload_namespaces.php', $namespacesFile); + $filesystem->filePutContentsIfModified($targetDir.'/autoload_psr4.php', $psr4File); + $filesystem->filePutContentsIfModified($targetDir.'/autoload_classmap.php', $classmapFile); $includePathFilePath = $targetDir.'/include_paths.php'; if ($includePathFileContents = $this->getIncludePathsFile($packageMap, $filesystem, $basePath, $vendorPath, $vendorPathCode52, $appBaseDirCode)) { - $this->filePutContentsIfModified($includePathFilePath, $includePathFileContents); + $filesystem->filePutContentsIfModified($includePathFilePath, $includePathFileContents); } elseif (file_exists($includePathFilePath)) { unlink($includePathFilePath); } $includeFilesFilePath = $targetDir.'/autoload_files.php'; if ($includeFilesFileContents = $this->getIncludeFilesFile($autoloads['files'], $filesystem, $basePath, $vendorPath, $vendorPathCode52, $appBaseDirCode)) { - $this->filePutContentsIfModified($includeFilesFilePath, $includeFilesFileContents); + $filesystem->filePutContentsIfModified($includeFilesFilePath, $includeFilesFileContents); } elseif (file_exists($includeFilesFilePath)) { unlink($includeFilesFilePath); } - $this->filePutContentsIfModified($targetDir.'/autoload_static.php', $this->getStaticFile($suffix, $targetDir, $vendorPath, $basePath, $staticPhpVersion)); - $this->filePutContentsIfModified($vendorPath.'/autoload.php', $this->getAutoloadFile($vendorPathToTargetDirCode, $suffix)); - $this->filePutContentsIfModified($targetDir.'/autoload_real.php', $this->getAutoloadRealFile(true, (bool) $includePathFileContents, $targetDirLoader, (bool) $includeFilesFileContents, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader, $staticPhpVersion)); + $filesystem->filePutContentsIfModified($targetDir.'/autoload_static.php', $this->getStaticFile($suffix, $targetDir, $vendorPath, $basePath, $staticPhpVersion)); + $checkPlatform = $config->get('platform-check') && $this->ignorePlatformReqs !== true; + $platformCheckContent = null; + if ($checkPlatform) { + $platformCheckContent = $this->getPlatformCheck($packageMap, $this->ignorePlatformReqs ?: array()); + if (null === $platformCheckContent) { + $checkPlatform = false; + } + } + if ($checkPlatform) { + $filesystem->filePutContentsIfModified($targetDir.'/platform_check.php', $platformCheckContent); + } elseif (file_exists($targetDir.'/platform_check.php')) { + unlink($targetDir.'/platform_check.php'); + } + $filesystem->filePutContentsIfModified($vendorPath.'/autoload.php', $this->getAutoloadFile($vendorPathToTargetDirCode, $suffix)); + $filesystem->filePutContentsIfModified($targetDir.'/autoload_real.php', $this->getAutoloadRealFile(true, (bool) $includePathFileContents, $targetDirLoader, (bool) $includeFilesFileContents, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader, $staticPhpVersion, $checkPlatform)); - $this->safeCopy(__DIR__.'/ClassLoader.php', $targetDir.'/ClassLoader.php'); - $this->safeCopy(__DIR__.'/../../../LICENSE', $targetDir.'/LICENSE'); + $filesystem->safeCopy(__DIR__.'/ClassLoader.php', $targetDir.'/ClassLoader.php'); + $filesystem->safeCopy(__DIR__.'/../../../LICENSE', $targetDir.'/LICENSE'); if ($this->runScripts) { $this->eventDispatcher->dispatchScript(ScriptEvents::POST_AUTOLOAD_DUMP, $this->devMode, array(), array( @@ -326,16 +368,6 @@ EOF; return count($classMap); } - private function filePutContentsIfModified($path, $content) - { - $currentContent = @file_get_contents($path); - if (!$currentContent || ($currentContent != $content)) { - return file_put_contents($path, $content); - } - - return 0; - } - private function addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist, $namespaceFilter, $autoloadType, array $classMap, array &$ambiguousClasses, array &$scannedFiles) { foreach ($this->generateClassMap($dir, $blacklist, $namespaceFilter, $autoloadType, true, $scannedFiles) as $class => $path) { @@ -570,6 +602,135 @@ EOF; return $baseDir . (($path !== false) ? var_export($path, true) : ""); } + protected function getPlatformCheck($packageMap, array $ignorePlatformReqs) + { + $lowestPhpVersion = Bound::zero(); + $requiredExtensions = array(); + $extensionProviders = array(); + + foreach ($packageMap as $item) { + list($package, $installPath) = $item; + foreach (array_merge($package->getReplaces(), $package->getProvides()) as $link) { + if (preg_match('{^ext-(.+)$}iD', $link->getTarget(), $match)) { + $extensionProviders[$match[1]][] = $link->getConstraint() ?: new MatchAllConstraint(); + } + } + } + + foreach ($packageMap as $item) { + list($package, $installPath) = $item; + foreach ($package->getRequires() as $link) { + if (in_array($link->getTarget(), $ignorePlatformReqs, true)) { + continue; + } + + if ('php' === $link->getTarget() && ($constraint = $link->getConstraint())) { + if ($constraint->getLowerBound()->compareTo($lowestPhpVersion, '>')) { + $lowestPhpVersion = $constraint->getLowerBound(); + } + } + + if (preg_match('{^ext-(.+)$}iD', $link->getTarget(), $match)) { + // skip extension checks if they have a valid provider/replacer + if (isset($extensionProviders[$match[1]])) { + foreach ($extensionProviders[$match[1]] as $provided) { + if (!$link->getConstraint() || $provided->matches($link->getConstraint())) { + continue 2; + } + } + } + + $extension = var_export($match[1], true); + if ($match[1] === 'pcntl' || $match[1] === 'readline') { + $requiredExtensions[$extension] = "PHP_SAPI !== 'cli' || extension_loaded($extension) || \$missingExtensions[] = $extension;\n"; + } else { + $requiredExtensions[$extension] = "extension_loaded($extension) || \$missingExtensions[] = $extension;\n"; + } + } + } + } + + ksort($requiredExtensions); + + $formatToPhpVersionId = function (Bound $bound) { + if ($bound->isZero()) { + return 0; + } + + if ($bound->isPositiveInfinity()) { + return 99999; + } + + $version = str_replace('-', '.', $bound->getVersion()); + $chunks = array_map('intval', explode('.', $version)); + return $chunks[0] * 10000 + $chunks[1] * 100 + $chunks[2]; + }; + + $formatToHumanReadable = function (Bound $bound) { + if ($bound->isZero()) { + return 0; + } + + if ($bound->isPositiveInfinity()) { + return 99999; + } + + $version = str_replace('-', '.', $bound->getVersion()); + $chunks = explode('.', $version); + $chunks = array_slice($chunks, 0, 3); + return implode('.', $chunks); + }; + + $requiredPhp = ''; + $requiredPhpError = ''; + if (!$lowestPhpVersion->isZero()) { + $operator = $lowestPhpVersion->isInclusive() ? '>=' : '>'; + $requiredPhp = 'PHP_VERSION_ID '.$operator.' '.$formatToPhpVersionId($lowestPhpVersion); + $requiredPhpError = '"'.$operator.' '.$formatToHumanReadable($lowestPhpVersion).'"'; + } + + if ($requiredPhp) { + $requiredPhp = <<= $staticPhpVersion && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if (\$useStaticLoader) { - require_once __DIR__ . '/autoload_static.php'; + require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInit$suffix::getInitializer(\$loader)); } else { @@ -1020,51 +1193,4 @@ INITIALIZER; return $sortedPackageMap; } - - /** - * Copy file using stream_copy_to_stream to work around https://bugs.php.net/bug.php?id=6463 - * - * @param string $source - * @param string $target - */ - protected function safeCopy($source, $target) - { - if (!file_exists($target) || !file_exists($source) || !$this->filesAreEqual($source, $target)) { - $source = fopen($source, 'r'); - $target = fopen($target, 'w+'); - - stream_copy_to_stream($source, $target); - fclose($source); - fclose($target); - } - } - - /** - * compare 2 files - * https://stackoverflow.com/questions/3060125/can-i-use-file-get-contents-to-compare-two-files - */ - private function filesAreEqual($a, $b) - { - // Check if filesize is different - if (filesize($a) !== filesize($b)) { - return false; - } - - // Check if content is different - $ah = fopen($a, 'rb'); - $bh = fopen($b, 'rb'); - - $result = true; - while (!feof($ah)) { - if (fread($ah, 8192) != fread($bh, 8192)) { - $result = false; - break; - } - } - - fclose($ah); - fclose($bh); - - return $result; - } } diff --git a/src/Composer/Autoload/ClassMapGenerator.php b/src/Composer/Autoload/ClassMapGenerator.php index f242a0594..b86a4e9ce 100644 --- a/src/Composer/Autoload/ClassMapGenerator.php +++ b/src/Composer/Autoload/ClassMapGenerator.php @@ -65,7 +65,7 @@ class ClassMapGenerator if (is_string($path)) { if (is_file($path)) { $path = array(new \SplFileInfo($path)); - } elseif (is_dir($path)) { + } elseif (is_dir($path) || strpos($path, '*') !== false) { $path = Finder::create()->files()->followLinks()->name('/\.(php|inc|hh)$/')->in($path); } else { throw new \RuntimeException( diff --git a/src/Composer/Command/BaseCommand.php b/src/Composer/Command/BaseCommand.php index 56ee9f7f4..8c45899cf 100644 --- a/src/Composer/Command/BaseCommand.php +++ b/src/Composer/Command/BaseCommand.php @@ -19,6 +19,7 @@ use Composer\Factory; use Composer\IO\IOInterface; use Composer\IO\NullIO; use Composer\Plugin\PreCommandRunEvent; +use Composer\Package\Version\VersionParser; use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -180,4 +181,25 @@ abstract class BaseCommand extends Command return array($preferSource, $preferDist); } + + protected function formatRequirements(array $requirements) + { + $requires = array(); + $requirements = $this->normalizeRequirements($requirements); + foreach ($requirements as $requirement) { + if (!isset($requirement['version'])) { + throw new \UnexpectedValueException('Option '.$requirement['name'] .' is missing a version constraint, use e.g. '.$requirement['name'].':^1.0'); + } + $requires[$requirement['name']] = $requirement['version']; + } + + return $requires; + } + + protected function normalizeRequirements(array $requirements) + { + $parser = new VersionParser(); + + return $parser->parseNameVersionPairs($requirements); + } } diff --git a/src/Composer/Command/CheckPlatformReqsCommand.php b/src/Composer/Command/CheckPlatformReqsCommand.php index 68b55932d..c80884444 100644 --- a/src/Composer/Command/CheckPlatformReqsCommand.php +++ b/src/Composer/Command/CheckPlatformReqsCommand.php @@ -20,6 +20,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Repository\PlatformRepository; +use Composer\Repository\RootPackageRepository; use Composer\Repository\InstalledRepository; class CheckPlatformReqsCommand extends BaseCommand @@ -47,7 +48,7 @@ EOT { $composer = $this->getComposer(); - $requires = $composer->getPackage()->getRequires(); + $requires = array(); if ($input->getOption('no-dev')) { $installedRepo = $composer->getLocker()->getLockedRepository(!$input->getOption('no-dev')); $dependencies = $installedRepo->getPackages(); @@ -63,7 +64,7 @@ EOT $requires[$require] = array($link); } - $installedRepo = new InstalledRepository(array($installedRepo)); + $installedRepo = new InstalledRepository(array($installedRepo, new RootPackageRepository($composer->getPackage()))); foreach ($installedRepo->getPackages() as $package) { foreach ($package->getRequires() as $require => $link) { $requires[$require][] = $link; diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index 2695f0399..099e5c872 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -73,6 +73,8 @@ class ConfigCommand extends BaseCommand 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'), new InputOption('absolute', null, InputOption::VALUE_NONE, 'Returns absolute paths when fetching *-dir config values instead of relative'), + new InputOption('json', 'j', InputOption::VALUE_NONE, 'JSON decode the setting value, to be used with extra.* keys'), + new InputOption('merge', 'm', InputOption::VALUE_NONE, 'Merge the setting value with the current value, to be used with extra.* keys in combination with --json'), new InputArgument('setting-key', null, 'Setting key'), new InputArgument('setting-value', InputArgument::IS_ARRAY, 'Setting value'), )) @@ -119,6 +121,10 @@ To add or edit extra properties you can use: %command.full_name% extra.property value +Or to add a complex value you can use json with: + + %command.full_name% extra.property --json '{"foo":true, "bar": []}' + To edit the file in an external editor: %command.full_name% --editor @@ -421,6 +427,7 @@ EOT 'github-expose-hostname' => array($booleanValidator, $booleanNormalizer), 'htaccess-protect' => array($booleanValidator, $booleanNormalizer), 'lock' => array($booleanValidator, $booleanNormalizer), + 'platform-check' => array($booleanValidator, $booleanNormalizer), ); $multiConfigValues = array( 'github-protocols' => array( @@ -621,7 +628,21 @@ EOT return 0; } - $this->configSource->addProperty($settingKey, $values[0]); + $value = $values[0]; + if ($input->getOption('json')) { + $value = JsonFile::parseJson($value); + if ($input->getOption('merge')) { + $currentValue = $this->configFile->read(); + $bits = explode('.', $settingKey); + foreach ($bits as $bit) { + $currentValue = isset($currentValue[$bit]) ? $currentValue[$bit] : null; + } + if (is_array($currentValue)) { + $value = array_merge($currentValue, $value); + } + } + } + $this->configSource->addProperty($settingKey, $value); return 0; } diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 9263ba7ee..07ff8bed8 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -80,7 +80,8 @@ class CreateProjectCommand extends BaseCommand new InputOption('keep-vcs', null, InputOption::VALUE_NONE, 'Whether to prevent deleting the vcs folder.'), new InputOption('remove-vcs', null, InputOption::VALUE_NONE, 'Whether to force deletion of the vcs folder without prompting.'), new InputOption('no-install', null, InputOption::VALUE_NONE, 'Whether to skip installation of the package dependencies.'), - new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'), + new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), + new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), )) ->setHelp( <<setOption('no-plugins', true); } + $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); + return $this->installProject( $io, $config, @@ -143,7 +146,7 @@ EOT $input->getOption('no-scripts'), $input->getOption('no-progress'), $input->getOption('no-install'), - $input->getOption('ignore-platform-reqs'), + $ignorePlatformReqs, !$input->getOption('no-secure-http'), $input->getOption('add-repository') ); @@ -336,32 +339,24 @@ EOT $repositorySet = new RepositorySet($stability); $repositorySet->addRepository($sourceRepo); - $phpVersion = null; - $prettyPhpVersion = null; - if (!$ignorePlatformReqs) { - $platformOverrides = $config->get('platform') ?: array(); - // initialize $this->repos as it is used by the parent InitCommand - $platform = new PlatformRepository(array(), $platformOverrides); - $phpPackage = $platform->findPackage('php', '*'); - $phpVersion = $phpPackage->getVersion(); - $prettyPhpVersion = $phpPackage->getPrettyVersion(); - } + $platformOverrides = $config->get('platform') ?: array(); + $platformRepo = new PlatformRepository(array(), $platformOverrides); // find the latest version if there are multiple - $versionSelector = new VersionSelector($repositorySet); - $package = $versionSelector->findBestCandidate($name, $packageVersion, $phpVersion, $stability); + $versionSelector = new VersionSelector($repositorySet, $platformRepo); + $package = $versionSelector->findBestCandidate($name, $packageVersion, $stability, $ignorePlatformReqs); if (!$package) { $errorMessage = "Could not find package $name with " . ($packageVersion ? "version $packageVersion" : "stability $stability"); - if ($phpVersion && $versionSelector->findBestCandidate($name, $packageVersion, null, $stability)) { - throw new \InvalidArgumentException($errorMessage .' in a version installable using your PHP version '.$prettyPhpVersion.'.'); + if (true !== $ignorePlatformReqs && $versionSelector->findBestCandidate($name, $packageVersion, $stability, true)) { + throw new \InvalidArgumentException($errorMessage .' in a version installable using your PHP version, PHP extensions and Composer version.'); } throw new \InvalidArgumentException($errorMessage .'.'); } // handler Ctrl+C for unix-like systems - if (function_exists('pcntl_async_signals')) { + if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) { @mkdir($directory, 0777, true); if ($realDir = realpath($directory)) { pcntl_async_signals(true); @@ -372,6 +367,22 @@ EOT }); } } + // handler Ctrl+C for Windows on PHP 7.4+ + if (function_exists('sapi_windows_set_ctrl_handler')) { + @mkdir($directory, 0777, true); + if ($realDir = realpath($directory)) { + sapi_windows_set_ctrl_handler(function () use ($realDir) { + $fs = new Filesystem(); + $fs->removeDirectory($realDir); + exit(130); + }, true); + } + } + + // avoid displaying 9999999-dev as version if dev-master was selected + if ($package instanceof AliasPackage && $package->getPrettyVersion() === VersionParser::DEV_MASTER_ALIAS) { + $package = $package->getAliasOf(); + } $io->writeError('Installing ' . $package->getName() . ' (' . $package->getFullPrettyVersion(false) . ')'); diff --git a/src/Composer/Command/DumpAutoloadCommand.php b/src/Composer/Command/DumpAutoloadCommand.php index 9627b2a88..7449d197c 100644 --- a/src/Composer/Command/DumpAutoloadCommand.php +++ b/src/Composer/Command/DumpAutoloadCommand.php @@ -35,6 +35,8 @@ class DumpAutoloadCommand extends BaseCommand new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize`.'), new InputOption('apcu', null, InputOption::VALUE_NONE, 'Use APCu to cache found/not-found classes.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables autoload-dev rules.'), + new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), + new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), )) ->setHelp( <<getIO()->write('Generating autoload files'); } + $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); + $generator = $composer->getAutoloadGenerator(); $generator->setDevMode(!$input->getOption('no-dev')); $generator->setClassMapAuthoritative($authoritative); $generator->setApcu($apcu); $generator->setRunScripts(!$input->getOption('no-scripts')); + $generator->setIgnorePlatformRequirements($ignorePlatformReqs); $numberOfClasses = $generator->dump($config, $localRepo, $package, $installationManager, 'composer', $optimize); if ($authoritative) { diff --git a/src/Composer/Command/ExecCommand.php b/src/Composer/Command/ExecCommand.php index d530def66..fba9e7e8a 100644 --- a/src/Composer/Command/ExecCommand.php +++ b/src/Composer/Command/ExecCommand.php @@ -99,7 +99,7 @@ EOT try { chdir($this->getApplication()->getInitialWorkingDirectory()); } catch (\Exception $e) { - throw new \RuntimeException('Could not switch back to working directory "'.$this->getApplication()->getWorkingDirectory().'"', 0, $e); + throw new \RuntimeException('Could not switch back to working directory "'.$this->getApplication()->getInitialWorkingDirectory().'"', 0, $e); } } diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 8f101515c..111edbde9 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -345,13 +345,21 @@ EOT // prepare to resolve dependencies $repos = $this->getRepos(); $preferredStability = $minimumStability ?: 'stable'; - $phpVersion = $repos->findPackage('php', '*')->getPrettyVersion(); + $platformRepo = null; + if ($repos instanceof CompositeRepository) { + foreach ($repos->getRepositories() as $candidateRepo) { + if ($candidateRepo instanceof PlatformRepository) { + $platformRepo = $candidateRepo; + break; + } + } + } $question = 'Would you like to define your dependencies (require) interactively [yes]? '; $require = $input->getOption('require'); $requirements = array(); if ($require || $io->askConfirmation($question, true)) { - $requirements = $this->determineRequirements($input, $output, $require, $phpVersion, $preferredStability); + $requirements = $this->determineRequirements($input, $output, $require, $platformRepo, $preferredStability); } $input->setOption('require', $requirements); @@ -359,7 +367,7 @@ EOT $requireDev = $input->getOption('require-dev'); $devRequirements = array(); if ($requireDev || $io->askConfirmation($question, true)) { - $devRequirements = $this->determineRequirements($input, $output, $requireDev, $phpVersion, $preferredStability); + $devRequirements = $this->determineRequirements($input, $output, $requireDev, $platformRepo, $preferredStability); } $input->setOption('require-dev', $devRequirements); } @@ -403,7 +411,7 @@ EOT return $this->repos; } - final protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array(), $phpVersion = null, $preferredStability = 'stable', $checkProvidedVersions = true, $fixed = false) + final protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array(), PlatformRepository $platformRepo = null, $preferredStability = 'stable', $checkProvidedVersions = true, $fixed = false) { if ($requires) { $requires = $this->normalizeRequirements($requires); @@ -413,7 +421,7 @@ EOT foreach ($requires as $requirement) { if (!isset($requirement['version'])) { // determine the best version automatically - list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $phpVersion, $preferredStability, null, null, $fixed); + list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $platformRepo, $preferredStability, null, null, $fixed); $requirement['version'] = $version; // replace package name from packagist.org @@ -426,7 +434,7 @@ EOT )); } else { // check that the specified version/constraint exists before we proceed - list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $phpVersion, $preferredStability, $checkProvidedVersions ? $requirement['version'] : null, 'dev', $fixed); + list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $platformRepo, $preferredStability, $checkProvidedVersions ? $requirement['version'] : null, 'dev', $fixed); // replace package name from packagist.org $requirement['name'] = $name; @@ -550,7 +558,7 @@ EOT ); if (false === $constraint) { - list($name, $constraint) = $this->findBestVersionAndNameForPackage($input, $package, $phpVersion, $preferredStability); + list($name, $constraint) = $this->findBestVersionAndNameForPackage($input, $package, $platformRepo, $preferredStability); $io->writeError(sprintf( 'Using version %s for %s', @@ -577,17 +585,6 @@ EOT return array($this->parseAuthorString($author)); } - protected function formatRequirements(array $requirements) - { - $requires = array(); - $requirements = $this->normalizeRequirements($requirements); - foreach ($requirements as $requirement) { - $requires[$requirement['name']] = $requirement['version']; - } - - return $requires; - } - protected function getGitConfig() { if (null !== $this->gitConfig) { @@ -652,13 +649,6 @@ EOT return false; } - protected function normalizeRequirements(array $requirements) - { - $parser = new VersionParser(); - - return $parser->parseNameVersionPairs($requirements); - } - protected function addVendorIgnore($ignoreFile, $vendor = '/vendor/') { $contents = ""; @@ -723,7 +713,7 @@ EOT * * @param InputInterface $input * @param string $name - * @param string|null $phpVersion + * @param PlatformRepository|null $platformRepo * @param string $preferredStability * @param string|null $requiredVersion * @param string $minimumStability @@ -731,49 +721,47 @@ EOT * @throws \InvalidArgumentException * @return array name version */ - private function findBestVersionAndNameForPackage(InputInterface $input, $name, $phpVersion, $preferredStability = 'stable', $requiredVersion = null, $minimumStability = null, $fixed = null) + private function findBestVersionAndNameForPackage(InputInterface $input, $name, PlatformRepository $platformRepo = null, $preferredStability = 'stable', $requiredVersion = null, $minimumStability = null, $fixed = null) { - // find the latest version allowed in this repo set - $versionSelector = new VersionSelector($this->getRepositorySet($input, $minimumStability)); - $ignorePlatformReqs = $input->hasOption('ignore-platform-reqs') && $input->getOption('ignore-platform-reqs'); - - // ignore phpVersion if platform requirements are ignored - if ($ignorePlatformReqs) { - $phpVersion = null; + // handle ignore-platform-reqs flag if present + $ignorePlatformReqs = false; + if ($input->hasOption('ignore-platform-reqs') && $input->hasOption('ignore-platform-req')) { + $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); } - $package = $versionSelector->findBestCandidate($name, $requiredVersion, $phpVersion, $preferredStability); + // find the latest version allowed in this repo set + $versionSelector = new VersionSelector($this->getRepositorySet($input, $minimumStability), $platformRepo); + + $package = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, $ignorePlatformReqs); if (!$package) { // platform packages can not be found in the pool in versions other than the local platform's has // so if platform reqs are ignored we just take the user's word for it - if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name)) { + if ((true === $ignorePlatformReqs || (is_array($ignorePlatformReqs) && in_array($name, $ignorePlatformReqs))) && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name)) { return array($name, $requiredVersion ?: '*'); } // Check whether the PHP version was the problem - if ($phpVersion && $versionSelector->findBestCandidate($name, $requiredVersion, null, $preferredStability)) { + if (true !== $ignorePlatformReqs && $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, true)) { throw new \InvalidArgumentException(sprintf( - 'Package %s at version %s has a PHP requirement incompatible with your PHP version (%s)', + 'Package %s at version %s has a PHP requirement incompatible with your PHP version, PHP extensions and Composer version', $name, - $requiredVersion, - $phpVersion + $requiredVersion )); } // Check whether the required version was the problem - if ($requiredVersion && $versionSelector->findBestCandidate($name, null, $phpVersion, $preferredStability)) { + if ($requiredVersion && $versionSelector->findBestCandidate($name, null, $preferredStability, $ignorePlatformReqs)) { throw new \InvalidArgumentException(sprintf( 'Could not find package %s in a version matching %s', $name, $requiredVersion )); } - // Check whether the PHP version was the problem - if ($phpVersion && $versionSelector->findBestCandidate($name)) { + // Check whether the PHP version was the problem for all versions + if (true !== $ignorePlatformReqs && $versionSelector->findBestCandidate($name, null, $preferredStability, true)) { throw new \InvalidArgumentException(sprintf( - 'Could not find package %s in any version matching your PHP version (%s)', - $name, - $phpVersion + 'Could not find package %s in any version matching your PHP version, PHP extensions and Composer version', + $name )); } diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index 033f32195..93523cba7 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -44,11 +44,13 @@ class InstallCommand extends BaseCommand new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), + new InputOption('no-install', null, InputOption::VALUE_NONE, 'Do not use, only defined here to catch misuse of the install command.'), new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'), new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'), new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'), new InputOption('apcu-autoloader', null, InputOption::VALUE_NONE, 'Use APCu to cache found/not-found classes.'), - new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'), + new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), + new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Should not be provided, use composer require instead to add a given package to composer.json.'), )) ->setHelp( @@ -82,6 +84,12 @@ EOT return 1; } + if ($input->getOption('no-install')) { + $io->writeError('Invalid option "--no-install". Use "composer update --no-install" instead if you are trying to update the composer.lock file.'); + + return 1; + } + $composer = $this->getComposer(true, $input->getOption('no-plugins')); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output); @@ -96,6 +104,8 @@ EOT $authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative'); $apcu = $input->getOption('apcu-autoloader') || $config->get('apcu-autoloader'); + $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); + $install ->setDryRun($input->getOption('dry-run')) ->setVerbose($input->getOption('verbose')) @@ -107,7 +117,7 @@ EOT ->setOptimizeAutoloader($optimize) ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) - ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) + ->setIgnorePlatformRequirements($ignorePlatformReqs) ; if ($input->getOption('no-plugins')) { diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index 452b30585..62e23dd9f 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -41,7 +41,8 @@ class RemoveCommand extends BaseCommand new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), - new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'), + new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies (implies --no-install).'), + new InputOption('no-install', null, InputOption::VALUE_NONE, 'Skip the install step after updating the composer.lock file.'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'), new InputOption('update-with-dependencies', null, InputOption::VALUE_NONE, 'Allows inherited dependencies to be updated with explicit dependencies. (Deprecrated, is now default behavior)'), @@ -49,7 +50,8 @@ class RemoveCommand extends BaseCommand new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-all-dependencies'), new InputOption('no-update-with-dependencies', null, InputOption::VALUE_NONE, 'Does not allow inherited dependencies to be updated with explicit dependencies.'), new InputOption('unused', null, InputOption::VALUE_NONE, 'Remove all packages which are locked but not required by any other package.'), - new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'), + new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), + new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'), new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'), new InputOption('apcu-autoloader', null, InputOption::VALUE_NONE, 'Use APCu to cache found/not-found classes.'), @@ -190,6 +192,8 @@ EOT } } + $io->writeError(''.$file.' has been updated'); + if ($input->getOption('no-update')) { return 0; } @@ -224,12 +228,19 @@ EOT $apcu = $input->getOption('apcu-autoloader') || $composer->getConfig()->get('apcu-autoloader'); $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; + $flags = ''; if ($input->getOption('update-with-all-dependencies') || $input->getOption('with-all-dependencies')) { $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; + $flags .= ' --with-all-dependencies'; } elseif ($input->getOption('no-update-with-dependencies')) { $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; + $flags .= ' --with-dependencies'; } + $io->writeError('Running composer update '.implode(' ', $packages).$flags); + + $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); + $install ->setVerbose($input->getOption('verbose')) ->setDevMode($updateDevMode) @@ -237,9 +248,10 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) + ->setInstall(!$input->getOption('no-install')) ->setUpdateAllowList($packages) ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) - ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) + ->setIgnorePlatformRequirements($ignorePlatformReqs) ->setRunScripts(!$input->getOption('no-scripts')) ->setDryRun($dryRun) ; diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 67188504d..b77ab5a4e 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -61,14 +61,16 @@ class RequireCommand extends InitCommand new InputOption('fixed', null, InputOption::VALUE_NONE, 'Write fixed version to the composer.json.'), new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'DEPRECATED: This flag does not exist anymore.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), - new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'), + new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies (implies --no-install).'), + new InputOption('no-install', null, InputOption::VALUE_NONE, 'Skip the install step after updating the composer.lock file.'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'), new InputOption('update-with-dependencies', null, InputOption::VALUE_NONE, 'Allows inherited dependencies to be updated, except those that are root requirements.'), new InputOption('update-with-all-dependencies', null, InputOption::VALUE_NONE, 'Allows all inherited dependencies to be updated, including those that are root requirements.'), new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-dependencies'), new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-all-dependencies'), - new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'), + new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), + new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), new InputOption('prefer-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies.'), new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest versions of dependencies.'), new InputOption('sort-packages', null, InputOption::VALUE_NONE, 'Sorts packages when adding/updating a new dependency'), @@ -95,7 +97,7 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { - if (function_exists('pcntl_async_signals')) { + if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) { pcntl_async_signals(true); pcntl_signal(SIGINT, array($this, 'revertComposerFile')); pcntl_signal(SIGTERM, array($this, 'revertComposerFile')); @@ -165,7 +167,7 @@ EOT $platformOverrides = $composer->getConfig()->get('platform') ?: array(); // initialize $this->repos as it is used by the parent InitCommand $this->repos = new CompositeRepository(array_merge( - array(new PlatformRepository(array(), $platformOverrides)), + array($platformRepo = new PlatformRepository(array(), $platformOverrides)), $repos )); @@ -175,9 +177,16 @@ EOT $preferredStability = $composer->getPackage()->getMinimumStability(); } - $phpVersion = $this->repos->findPackage('php', '*')->getPrettyVersion(); try { - $requirements = $this->determineRequirements($input, $output, $input->getArgument('packages'), $phpVersion, $preferredStability, !$input->getOption('no-update'), $input->getOption('fixed')); + $requirements = $this->determineRequirements( + $input, + $output, + $input->getArgument('packages'), + $platformRepo, + $preferredStability, + !$input->getOption('no-update'), + $input->getOption('fixed') + ); } catch (\Exception $e) { if ($this->newlyCreated) { throw new \RuntimeException('No composer.json present in the current directory, this may be the cause of the following exception.', 0, $e); @@ -256,23 +265,31 @@ EOT $rootPackage->setDevRequires($links['require-dev']); } + $updateDevMode = !$input->getOption('update-no-dev'); $optimize = $input->getOption('optimize-autoloader') || $composer->getConfig()->get('optimize-autoloader'); $authoritative = $input->getOption('classmap-authoritative') || $composer->getConfig()->get('classmap-authoritative'); $apcu = $input->getOption('apcu-autoloader') || $composer->getConfig()->get('apcu-autoloader'); $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; + $flags = ''; if ($input->getOption('update-with-all-dependencies') || $input->getOption('with-all-dependencies')) { $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; + $flags .= ' --with-all-dependencies'; } elseif ($input->getOption('update-with-dependencies') || $input->getOption('with-dependencies')) { $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; + $flags .= ' --with-dependencies'; } + $io->writeError('Running composer update '.implode(' ', array_keys($requirements)).$flags); + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $install = Installer::create($io, $composer); + $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); + $install ->setDryRun($input->getOption('dry-run')) ->setVerbose($input->getOption('verbose')) @@ -284,8 +301,9 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) + ->setInstall(!$input->getOption('no-install')) ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) - ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) + ->setIgnorePlatformRequirements($ignorePlatformReqs) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) ; diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 6cf1905f5..b928765f9 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -66,6 +66,7 @@ class ShowCommand extends BaseCommand new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.'), new InputArgument('version', InputArgument::OPTIONAL, 'Version or version constraint to inspect'), new InputOption('all', null, InputOption::VALUE_NONE, 'List all packages'), + new InputOption('locked', null, InputOption::VALUE_NONE, 'List all locked packages'), new InputOption('installed', 'i', InputOption::VALUE_NONE, 'List installed packages only (enabled by default, only present for BC).'), new InputOption('platform', 'p', InputOption::VALUE_NONE, 'List platform packages only'), new InputOption('available', 'a', InputOption::VALUE_NONE, 'List available packages only'), @@ -149,10 +150,14 @@ EOT $platformOverrides = $composer->getConfig()->get('platform') ?: array(); } $platformRepo = new PlatformRepository(array(), $platformOverrides); - $phpVersion = $platformRepo->findPackage('php', '*')->getVersion(); + $lockedRepo = null; if ($input->getOption('self')) { $package = $this->getComposer()->getPackage(); + if ($input->getOption('name-only')) { + $io->write($package->getName()); + return 0; + } $repos = $installedRepo = new InstalledRepository(array(new RootPackageRepository($package))); } elseif ($input->getOption('platform')) { $repos = $installedRepo = new InstalledRepository(array($platformRepo)); @@ -168,13 +173,27 @@ EOT } } elseif ($input->getOption('all') && $composer) { $localRepo = $composer->getRepositoryManager()->getLocalRepository(); - $installedRepo = new InstalledRepository(array($localRepo, $platformRepo)); + $locker = $composer->getLocker(); + if ($locker->isLocked()) { + $lockedRepo = $locker->getLockedRepository(); + $installedRepo = new InstalledRepository(array($lockedRepo, $localRepo, $platformRepo)); + } else { + $installedRepo = new InstalledRepository(array($localRepo, $platformRepo)); + } $repos = new CompositeRepository(array_merge(array($installedRepo), $composer->getRepositoryManager()->getRepositories())); } elseif ($input->getOption('all')) { $defaultRepos = RepositoryFactory::defaultRepos($io); $io->writeError('No composer.json found in the current directory, showing available packages from ' . implode(', ', array_keys($defaultRepos))); $installedRepo = new InstalledRepository(array($platformRepo)); $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos)); + } elseif ($input->getOption('locked')) { + if (!$composer || !$composer->getLocker()->isLocked()) { + throw new \UnexpectedValueException('A valid composer.json and composer.lock files is required to run this command with --locked'); + } + $locker = $composer->getLocker(); + $lockedRepo = $locker->getLockedRepository(); + $installedRepo = new InstalledRepository(array($lockedRepo)); + $repos = new CompositeRepository(array_merge(array($installedRepo), $composer->getRepositoryManager()->getRepositories())); } else { $repos = $installedRepo = new InstalledRepository(array($this->getComposer()->getRepositoryManager()->getLocalRepository())); $rootPkg = $this->getComposer()->getPackage(); @@ -229,7 +248,7 @@ EOT } else { $latestPackage = null; if ($input->getOption('latest')) { - $latestPackage = $this->findLatestPackage($package, $composer, $phpVersion); + $latestPackage = $this->findLatestPackage($package, $composer, $platformRepo); } if ($input->getOption('outdated') && $input->getOption('strict') && $latestPackage && $latestPackage->getFullPrettyVersion() !== $package->getFullPrettyVersion() && !$latestPackage->isAbandoned()) { $exitCode = 1; @@ -315,6 +334,8 @@ EOT foreach ($repos as $repo) { if ($repo === $platformRepo) { $type = 'platform'; + } elseif($lockedRepo !== null && $repo === $lockedRepo) { + $type = 'locked'; } elseif ($repo === $installedRepo || in_array($repo, $installedRepo->getRepositories(), true)) { $type = 'installed'; } else { @@ -351,7 +372,7 @@ EOT $exitCode = 0; $viewData = array(); $viewMetaData = array(); - foreach (array('platform' => true, 'available' => false, 'installed' => true) as $type => $showVersion) { + foreach (array('platform' => true, 'locked' => true, 'available' => false, 'installed' => true) as $type => $showVersion) { if (isset($packages[$type])) { ksort($packages[$type]); @@ -360,7 +381,7 @@ EOT if ($showLatest && $showVersion) { foreach ($packages[$type] as $package) { if (is_object($package)) { - $latestPackage = $this->findLatestPackage($package, $composer, $phpVersion, $showMinorOnly); + $latestPackage = $this->findLatestPackage($package, $composer, $platformRepo, $showMinorOnly); if ($latestPackage === false) { continue; } @@ -1158,18 +1179,18 @@ EOT /** * Given a package, this finds the latest package matching it * - * @param PackageInterface $package - * @param Composer $composer - * @param string $phpVersion - * @param bool $minorOnly + * @param PackageInterface $package + * @param Composer $composer + * @param PlatformRepository $platformRepo + * @param bool $minorOnly * * @return PackageInterface|false */ - private function findLatestPackage(PackageInterface $package, Composer $composer, $phpVersion, $minorOnly = false) + private function findLatestPackage(PackageInterface $package, Composer $composer, PlatformRepository $platformRepo, $minorOnly = false) { // find the latest version allowed in this repo set $name = $package->getName(); - $versionSelector = new VersionSelector($this->getRepositorySet($composer)); + $versionSelector = new VersionSelector($this->getRepositorySet($composer), $platformRepo); $stability = $composer->getPackage()->getMinimumStability(); $flags = $composer->getPackage()->getStabilityFlags(); if (isset($flags[$name])) { @@ -1190,7 +1211,7 @@ EOT $targetVersion = '^' . $package->getVersion(); } - return $versionSelector->findBestCandidate($name, $targetVersion, $phpVersion, $bestStability); + return $versionSelector->findBestCandidate($name, $targetVersion, $bestStability); } private function getRepositorySet(Composer $composer) diff --git a/src/Composer/Command/SuggestsCommand.php b/src/Composer/Command/SuggestsCommand.php index 61875ba0b..d874d1d19 100644 --- a/src/Composer/Command/SuggestsCommand.php +++ b/src/Composer/Command/SuggestsCommand.php @@ -14,7 +14,7 @@ namespace Composer\Command; use Composer\Repository\PlatformRepository; use Composer\Repository\RootPackageRepository; -use Composer\Repository\CompositeRepository; +use Composer\Repository\InstalledRepository; use Composer\Installer\SuggestedPackagesReporter; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -67,16 +67,13 @@ EOT $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository(); } - $installedRepo = new CompositeRepository($installedRepos); + $installedRepo = new InstalledRepository($installedRepos); $reporter = new SuggestedPackagesReporter($this->getIO()); $filter = $input->getArgument('packages'); - if (empty($filter) && !$input->getOption('all')) { - $filter = array_map(function ($link) { - return $link->getTarget(); - }, array_merge($composer->getPackage()->getRequires(), $composer->getPackage()->getDevRequires())); - } - foreach ($installedRepo->getPackages() as $package) { + $packages = $installedRepo->getPackages(); + $packages[] = $composer->getPackage(); + foreach ($packages as $package) { if (!empty($filter) && !in_array($package->getName(), $filter)) { continue; } @@ -100,7 +97,7 @@ EOT $mode = SuggestedPackagesReporter::MODE_LIST; } - $reporter->output($mode, $installedRepo); + $reporter->output($mode, $installedRepo, empty($filter) && !$input->getOption('all') ? $composer->getPackage() : null); return 0; } diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index 9c875a57d..9576b141f 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -18,6 +18,9 @@ use Composer\Installer; use Composer\IO\IOInterface; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; +use Composer\Package\Version\VersionParser; +use Composer\Semver\Constraint\MultiConstraint; +use Composer\Package\Link; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -39,12 +42,14 @@ class UpdateCommand extends BaseCommand ->setDescription('Upgrades your dependencies to the latest version according to composer.json, and updates the composer.lock file.') ->setDefinition(array( new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that should be updated, if not provided all packages are.'), + new InputOption('with', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Temporary version constraint to add, e.g. foo/bar:1.0.0 or foo/bar=1.0.0'), 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, 'DEPRECATED: 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-install', null, InputOption::VALUE_NONE, 'Skip the install step after updating the composer.lock file.'), new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'DEPRECATED: This flag does not exist anymore.'), @@ -55,7 +60,8 @@ class UpdateCommand extends BaseCommand new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump.'), new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'), new InputOption('apcu-autoloader', null, InputOption::VALUE_NONE, 'Use APCu to cache found/not-found classes.'), - new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore platform requirements (php & ext- packages).'), + new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), + new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), new InputOption('prefer-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies.'), new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest versions of dependencies.'), new InputOption('interactive', 'i', InputOption::VALUE_NONE, 'Interactive interface with autocompletion to select the packages to update.'), @@ -79,6 +85,14 @@ from a specific vendor: php composer.phar update vendor/package1 foo/* [...] +To run an update with more restrictive constraints you can use: + +php composer.phar update --with vendor/package:1.0.* + +To run a partial update with more restrictive constraints you can use the shorthand: + +php composer.phar update vendor/package:1.0.* + To select packages names interactively with auto-completion use -i. Read more at https://getcomposer.org/doc/03-cli.md#update-u @@ -100,22 +114,53 @@ EOT $composer = $this->getComposer(true, $input->getOption('no-plugins')); $packages = $input->getArgument('packages'); + $reqs = $this->formatRequirements($input->getOption('with')); + + // extract --with shorthands from the allowlist + if ($packages) { + $allowlistPackagesWithRequirements = array_filter($packages, function ($pkg) { + return preg_match('{\S+[ =:]\S+}', $pkg) > 0; + }); + foreach ($this->formatRequirements($allowlistPackagesWithRequirements) as $package => $constraint) { + $reqs[$package] = $constraint; + } + + // replace the foo/bar:req by foo/bar in the allowlist + foreach ($allowlistPackagesWithRequirements as $package) { + $packageName = preg_replace('{^([^ =:]+)[ =:].*$}', '$1', $package); + $index = array_search($package, $packages); + $packages[$index] = $packageName; + } + } + + $rootRequires = $composer->getPackage()->getRequires(); + $rootDevRequires = $composer->getPackage()->getDevRequires(); + foreach ($reqs as $package => $constraint) { + if (isset($rootRequires[$package])) { + $rootRequires[$package] = $this->appendConstraintToLink($rootRequires[$package], $constraint); + } elseif (isset($rootDevRequires[$package])) { + $rootDevRequires[$package] = $this->appendConstraintToLink($rootDevRequires[$package], $constraint); + } else { + throw new \UnexpectedValueException('Only root package requirements can receive temporary constraints and '.$package.' is not one'); + } + } + $composer->getPackage()->setRequires($rootRequires); + $composer->getPackage()->setDevRequires($rootDevRequires); if ($input->getOption('interactive')) { $packages = $this->getPackagesInteractively($io, $input, $output, $composer, $packages); } if ($input->getOption('root-reqs')) { - $require = array_keys($composer->getPackage()->getRequires()); + $requires = array_keys($rootRequires); if (!$input->getOption('no-dev')) { - $requireDev = array_keys($composer->getPackage()->getDevRequires()); - $require = array_merge($require, $requireDev); + $requires = array_merge($requires, array_keys($rootDevRequires)); } if (!empty($packages)) { - $packages = array_intersect($packages, $require); + $packages = array_intersect($packages, $requires); } else { - $packages = $require; + $packages = $requires; } } @@ -151,6 +196,8 @@ EOT $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; } + $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); + $install ->setDryRun($input->getOption('dry-run')) ->setVerbose($input->getOption('verbose')) @@ -163,10 +210,11 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) + ->setInstall(!$input->getOption('no-install')) ->setUpdateMirrors($updateMirrors) ->setUpdateAllowList($packages) ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) - ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) + ->setIgnorePlatformRequirements($ignorePlatformReqs) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) ; @@ -240,4 +288,19 @@ EOT throw new \RuntimeException('Installation aborted.'); } + + private function appendConstraintToLink(Link $link, $constraint) + { + $parser = new VersionParser; + $oldPrettyString = $link->getConstraint()->getPrettyString(); + $newConstraint = MultiConstraint::create(array($link->getConstraint(), $parser->parseConstraints($constraint)), true); + $newConstraint->setPrettyString($oldPrettyString.', '.$constraint); + return new Link( + $link->getSource(), + $link->getTarget(), + $newConstraint, + $link->getDescription(), + $link->getPrettyConstraint() . ', ' . $constraint + ); + } } diff --git a/src/Composer/Command/ValidateCommand.php b/src/Composer/Command/ValidateCommand.php index 6ab95ed1c..1c76d678d 100644 --- a/src/Composer/Command/ValidateCommand.php +++ b/src/Composer/Command/ValidateCommand.php @@ -42,6 +42,7 @@ class ValidateCommand extends BaseCommand new InputOption('no-check-all', null, InputOption::VALUE_NONE, 'Do not validate requires for overly strict/loose constraints'), new InputOption('no-check-lock', null, InputOption::VALUE_NONE, 'Do not check if lock file is up to date'), new InputOption('no-check-publish', null, InputOption::VALUE_NONE, 'Do not check for publish errors'), + new InputOption('no-check-version', null, InputOption::VALUE_NONE, 'Do not report a warning if the version field is present'), new InputOption('with-dependencies', 'A', InputOption::VALUE_NONE, 'Also validate the composer.json of all installed dependencies'), new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code for warnings as well as errors'), new InputArgument('file', InputArgument::OPTIONAL, 'path to composer.json file'), @@ -86,8 +87,9 @@ EOT $checkAll = $input->getOption('no-check-all') ? 0 : ValidatingArrayLoader::CHECK_ALL; $checkPublish = !$input->getOption('no-check-publish'); $checkLock = !$input->getOption('no-check-lock'); + $checkVersion = $input->getOption('no-check-version') ? 0 : ConfigValidator::CHECK_VERSION; $isStrict = $input->getOption('strict'); - list($errors, $publishErrors, $warnings) = $validator->validate($file, $checkAll); + list($errors, $publishErrors, $warnings) = $validator->validate($file, $checkAll, $checkVersion); $lockErrors = array(); $composer = Factory::create($io, $file, $input->hasParameterOption('--no-plugins')); @@ -107,7 +109,7 @@ EOT $path = $composer->getInstallationManager()->getInstallPath($package); $file = $path . '/composer.json'; if (is_dir($path) && file_exists($file)) { - list($errors, $publishErrors, $warnings) = $validator->validate($file, $checkAll); + list($errors, $publishErrors, $warnings) = $validator->validate($file, $checkAll, $checkVersion); $this->outputResult($io, $package->getPrettyName(), $errors, $warnings, $checkPublish, $publishErrors); diff --git a/src/Composer/Compiler.php b/src/Composer/Compiler.php index 41e834993..240870a1c 100644 --- a/src/Composer/Compiler.php +++ b/src/Composer/Compiler.php @@ -139,6 +139,11 @@ class Compiler $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/autoload_files.php')); $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/autoload_real.php')); $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/autoload_static.php')); + $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/installed.php')); + $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/InstalledVersions.php')); + if (file_exists(__DIR__.'/../../vendor/composer/platform_check.php')) { + $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/platform_check.php')); + } if (file_exists(__DIR__.'/../../vendor/composer/include_paths.php')) { $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/include_paths.php')); } diff --git a/src/Composer/Composer.php b/src/Composer/Composer.php index b5bbb0a0b..8f0ac058a 100644 --- a/src/Composer/Composer.php +++ b/src/Composer/Composer.php @@ -14,6 +14,7 @@ namespace Composer; use Composer\Package\RootPackageInterface; use Composer\Package\Locker; +use Composer\Util\Loop; use Composer\Repository\RepositoryManager; use Composer\Installer\InstallationManager; use Composer\Plugin\PluginManager; @@ -55,6 +56,17 @@ class Composer const RELEASE_DATE = '@release_date@'; const SOURCE_VERSION = '2.0-dev+source'; + /** + * Version number of the internal composer-runtime-api package + * + * This is used to version features available to projects at runtime + * like the platform-check file, the Composer\InstalledVersions class + * and possibly others in the future. + * + * @var string + */ + const RUNTIME_API_VERSION = '2.0.0'; + public static function getVersion() { // no replacement done, this must be a source checkout @@ -71,7 +83,7 @@ class Composer } /** - * @var Package\RootPackageInterface + * @var RootPackageInterface */ private $package; @@ -80,6 +92,11 @@ class Composer */ private $locker; + /** + * @var Loop + */ + private $loop; + /** * @var Repository\RepositoryManager */ @@ -121,7 +138,7 @@ class Composer private $archiveManager; /** - * @param Package\RootPackageInterface $package + * @param RootPackageInterface $package * @return void */ public function setPackage(RootPackageInterface $package) @@ -130,7 +147,7 @@ class Composer } /** - * @return Package\RootPackageInterface + * @return RootPackageInterface */ public function getPackage() { @@ -154,7 +171,7 @@ class Composer } /** - * @param Package\Locker $locker + * @param Locker $locker */ public function setLocker(Locker $locker) { @@ -162,7 +179,7 @@ class Composer } /** - * @return Package\Locker + * @return Locker */ public function getLocker() { @@ -170,7 +187,23 @@ class Composer } /** - * @param Repository\RepositoryManager $manager + * @param Loop $loop + */ + public function setLoop(Loop $loop) + { + $this->loop = $loop; + } + + /** + * @return Loop + */ + public function getLoop() + { + return $this->loop; + } + + /** + * @param RepositoryManager $manager */ public function setRepositoryManager(RepositoryManager $manager) { @@ -178,7 +211,7 @@ class Composer } /** - * @return Repository\RepositoryManager + * @return RepositoryManager */ public function getRepositoryManager() { @@ -186,7 +219,7 @@ class Composer } /** - * @param Downloader\DownloadManager $manager + * @param DownloadManager $manager */ public function setDownloadManager(DownloadManager $manager) { @@ -194,7 +227,7 @@ class Composer } /** - * @return Downloader\DownloadManager + * @return DownloadManager */ public function getDownloadManager() { @@ -218,7 +251,7 @@ class Composer } /** - * @param Installer\InstallationManager $manager + * @param InstallationManager $manager */ public function setInstallationManager(InstallationManager $manager) { @@ -226,7 +259,7 @@ class Composer } /** - * @return Installer\InstallationManager + * @return InstallationManager */ public function getInstallationManager() { @@ -234,7 +267,7 @@ class Composer } /** - * @param Plugin\PluginManager $manager + * @param PluginManager $manager */ public function setPluginManager(PluginManager $manager) { @@ -242,7 +275,7 @@ class Composer } /** - * @return Plugin\PluginManager + * @return PluginManager */ public function getPluginManager() { @@ -266,7 +299,7 @@ class Composer } /** - * @param Autoload\AutoloadGenerator $autoloadGenerator + * @param AutoloadGenerator $autoloadGenerator */ public function setAutoloadGenerator(AutoloadGenerator $autoloadGenerator) { @@ -274,7 +307,7 @@ class Composer } /** - * @return Autoload\AutoloadGenerator + * @return AutoloadGenerator */ public function getAutoloadGenerator() { diff --git a/src/Composer/Config.php b/src/Composer/Config.php index e57d21cab..455ae6db7 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -64,6 +64,7 @@ class Config 'htaccess-protect' => true, 'use-github-api' => true, 'lock' => true, + 'platform-check' => true, // valid keys without defaults (auth config stuff): // bitbucket-oauth // github-oauth diff --git a/src/Composer/DependencyResolver/Decisions.php b/src/Composer/DependencyResolver/Decisions.php index e2773501f..bbf774ba9 100644 --- a/src/Composer/DependencyResolver/Decisions.php +++ b/src/Composer/DependencyResolver/Decisions.php @@ -108,17 +108,17 @@ class Decisions implements \Iterator, \Countable public function validOffset($queueOffset) { - return $queueOffset >= 0 && $queueOffset < count($this->decisionQueue); + return $queueOffset >= 0 && $queueOffset < \count($this->decisionQueue); } public function lastReason() { - return $this->decisionQueue[count($this->decisionQueue) - 1][self::DECISION_REASON]; + return $this->decisionQueue[\count($this->decisionQueue) - 1][self::DECISION_REASON]; } public function lastLiteral() { - return $this->decisionQueue[count($this->decisionQueue) - 1][self::DECISION_LITERAL]; + return $this->decisionQueue[\count($this->decisionQueue) - 1][self::DECISION_LITERAL]; } public function reset() @@ -130,7 +130,7 @@ class Decisions implements \Iterator, \Countable public function resetToOffset($offset) { - while (count($this->decisionQueue) > $offset + 1) { + while (\count($this->decisionQueue) > $offset + 1) { $decision = array_pop($this->decisionQueue); $this->decisionMap[abs($decision[self::DECISION_LITERAL])] = 0; } @@ -144,7 +144,7 @@ class Decisions implements \Iterator, \Countable public function count() { - return count($this->decisionQueue); + return \count($this->decisionQueue); } public function rewind() @@ -174,7 +174,7 @@ class Decisions implements \Iterator, \Countable public function isEmpty() { - return count($this->decisionQueue) === 0; + return \count($this->decisionQueue) === 0; } protected function addDecision($literal, $level) diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index 3fdb4438b..235987aa3 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -60,7 +60,7 @@ class DefaultPolicy implements PolicyInterface $sortedLiterals = $this->pruneRemoteAliases($pool, $sortedLiterals); } - $selected = call_user_func_array('array_merge', $packages); + $selected = \call_user_func_array('array_merge', $packages); // now sort the result across all packages to respect replaces across packages usort($selected, function ($a, $b) use ($policy, $pool, $requiredPackage) { diff --git a/src/Composer/DependencyResolver/GenericRule.php b/src/Composer/DependencyResolver/GenericRule.php index a07883872..2d2cec7b1 100644 --- a/src/Composer/DependencyResolver/GenericRule.php +++ b/src/Composer/DependencyResolver/GenericRule.php @@ -64,7 +64,7 @@ class GenericRule extends Rule public function isAssertion() { - return 1 === count($this->literals); + return 1 === \count($this->literals); } /** diff --git a/src/Composer/DependencyResolver/MultiConflictRule.php b/src/Composer/DependencyResolver/MultiConflictRule.php index 8de77a41b..8200eef0a 100644 --- a/src/Composer/DependencyResolver/MultiConflictRule.php +++ b/src/Composer/DependencyResolver/MultiConflictRule.php @@ -33,7 +33,7 @@ class MultiConflictRule extends Rule { parent::__construct($reason, $reasonData); - if (count($literals) < 3) { + if (\count($literals) < 3) { throw new \RuntimeException("multi conflict rule requires at least 3 literals"); } diff --git a/src/Composer/DependencyResolver/Operation/SolverOperation.php b/src/Composer/DependencyResolver/Operation/SolverOperation.php index bbc077c31..5839cbd64 100644 --- a/src/Composer/DependencyResolver/Operation/SolverOperation.php +++ b/src/Composer/DependencyResolver/Operation/SolverOperation.php @@ -42,10 +42,4 @@ abstract class SolverOperation implements OperationInterface { return $this->reason; } - - /** - * @param $lock bool Whether this is an operation on the lock file - * @return string - */ - abstract public function show($lock); } diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index abf3d70bf..26ca947d3 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -16,7 +16,6 @@ use Composer\Package\AliasPackage; use Composer\Package\Version\VersionParser; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; -use Composer\Semver\Constraint\EmptyConstraint; use Composer\Package\PackageInterface; /** @@ -71,7 +70,7 @@ class Pool implements \Countable */ public function count() { - return count($this->packages); + return \count($this->packages); } /** @@ -189,6 +188,6 @@ class Pool implements \Countable public function isUnacceptableFixedPackage(PackageInterface $package) { - return in_array($package, $this->unacceptableFixedPackages, true); + return \in_array($package, $this->unacceptableFixedPackages, true); } } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 44f7e2f45..29903f493 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -21,7 +21,8 @@ use Composer\Package\Version\StabilityFilter; use Composer\Repository\PlatformRepository; use Composer\Repository\RootPackageRepository; use Composer\Semver\Constraint\Constraint; -use Composer\Semver\Constraint\EmptyConstraint; +use Composer\Semver\Constraint\ConstraintInterface; +use Composer\Semver\Constraint\MatchAllConstraint; use Composer\Semver\Constraint\MultiConstraint; use Composer\EventDispatcher\EventDispatcher; use Composer\Plugin\PrePoolCreateEvent; @@ -32,23 +33,53 @@ use Composer\Plugin\PluginEvents; */ class PoolBuilder { + /** + * @var int[] + */ private $acceptableStabilities; + /** + * @var int[] + */ private $stabilityFlags; /** * @psalm-var array> */ private $rootAliases; + /** + * @psalm-var array + */ private $rootReferences; + /** + * @var EventDispatcher + */ private $eventDispatcher; + /** + * @var IOInterface + */ private $io; + /** + * @psalm-var array + */ private $aliasMap = array(); + /** + * @psalm-var array + */ private $nameConstraints = array(); private $loadedNames = array(); + /** + * @psalm-var Package[] + */ private $packages = array(); + /** + * @psalm-var list + */ private $unacceptableFixedPackages = array(); private $updateAllowList = array(); private $skippedLoad = array(); + /** + * @psalm-var array + */ private $updateAllowWarned = array(); /** @@ -82,7 +113,7 @@ class PoolBuilder $request->fixPackage($lockedPackage); $lockedName = $lockedPackage->getName(); // remember which packages we skipped loading remote content for in this partial update - $this->skippedLoad[$lockedPackage->getName()] = $lockedName; + $this->skippedLoad[$lockedName] = $lockedName; foreach ($lockedPackage->getReplaces() as $link) { $this->skippedLoad[$link->getTarget()] = $lockedName; } @@ -122,7 +153,7 @@ class PoolBuilder } $loadNames[$packageName] = $constraint; - $this->nameConstraints[$packageName] = $constraint ? new MultiConstraint(array($constraint), false) : null; + $this->nameConstraints[$packageName] = $constraint && !($constraint instanceof MatchAllConstraint) ? array($constraint) : null; } // clean up loadNames for anything we manually marked loaded above @@ -159,11 +190,17 @@ class PoolBuilder } // filter packages according to all the require statements collected for each package + $nameConstraints = array(); + foreach ($this->nameConstraints as $name => $constraints) { + if (\is_array($constraints)) { + $nameConstraints[$name] = MultiConstraint::create(array_values(array_unique($constraints)), false); + } + } foreach ($this->packages as $i => $package) { // we check all alias related packages at once, so no need to check individual aliases // isset also checks non-null value - if (!$package instanceof AliasPackage && isset($this->nameConstraints[$package->getName()])) { - $constraint = $this->nameConstraints[$package->getName()]; + if (!$package instanceof AliasPackage && isset($nameConstraints[$package->getName()])) { + $constraint = $nameConstraints[$package->getName()]; $aliasedPackages = array($i => $package); if (isset($this->aliasMap[spl_object_hash($package)])) { @@ -268,12 +305,11 @@ class PoolBuilder } $linkConstraint = $link->getConstraint(); - if ($linkConstraint && !($linkConstraint instanceof EmptyConstraint)) { - if (!array_key_exists($require, $this->nameConstraints)) { - $this->nameConstraints[$require] = new MultiConstraint(array($linkConstraint), false); - } elseif ($this->nameConstraints[$require]) { - // TODO addConstraint function? - $this->nameConstraints[$require] = new MultiConstraint(array_merge(array($linkConstraint), $this->nameConstraints[$require]->getConstraints()), false); + if ($linkConstraint && !($linkConstraint instanceof MatchAllConstraint)) { + if (!\array_key_exists($require, $this->nameConstraints)) { + $this->nameConstraints[$require] = array($linkConstraint); + } elseif (\is_array($this->nameConstraints[$require])) { + $this->nameConstraints[$require][] = $linkConstraint; } // else it is null and should stay null } else { @@ -377,8 +413,12 @@ class PoolBuilder } } - // if we unfixed a replaced package name, we also need to unfix the replacer itself - if ($this->skippedLoad[$name] !== $name) { + if ( + // if we unfixed a replaced package name, we also need to unfix the replacer itself + $this->skippedLoad[$name] !== $name + // as long as it was not unfixed yet + && isset($this->skippedLoad[$this->skippedLoad[$name]]) + ) { $this->unfixPackage($request, $this->skippedLoad[$name]); } diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 6861b814c..eed4c5043 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -66,7 +66,7 @@ class Problem * @param array $installedMap A map of all present packages * @return string */ - public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array()) + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, array $installedMap = array(), array $learnedPool = array()) { // TODO doesn't this entirely defeat the purpose of the problem sections? what's the point of sections? $reasons = call_user_func_array('array_merge', array_reverse($this->reasons)); @@ -90,23 +90,62 @@ class Problem } if (empty($packages)) { - return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $packageName, $constraint)); + return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $isVerbose, $packageName, $constraint)); } } - $messages = array(); - foreach ($reasons as $rule) { - $messages[] = $rule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool); - } - - return "\n - ".implode("\n - ", array_unique($messages)); + return self::formatDeduplicatedRules($reasons, ' ', $repositorySet, $request, $pool, $isVerbose, $installedMap, $learnedPool); } - public function isCausedByLock() + /** + * @internal + */ + public static function formatDeduplicatedRules($rules, $indent, RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, array $installedMap = array(), array $learnedPool = array()) + { + $messages = array(); + $templates = array(); + $parser = new VersionParser; + $deduplicatableRuleTypes = array(Rule::RULE_PACKAGE_REQUIRES, Rule::RULE_PACKAGE_CONFLICT); + foreach ($rules as $rule) { + $message = $rule->getPrettyString($repositorySet, $request, $pool, $isVerbose, $installedMap, $learnedPool); + if (in_array($rule->getReason(), $deduplicatableRuleTypes, true) && preg_match('{^(?P\S+) (?P\S+) (?Prequires|conflicts)}', $message, $m)) { + $template = preg_replace('{^\S+ \S+ }', '%s%s ', $message); + $messages[] = $template; + $templates[$template][$m[1]][$parser->normalize($m[2])] = $m[2]; + } elseif ($message !== '') { + $messages[] = $message; + } + } + + $result = array(); + foreach (array_unique($messages) as $message) { + if (isset($templates[$message])) { + foreach ($templates[$message] as $package => $versions) { + uksort($versions, 'version_compare'); + if (!$isVerbose) { + $versions = self::condenseVersionList($versions, 1); + } + if (count($versions) > 1) { + // remove the s from requires/conflicts to correct grammar + $message = preg_replace('{^(%s%s (?:require|conflict))s}', '$1', $message); + $result[] = sprintf($message, $package, '['.implode(', ', $versions).']'); + } else { + $result[] = sprintf($message, $package, ' '.reset($versions)); + } + } + } else { + $result[] = $message; + } + } + + return "\n$indent- ".implode("\n$indent- ", $result); + } + + public function isCausedByLock(RepositorySet $repositorySet, Request $request, Pool $pool) { foreach ($this->reasons as $sectionRules) { foreach ($sectionRules as $rule) { - if ($rule->isCausedByLock()) { + if ($rule->isCausedByLock($repositorySet, $request, $pool)) { return true; } } @@ -138,7 +177,7 @@ class Problem /** * @internal */ - public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $packageName, $constraint = null) + public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, $packageName, $constraint = null) { // handle php/hhvm if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') { @@ -210,7 +249,7 @@ class Problem return $rootReqs[$packageName]->matches(new Constraint('==', $p->getVersion())); }); if (0 === count($filtered)) { - return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with your root composer.json require ('.$rootReqs[$packageName]->getPrettyString().').'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with your root composer.json require ('.$rootReqs[$packageName]->getPrettyString().').'); } } @@ -220,7 +259,7 @@ class Problem return $fixedConstraint->matches(new Constraint('==', $p->getVersion())); }); if (0 === count($filtered)) { - return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but the package is fixed to '.$fixedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but the package is fixed to '.$fixedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.'); } } @@ -229,15 +268,15 @@ class Problem }); if (!$nonLockedPackages) { - return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' in lock file but not in remote repositories, make sure you avoid updating this package to keep the one from lock file.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' in lock file but not in remote repositories, make sure you avoid updating this package to keep the one from lock file.'); } - return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with another require.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with another require.'); } // check if the package is found when bypassing stability checks if ($packages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) { - return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your minimum-stability.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your minimum-stability.'); } // check if the package is found when bypassing the constraint check @@ -257,10 +296,10 @@ class Problem } } - return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable. See https://getcomposer.org/repoprio for details and assistance.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages, $isVerbose).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages, $isVerbose).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable. See https://getcomposer.org/repoprio for details and assistance.'); } - return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your constraint.'); + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match the constraint.'); } if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) { @@ -287,7 +326,7 @@ class Problem /** * @internal */ - public static function getPackageList(array $packages) + public static function getPackageList(array $packages, $isVerbose) { $prepared = array(); foreach ($packages as $package) { @@ -299,12 +338,46 @@ class Problem if (isset($package['versions'][VersionParser::DEV_MASTER_ALIAS]) && isset($package['versions']['dev-master'])) { unset($package['versions'][VersionParser::DEV_MASTER_ALIAS]); } + + uksort($package['versions'], 'version_compare'); + + if (!$isVerbose) { + $package['versions'] = self::condenseVersionList($package['versions'], 4); + } $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']'; } return implode(', ', $prepared); } + /** + * @param string[] $versions an array of pretty versions, with normalized versions as keys + * @return list a list of pretty versions and '...' where versions were removed + */ + private static function condenseVersionList(array $versions, $max) + { + if (count($versions) <= $max) { + return $versions; + } + + $filtered = array(); + $byMajor = array(); + foreach ($versions as $version => $pretty) { + $byMajor[preg_replace('{^(\d+)\..*}', '$1', $version)][] = $pretty; + } + foreach ($byMajor as $versionsForMajor) { + if (count($versionsForMajor) > $max) { + $filtered[] = $versionsForMajor[0]; + $filtered[] = '...'; + $filtered[] = $versionsForMajor[count($versionsForMajor) - 1]; + } else { + $filtered = array_merge($filtered, $versionsForMajor); + } + } + + return $filtered; + } + private static function hasMultipleNames(array $packages) { $name = null; diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 2e82dab82..8ba9756d6 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -17,7 +17,9 @@ use Composer\Package\Link; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; use Composer\Repository\RepositorySet; +use Composer\Repository\PlatformRepository; use Composer\Package\Version\VersionParser; +use Composer\Semver\Constraint\Constraint; /** * @author Nils Adermann @@ -26,7 +28,6 @@ use Composer\Package\Version\VersionParser; abstract class Rule { // reason constants - const RULE_INTERNAL_ALLOW_UPDATE = 1; const RULE_ROOT_REQUIRE = 2; const RULE_FIXED = 3; const RULE_PACKAGE_CONFLICT = 6; @@ -122,27 +123,54 @@ abstract class Rule abstract public function isAssertion(); - public function isCausedByLock() + public function isCausedByLock(RepositorySet $repositorySet, Request $request, Pool $pool) { - return $this->getReason() === self::RULE_FIXED && $this->reasonData['lockable']; + if ($this->getReason() === self::RULE_FIXED && $this->reasonData['lockable']) { + return true; + } + + if ($this->getReason() === self::RULE_PACKAGE_REQUIRES) { + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $this->reasonData->getTarget())) { + return false; + } + foreach ($request->getFixedPackages() as $package) { + if ($package->getName() === $this->reasonData->getTarget()) { + if ($pool->isUnacceptableFixedPackage($package)) { + return true; + } + if (!$this->reasonData->getConstraint()->matches(new Constraint('=', $package->getVersion()))) { + return true; + } + break; + } + } + } + + if ($this->getReason() === self::RULE_ROOT_REQUIRE) { + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $this->reasonData['packageName'])) { + return false; + } + foreach ($request->getFixedPackages() as $package) { + if ($package->getName() === $this->reasonData['packageName']) { + if ($pool->isUnacceptableFixedPackage($package)) { + return true; + } + if (!$this->reasonData['constraint']->matches(new Constraint('=', $package->getVersion()))) { + return true; + } + break; + } + } + } + + return false; } - public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array()) + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, array $installedMap = array(), array $learnedPool = array()) { $literals = $this->getLiterals(); - $ruleText = ''; - foreach ($literals as $i => $literal) { - if ($i != 0) { - $ruleText .= '|'; - } - $ruleText .= $pool->literalToPrettyString($literal, $installedMap); - } - switch ($this->getReason()) { - case self::RULE_INTERNAL_ALLOW_UPDATE: - return $ruleText; - case self::RULE_ROOT_REQUIRE: $packageName = $this->reasonData['packageName']; $constraint = $this->reasonData['constraint']; @@ -152,7 +180,7 @@ abstract class Rule return 'No package found to satisfy root composer.json require '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : ''); } - return 'Root composer.json requires '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '').' -> satisfiable by '.$this->formatPackagesUnique($pool, $packages).'.'; + return 'Root composer.json requires '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '').' -> satisfiable by '.$this->formatPackagesUnique($pool, $packages, $isVerbose).'.'; case self::RULE_FIXED: $package = $this->deduplicateMasterAlias($this->reasonData['package']); @@ -179,11 +207,11 @@ abstract class Rule $text = $this->reasonData->getPrettyString($sourcePackage); if ($requires) { - $text .= ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $requires) . '.'; + $text .= ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $requires, $isVerbose) . '.'; } else { $targetName = $this->reasonData->getTarget(); - $reason = Problem::getMissingPackageReason($repositorySet, $request, $pool, $targetName, $this->reasonData->getConstraint()); + $reason = Problem::getMissingPackageReason($repositorySet, $request, $pool, $isVerbose, $targetName, $this->reasonData->getConstraint()); return $text . ' -> ' . $reason[1]; } @@ -227,29 +255,61 @@ abstract class Rule } if ($installedPackages && $removablePackages) { - return $this->formatPackagesUnique($pool, $removablePackages).' cannot be installed as that would require removing '.$this->formatPackagesUnique($pool, $installedPackages).'. '.$reason; + return $this->formatPackagesUnique($pool, $removablePackages, $isVerbose).' cannot be installed as that would require removing '.$this->formatPackagesUnique($pool, $installedPackages, $isVerbose).'. '.$reason; } - return 'Only one of these can be installed: '.$this->formatPackagesUnique($pool, $literals).'. '.$reason; + return 'Only one of these can be installed: '.$this->formatPackagesUnique($pool, $literals, $isVerbose).'. '.$reason; } - return 'You can only install one version of a package, so only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '.'; + return 'You can only install one version of a package, so only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals, $isVerbose) . '.'; case self::RULE_LEARNED: if (isset($learnedPool[$this->reasonData])) { - $learnedString = ', learned rules:'."\n - "; - $reasons = array(); - foreach ($learnedPool[$this->reasonData] as $learnedRule) { - $reasons[] = $learnedRule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool); - } - $learnedString .= implode("\n - ", array_unique($reasons)); + $learnedString = ', learned rules:' . Problem::formatDeduplicatedRules($learnedPool[$this->reasonData], ' ', $repositorySet, $request, $pool, $installedMap, $learnedPool); } else { $learnedString = ' (reasoning unavailable)'; } + if (count($literals) === 1) { + $ruleText = $pool->literalToPrettyString($literals[0], $installedMap); + } else { + $groups = array(); + foreach ($literals as $literal) { + $package = $pool->literalToPackage($literal); + if (isset($installedMap[$package->id])) { + $group = $literal > 0 ? 'keep' : 'remove'; + } else { + $group = $literal > 0 ? 'install' : 'don\'t install'; + } + + $groups[$group][] = $this->deduplicateMasterAlias($package); + } + $ruleTexts = array(); + foreach ($groups as $group => $packages) { + $ruleTexts[] = $group . (count($packages) > 1 ? ' one of' : '').' ' . $this->formatPackagesUnique($pool, $packages, $isVerbose); + } + + $ruleText = implode(' | ', $ruleTexts); + } + return 'Conclusion: '.$ruleText.$learnedString; case self::RULE_PACKAGE_ALIAS: - return $ruleText; + $aliasPackage = $pool->literalToPackage($literals[0]); + // avoid returning content like "9999999-dev is an alias of dev-master" as it is useless + if ($aliasPackage->getVersion() === VersionParser::DEV_MASTER_ALIAS) { + return ''; + } + $package = $this->deduplicateMasterAlias($pool->literalToPackage($literals[1])); + + return $aliasPackage->getPrettyString() .' is an alias of '.$package->getPrettyString().' and thus requires it to be installed too.'; default: + $ruleText = ''; + foreach ($literals as $i => $literal) { + if ($i != 0) { + $ruleText .= '|'; + } + $ruleText .= $pool->literalToPrettyString($literal, $installedMap); + } + return '('.$ruleText.')'; } } @@ -260,16 +320,16 @@ abstract class Rule * * @return string */ - protected function formatPackagesUnique($pool, array $packages) + protected function formatPackagesUnique($pool, array $packages, $isVerbose) { $prepared = array(); foreach ($packages as $index => $package) { - if (!is_object($package)) { + if (!\is_object($package)) { $packages[$index] = $pool->literalToPackage($package); } } - return Problem::getPackageList($packages); + return Problem::getPackageList($packages, $isVerbose); } private function getReplacedNames(PackageInterface $package) diff --git a/src/Composer/DependencyResolver/Rule2Literals.php b/src/Composer/DependencyResolver/Rule2Literals.php index 2df95e09d..b67843c96 100644 --- a/src/Composer/DependencyResolver/Rule2Literals.php +++ b/src/Composer/DependencyResolver/Rule2Literals.php @@ -76,7 +76,7 @@ class Rule2Literals extends Rule } $literals = $rule->getLiterals(); - if (2 != count($literals)) { + if (2 != \count($literals)) { return false; } diff --git a/src/Composer/DependencyResolver/RuleSet.php b/src/Composer/DependencyResolver/RuleSet.php index d37ca1e9f..5de29bb51 100644 --- a/src/Composer/DependencyResolver/RuleSet.php +++ b/src/Composer/DependencyResolver/RuleSet.php @@ -65,7 +65,7 @@ class RuleSet implements \IteratorAggregate, \Countable // Do not add if rule already exists if (isset($this->rulesByHash[$hash])) { $potentialDuplicates = $this->rulesByHash[$hash]; - if (is_array($potentialDuplicates)) { + if (\is_array($potentialDuplicates)) { foreach ($potentialDuplicates as $potentialDuplicate) { if ($rule->equals($potentialDuplicate)) { return; @@ -90,7 +90,7 @@ class RuleSet implements \IteratorAggregate, \Countable if (!isset($this->rulesByHash[$hash])) { $this->rulesByHash[$hash] = $rule; - } elseif (is_array($this->rulesByHash[$hash])) { + } elseif (\is_array($this->rulesByHash[$hash])) { $this->rulesByHash[$hash][] = $rule; } else { $originalRule = $this->rulesByHash[$hash]; @@ -120,7 +120,7 @@ class RuleSet implements \IteratorAggregate, \Countable public function getIteratorFor($types) { - if (!is_array($types)) { + if (!\is_array($types)) { $types = array($types); } @@ -136,7 +136,7 @@ class RuleSet implements \IteratorAggregate, \Countable public function getIteratorWithout($types) { - if (!is_array($types)) { + if (!\is_array($types)) { $types = array($types); } @@ -157,13 +157,13 @@ class RuleSet implements \IteratorAggregate, \Countable return array_keys($types); } - public function getPrettyString(RepositorySet $repositorySet = null, Request $request = null, Pool $pool = null) + public function getPrettyString(RepositorySet $repositorySet = null, Request $request = null, Pool $pool = null, $isVerbose = false) { $string = "\n"; foreach ($this->rules as $type => $rules) { $string .= str_pad(self::$types[$type], 8, ' ') . ": "; foreach ($rules as $rule) { - $string .= ($repositorySet && $request && $pool ? $rule->getPrettyString($repositorySet, $request, $pool) : $rule)."\n"; + $string .= ($repositorySet && $request && $pool ? $rule->getPrettyString($repositorySet, $request, $pool, $isVerbose) : $rule)."\n"; } $string .= "\n\n"; } diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index e6ac7ac31..e16ce7790 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -120,7 +120,7 @@ class RuleSetGenerator $literals[] = -$package->id; } - if (count($literals) == 2) { + if (\count($literals) == 2) { return new Rule2Literals($literals[0], $literals[1], $reason, $reasonData); } @@ -165,11 +165,18 @@ class RuleSetGenerator $this->addedPackagesByNames[$name][] = $package; } } else { + $workQueue->enqueue($package->getAliasOf()); $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, array($package->getAliasOf()), Rule::RULE_PACKAGE_ALIAS, $package)); + + // if alias package has no self.version requires, its requirements do not + // need to be added as the aliased package processing will take care of it + if (!$package->hasSelfVersionRequires()) { + continue; + } } foreach ($package->getRequires() as $link) { - if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $link->getTarget())) { + if ((true === $ignorePlatformReqs || (is_array($ignorePlatformReqs) && in_array($link->getTarget(), $ignorePlatformReqs, true))) && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $link->getTarget())) { continue; } @@ -193,7 +200,7 @@ class RuleSetGenerator continue; } - if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $link->getTarget())) { + if ((true === $ignorePlatformReqs || (is_array($ignorePlatformReqs) && in_array($link->getTarget(), $ignorePlatformReqs, true))) && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $link->getTarget())) { continue; } @@ -207,27 +214,13 @@ class RuleSetGenerator } foreach ($this->addedPackagesByNames as $name => $packages) { - if (count($packages) > 1) { + if (\count($packages) > 1) { $reason = Rule::RULE_PACKAGE_SAME_NAME; $this->addRule(RuleSet::TYPE_PACKAGE, $this->createMultiConflictRule($packages, $reason, $name)); } } } - protected function obsoleteImpossibleForAlias($package, $provider) - { - $packageIsAlias = $package instanceof AliasPackage; - $providerIsAlias = $provider instanceof AliasPackage; - - $impossible = ( - ($packageIsAlias && $package->getAliasOf() === $provider) || - ($providerIsAlias && $provider->getAliasOf() === $package) || - ($packageIsAlias && $providerIsAlias && $provider->getAliasOf() === $package->getAliasOf()) - ); - - return $impossible; - } - protected function addRulesForRequest(Request $request, $ignorePlatformReqs) { $unlockableMap = $request->getUnlockableMap(); @@ -253,7 +246,7 @@ class RuleSetGenerator } foreach ($request->getRequires() as $packageName => $constraint) { - if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $packageName)) { + if ((true === $ignorePlatformReqs || (is_array($ignorePlatformReqs) && in_array($packageName, $ignorePlatformReqs, true))) && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $packageName)) { continue; } @@ -272,6 +265,9 @@ class RuleSetGenerator } } + /** + * @param bool|array $ignorePlatformReqs + */ public function getRulesFor(Request $request, $ignorePlatformReqs = false) { $this->rules = new RuleSet; diff --git a/src/Composer/DependencyResolver/RuleSetIterator.php b/src/Composer/DependencyResolver/RuleSetIterator.php index 8c048624f..1c95218da 100644 --- a/src/Composer/DependencyResolver/RuleSetIterator.php +++ b/src/Composer/DependencyResolver/RuleSetIterator.php @@ -51,7 +51,7 @@ class RuleSetIterator implements \Iterator return; } - if ($this->currentOffset >= count($this->rules[$this->currentType])) { + if ($this->currentOffset >= \count($this->rules[$this->currentType])) { $this->currentOffset = 0; do { @@ -63,7 +63,7 @@ class RuleSetIterator implements \Iterator } $this->currentType = $this->types[$this->currentTypeOffset]; - } while (isset($this->types[$this->currentTypeOffset]) && !count($this->rules[$this->currentType])); + } while (isset($this->types[$this->currentTypeOffset]) && !\count($this->rules[$this->currentType])); } } @@ -83,7 +83,7 @@ class RuleSetIterator implements \Iterator } $this->currentType = $this->types[$this->currentTypeOffset]; - } while (isset($this->types[$this->currentTypeOffset]) && !count($this->rules[$this->currentType])); + } while (isset($this->types[$this->currentTypeOffset]) && !\count($this->rules[$this->currentType])); } public function valid() diff --git a/src/Composer/DependencyResolver/RuleWatchNode.php b/src/Composer/DependencyResolver/RuleWatchNode.php index 926c144b4..0986b08b1 100644 --- a/src/Composer/DependencyResolver/RuleWatchNode.php +++ b/src/Composer/DependencyResolver/RuleWatchNode.php @@ -37,7 +37,7 @@ class RuleWatchNode $literals = $rule->getLiterals(); - $literalCount = count($literals); + $literalCount = \count($literals); $this->watch1 = $literalCount > 0 ? $literals[0] : 0; $this->watch2 = $literalCount > 1 ? $literals[1] : 0; } @@ -55,7 +55,7 @@ class RuleWatchNode $literals = $this->rule->getLiterals(); // if there are only 2 elements, both are being watched anyway - if (count($literals) < 3 || $this->rule instanceof MultiConflictRule) { + if (\count($literals) < 3 || $this->rule instanceof MultiConflictRule) { return; } diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 73a17b112..73e79c819 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -31,8 +31,6 @@ class Solver /** @var RuleSet */ protected $rules; - /** @var RuleSetGenerator */ - protected $ruleSetGenerator; /** @var RuleWatchGraph */ protected $watchGraph; @@ -75,7 +73,7 @@ class Solver */ public function getRuleSetSize() { - return count($this->rules); + return \count($this->rules); } public function getPool() @@ -87,9 +85,9 @@ class Solver private function makeAssertionRuleDecisions() { - $decisionStart = count($this->decisions) - 1; + $decisionStart = \count($this->decisions) - 1; - $rulesCount = count($this->rules); + $rulesCount = \count($this->rules); for ($ruleIndex = 0; $ruleIndex < $rulesCount; $ruleIndex++) { $rule = $this->rules->ruleById[$ruleIndex]; @@ -164,13 +162,13 @@ class Solver } /** - * @param Request $request - * @param bool $ignorePlatformReqs + * @param Request $request + * @param bool|array $ignorePlatformReqs */ - protected function checkForRootRequireProblems($request, $ignorePlatformReqs) + protected function checkForRootRequireProblems(Request $request, $ignorePlatformReqs) { foreach ($request->getRequires() as $packageName => $constraint) { - if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $packageName)) { + if ((true === $ignorePlatformReqs || (is_array($ignorePlatformReqs) && in_array($packageName, $ignorePlatformReqs, true))) && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $packageName)) { continue; } @@ -183,8 +181,8 @@ class Solver } /** - * @param Request $request - * @param bool $ignorePlatformReqs + * @param Request $request + * @param bool|array $ignorePlatformReqs * @return LockTransaction */ public function solve(Request $request, $ignorePlatformReqs = false) @@ -192,8 +190,9 @@ class Solver $this->setupFixedMap($request); $this->io->writeError('Generating rules', true, IOInterface::DEBUG); - $this->ruleSetGenerator = new RuleSetGenerator($this->policy, $this->pool); - $this->rules = $this->ruleSetGenerator->getRulesFor($request, $ignorePlatformReqs); + $ruleSetGenerator = new RuleSetGenerator($this->policy, $this->pool); + $this->rules = $ruleSetGenerator->getRulesFor($request, $ignorePlatformReqs); + unset($ruleSetGenerator); $this->checkForRootRequireProblems($request, $ignorePlatformReqs); $this->decisions = new Decisions($this->pool); $this->watchGraph = new RuleWatchGraph; @@ -269,10 +268,10 @@ class Solver } $this->decisions->revertLast(); - $this->propagateIndex = count($this->decisions); + $this->propagateIndex = \count($this->decisions); } - while (!empty($this->branches) && $this->branches[count($this->branches) - 1][self::BRANCH_LEVEL] >= $level) { + while (!empty($this->branches) && $this->branches[\count($this->branches) - 1][self::BRANCH_LEVEL] >= $level) { array_pop($this->branches); } } @@ -357,7 +356,7 @@ class Solver $selectedLiteral = array_shift($literals); // if there are multiple candidates, then branch - if (count($literals)) { + if (\count($literals)) { $this->branches[] = array($literals, $level); } @@ -378,12 +377,12 @@ class Solver $seen = array(); $learnedLiterals = array(null); - $decisionId = count($this->decisions); + $decisionId = \count($this->decisions); $this->learnedPool[] = array(); while (true) { - $this->learnedPool[count($this->learnedPool) - 1][] = $rule; + $this->learnedPool[\count($this->learnedPool) - 1][] = $rule; foreach ($rule->getLiterals() as $literal) { // skip the one true literal @@ -466,7 +465,7 @@ class Solver $rule = $decision[Decisions::DECISION_REASON]; } - $why = count($this->learnedPool) - 1; + $why = \count($this->learnedPool) - 1; if (!$learnedLiterals[0]) { throw new SolverBugException( @@ -647,7 +646,7 @@ class Solver } } - if ($noneSatisfied && count($decisionQueue)) { + if ($noneSatisfied && \count($decisionQueue)) { // if any of the options in the decision queue are fixed, only use those $prunedQueue = array(); foreach ($decisionQueue as $literal) { @@ -660,7 +659,7 @@ class Solver } } - if ($noneSatisfied && count($decisionQueue)) { + if ($noneSatisfied && \count($decisionQueue)) { $oLevel = $level; $level = $this->selectAndInstall($level, $decisionQueue, $rule); @@ -687,7 +686,7 @@ class Solver $systemLevel = $level; } - $rulesCount = count($this->rules); + $rulesCount = \count($this->rules); $pass = 1; $this->io->writeError('Looking at all rules.', true, IOInterface::DEBUG); @@ -734,7 +733,7 @@ class Solver } // need to have at least 2 item to pick from - if (count($decisionQueue) < 2) { + if (\count($decisionQueue) < 2) { continue; } @@ -745,7 +744,7 @@ class Solver } // something changed, so look at all rules again - $rulesCount = count($this->rules); + $rulesCount = \count($this->rules); $n = -1; } @@ -754,13 +753,13 @@ class Solver } // minimization step - if (count($this->branches)) { + if (\count($this->branches)) { $lastLiteral = null; $lastLevel = null; $lastBranchIndex = 0; $lastBranchOffset = 0; - for ($i = count($this->branches) - 1; $i >= 0; $i--) { + for ($i = \count($this->branches) - 1; $i >= 0; $i--) { list($literals, $l) = $this->branches[$i]; foreach ($literals as $offset => $literal) { diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index 1821bde91..ff94c8932 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -31,7 +31,7 @@ class SolverProblemsException extends \RuntimeException parent::__construct('Failed resolving dependencies with '.count($problems).' problems, call getPrettyString to get formatted details', 2); } - public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isDevExtraction = false) + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, $isDevExtraction = false) { $installedMap = $request->getPresentMap(true); $hasExtensionProblems = false; @@ -39,13 +39,13 @@ class SolverProblemsException extends \RuntimeException $problems = array(); foreach ($this->problems as $problem) { - $problems[] = $problem->getPrettyString($repositorySet, $request, $pool, $installedMap, $this->learnedPool)."\n"; + $problems[] = $problem->getPrettyString($repositorySet, $request, $pool, $isVerbose, $installedMap, $this->learnedPool)."\n"; if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) { $hasExtensionProblems = true; } - $isCausedByLock |= $problem->isCausedByLock(); + $isCausedByLock |= $problem->isCausedByLock($repositorySet, $request, $pool); } $i = 1; @@ -54,27 +54,36 @@ class SolverProblemsException extends \RuntimeException $text .= " Problem ".($i++).$problem; } + $hints = array(); if (!$isDevExtraction && (strpos($text, 'could not be found') || strpos($text, 'no matching package found'))) { - $text .= "\nPotential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n see for more details.\n - It's a private package and you forgot to add a custom repository to find it\n\nRead for further common problems."; + $hints[] = "Potential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n see for more details.\n - It's a private package and you forgot to add a custom repository to find it\n\nRead for further common problems."; } if ($hasExtensionProblems) { - $text .= $this->createExtensionHint(); + $hints[] = $this->createExtensionHint(); } if ($isCausedByLock && !$isDevExtraction) { - $text .= "\nUse the option --with-all-dependencies to allow updates and removals for packages currently locked to specific versions."; + $hints[] = "Use the option --with-all-dependencies to allow upgrades, downgrades and removals for packages currently locked to specific versions."; + } + + if (strpos($text, 'found composer-plugin-api[2.0.0] but it does not match') && strpos($text, '- ocramius/package-versions')) { + $hints[] = "ocramius/package-versions only provides support for Composer 2 in 1.8+, which requires PHP 7.4.\nIf you can not upgrade PHP you can require composer/package-versions-deprecated to resolve this with PHP 7.0+."; } // TODO remove before 2.0 final if (!class_exists('PHPUnit\Framework\TestCase', false)) { if (strpos($text, 'found composer-plugin-api[2.0.0] but it does not match')) { - $text .= "\nYou are using a snapshot build of Composer 2, which some of your plugins seem to be incompatible with. Make sure you update your plugins or report an issue to them to ask them to support Composer 2. To work around this you can run Composer with --ignore-platform-reqs, but this will also ignore your PHP version and may result in bigger problems down the line."; + $hints[] = "You are using a snapshot build of Composer 2, which some of your plugins seem to be incompatible with. Make sure you update your plugins or report an issue to them to ask them to support Composer 2. To work around this you can run Composer with --ignore-platform-req=composer-plugin-api, but this may result in broken plugins and bigger problems down the line."; } else { - $text .= "\nYou are using a snapshot build of Composer 2, which may be the cause of the problem. Run `composer self-update --stable` and then try again. In case it solves the problem, please report an issue mentioning Composer 2."; + $hints[] = "You are using a snapshot build of Composer 2, which may be the cause of the problem. Run `composer self-update --stable` and then try again. In case it solves the problem, please report an issue mentioning Composer 2."; } } + if ($hints) { + $text .= "\n" . implode("\n\n", $hints); + } + return $text; } @@ -91,9 +100,9 @@ class SolverProblemsException extends \RuntimeException return ''; } - $text = "\n To enable extensions, verify that they are enabled in your .ini files:\n - "; + $text = "To enable extensions, verify that they are enabled in your .ini files:\n - "; $text .= implode("\n - ", $paths); - $text .= "\n You can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode."; + $text .= "\nYou can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode."; return $text; } diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php index 6f7cc79fb..a919238f6 100644 --- a/src/Composer/DependencyResolver/Transaction.php +++ b/src/Composer/DependencyResolver/Transaction.php @@ -261,9 +261,9 @@ class Transaction // is this a plugin or a dependency of a plugin? if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) { - // get the package's requires, but filter out any platform requirements or 'composer-plugin-api' + // get the package's requires, but filter out any platform requirements $requires = array_filter(array_keys($package->getRequires()), function ($req) { - return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); + return !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); }); // is this a plugin with no meaningful dependencies? @@ -295,7 +295,7 @@ class Transaction { $uninstOps = array(); foreach ($operations as $idx => $op) { - if ($op instanceof Operation\UninstallOperation) { + if ($op instanceof Operation\UninstallOperation || $op instanceof Operation\MarkAliasUninstalledOperation) { $uninstOps[] = $op; unset($operations[$idx]); } diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 318e73c6f..283d0103e 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -15,6 +15,7 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Symfony\Component\Finder\Finder; use Composer\IO\IOInterface; +use Composer\Exception\IrrecoverableDownloadException; /** * Base downloader for archives @@ -25,6 +26,18 @@ use Composer\IO\IOInterface; */ abstract class ArchiveDownloader extends FileDownloader { + public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) + { + $res = parent::download($package, $path, $prevPackage, $output); + + // if not downgrading and the dir already exists it seems we have an inconsistent state in the vendor dir and the user should fix it + if (!$prevPackage && is_dir($path) && !$this->filesystem->isDirEmpty($path)) { + throw new IrrecoverableDownloadException('Expected empty path to extract '.$package.' into but directory exists: '.$path); + } + + return $res; + } + /** * {@inheritDoc} * @throws \RuntimeException @@ -40,7 +53,7 @@ abstract class ArchiveDownloader extends FileDownloader $this->filesystem->ensureDirectoryExists($path); if (!$this->filesystem->isDirEmpty($path)) { - throw new \RuntimeException('Expected empty path to extract '.$package.' into but directory exists: '.$path); + throw new \UnexpectedValueException('Expected empty path to extract '.$package.' into but directory exists: '.$path); } do { diff --git a/src/Composer/Downloader/DownloadManager.php b/src/Composer/Downloader/DownloadManager.php index 3ee786c17..45467eb4c 100644 --- a/src/Composer/Downloader/DownloadManager.php +++ b/src/Composer/Downloader/DownloadManager.php @@ -15,6 +15,7 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\IO\IOInterface; use Composer\Util\Filesystem; +use Composer\Exception\IrrecoverableDownloadException; use React\Promise\PromiseInterface; /** @@ -25,7 +26,6 @@ use React\Promise\PromiseInterface; class DownloadManager { private $io; - private $httpDownloader; private $preferDist = false; private $preferSource = false; private $packagePreferences = array(); @@ -196,7 +196,7 @@ class DownloadManager } $handleError = function ($e) use ($sources, $source, $package, $io, $download) { - if ($e instanceof \RuntimeException) { + if ($e instanceof \RuntimeException && !$e instanceof IrrecoverableDownloadException) { if (!$sources) { throw $e; } diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 77a310888..c598efd9a 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -141,7 +141,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface ->then($accept, $reject); } - return $result->then(function ($result) use ($fileName, $checksum, $url, $eventDispatcher) { + return $result->then(function ($result) use ($fileName, $checksum, $url, $package, $eventDispatcher) { // in case of retry, the first call's Promise chain finally calls this twice at the end, // once with $result being the returned $fileName from $accept, and then once for every // failed request with a null result, which can be skipped. @@ -159,7 +159,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface } if ($eventDispatcher) { - $postFileDownloadEvent = new PostFileDownloadEvent(PluginEvents::POST_FILE_DOWNLOAD, $fileName, $checksum, $url['processed']); + $postFileDownloadEvent = new PostFileDownloadEvent(PluginEvents::POST_FILE_DOWNLOAD, $fileName, $checksum, $url['processed'], $package); $eventDispatcher->dispatch($postFileDownloadEvent->getName(), $postFileDownloadEvent); } @@ -325,7 +325,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface */ protected function getFileName(PackageInterface $package, $path) { - return rtrim($this->config->get('vendor-dir').'/composer/'.md5($package.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.'); + return rtrim($this->config->get('vendor-dir').'/composer/tmp-'.md5($package.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.'); } /** diff --git a/src/Composer/Downloader/SvnDownloader.php b/src/Composer/Downloader/SvnDownloader.php index 0281862b5..634c4a7d5 100644 --- a/src/Composer/Downloader/SvnDownloader.php +++ b/src/Composer/Downloader/SvnDownloader.php @@ -178,7 +178,7 @@ class SvnDownloader extends VcsDownloader */ protected function getCommitLogs($fromReference, $toReference, $path) { - if (preg_match('{.*@(\d+)$}', $fromReference) && preg_match('{.*@(\d+)$}', $toReference)) { + if (preg_match('{@(\d+)$}', $fromReference) && preg_match('{@(\d+)$}', $toReference)) { // retrieve the svn base url from the checkout folder $command = sprintf('svn info --non-interactive --xml %s', ProcessExecutor::escape($path)); if (0 !== $this->process->execute($command, $output, $path)) { diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index 6cbb0c314..2db26eb5f 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -160,7 +160,6 @@ class EventDispatcher if (is_array($callable) && (is_string($callable[0]) || is_object($callable[0])) && is_string($callable[1])) { $this->io->writeError(sprintf('> %s: %s', $event->getName(), (is_object($callable[0]) ? get_class($callable[0]) : $callable[0]).'->'.$callable[1] ), true, IOInterface::VERBOSE); } - $event = $this->checkListenerExpectedEvent($callable, $event); $return = false === call_user_func($callable, $event) ? 1 : 0; } elseif ($this->isComposerScript($callable)) { $this->io->writeError(sprintf('> %s: %s', $event->getName(), $callable), true, IOInterface::VERBOSE); @@ -304,8 +303,6 @@ class EventDispatcher */ protected function executeEventPhpScript($className, $methodName, Event $event) { - $event = $this->checkListenerExpectedEvent(array($className, $methodName), $event); - if ($this->io->isVerbose()) { $this->io->writeError(sprintf('> %s: %s::%s', $event->getName(), $className, $methodName)); } else { @@ -315,38 +312,6 @@ class EventDispatcher return $className::$methodName($event); } - /** - * @param mixed $target - * @param Event $event - * @return Event - */ - protected function checkListenerExpectedEvent($target, Event $event) - { - if (in_array($event->getName(), array( - 'init', - 'command', - 'pre-file-download', - ), true)) { - return $event; - } - - try { - $reflected = new \ReflectionParameter($target, 0); - } catch (\Exception $e) { - return $event; - } - - $typehint = $reflected->getClass(); - - if (!$typehint instanceof \ReflectionClass) { - return $event; - } - - $expected = $typehint->getName(); - - return $event; - } - private function serializeCallback($cb) { if (is_array($cb) && count($cb) === 2) { diff --git a/src/Composer/Exception/IrrecoverableDownloadException.php b/src/Composer/Exception/IrrecoverableDownloadException.php new file mode 100644 index 000000000..72a833c72 --- /dev/null +++ b/src/Composer/Exception/IrrecoverableDownloadException.php @@ -0,0 +1,20 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Exception; + +/** + * @author Jordi Boggiano + */ +class IrrecoverableDownloadException extends \RuntimeException +{ +} diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 85b958370..1b084461c 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -17,6 +17,7 @@ use Composer\Json\JsonFile; use Composer\IO\IOInterface; use Composer\Package\Archiver; use Composer\Package\Version\VersionGuesser; +use Composer\Package\RootPackageInterface; use Composer\Repository\RepositoryManager; use Composer\Repository\RepositoryFactory; use Composer\Repository\WritableRepositoryInterface; @@ -335,6 +336,7 @@ class Factory $httpDownloader = self::createHttpDownloader($io, $config); $loop = new Loop($httpDownloader); + $composer->setLoop($loop); // initialize event dispatcher $dispatcher = new EventDispatcher($composer, $io); @@ -344,9 +346,6 @@ class Factory $rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher); $composer->setRepositoryManager($rm); - // load local repository - $this->addLocalRepository($io, $rm, $vendorDir); - // force-set the version of the global package if not defined as // guessing it adds no value and only takes time if (!$fullLoad && !isset($localConfig['version'])) { @@ -360,6 +359,9 @@ class Factory $package = $loader->load($localConfig, 'Composer\Package\RootPackage', $cwd); $composer->setPackage($package); + // load local repository + $this->addLocalRepository($io, $rm, $vendorDir, $package); + // initialize installation manager $im = $this->createInstallationManager($loop, $io, $dispatcher); $composer->setInstallationManager($im); @@ -431,9 +433,9 @@ class Factory * @param Repository\RepositoryManager $rm * @param string $vendorDir */ - protected function addLocalRepository(IOInterface $io, RepositoryManager $rm, $vendorDir) + protected function addLocalRepository(IOInterface $io, RepositoryManager $rm, $vendorDir, RootPackageInterface $rootPackage) { - $rm->setLocalRepository(new Repository\InstalledFilesystemRepository(new JsonFile($vendorDir.'/composer/installed.json', null, $io))); + $rm->setLocalRepository(new Repository\InstalledFilesystemRepository(new JsonFile($vendorDir.'/composer/installed.json', null, $io), true, $rootPackage)); } /** @@ -582,7 +584,7 @@ class Factory * @param array $options Array of options passed directly to HttpDownloader constructor * @return HttpDownloader */ - public static function createHttpDownloader(IOInterface $io, Config $config = null, $options = array()) + public static function createHttpDownloader(IOInterface $io, Config $config, $options = array()) { static $warned = false; $disableTls = false; diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index e2d916a15..e3a263301 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -135,7 +135,9 @@ abstract class BaseIO implements IOInterface } foreach ($gitlabToken as $domain => $token) { - $this->checkAndSetAuthentication($domain, $token, 'private-token'); + $username = is_array($token) && array_key_exists("username", $token) ? $token["username"] : $token; + $password = is_array($token) && array_key_exists("token", $token) ? $token["token"] : 'private-token'; + $this->checkAndSetAuthentication($domain, $username, $password); } // reload http basic credentials from config if available diff --git a/src/Composer/InstalledVersions.php b/src/Composer/InstalledVersions.php new file mode 100644 index 000000000..c2c6dbd3e --- /dev/null +++ b/src/Composer/InstalledVersions.php @@ -0,0 +1,186 @@ + + */ + public static function getInstalledPackages() + { + return array_keys(self::$installed['versions']); + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @return bool + */ + public static function isInstalled($packageName) + { + return isset(self::$installed['versions'][$packageName]); + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param ?string $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints($constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + if (!isset(self::$installed['versions'][$packageName])) { + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + $ranges = array(); + if (isset(self::$installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = self::$installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', self::$installed['versions'][$packageName])) { + $ranges = array_merge($ranges, self::$installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', self::$installed['versions'][$packageName])) { + $ranges = array_merge($ranges, self::$installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', self::$installed['versions'][$packageName])) { + $ranges = array_merge($ranges, self::$installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + if (!isset(self::$installed['versions'][$packageName])) { + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + if (!isset(self::$installed['versions'][$packageName]['version'])) { + return null; + } + + return self::$installed['versions'][$packageName]['version']; + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + if (!isset(self::$installed['versions'][$packageName])) { + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + if (!isset(self::$installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return self::$installed['versions'][$packageName]['pretty_version']; + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + if (!isset(self::$installed['versions'][$packageName])) { + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + if (!isset(self::$installed['versions'][$packageName]['reference'])) { + return null; + } + + return self::$installed['versions'][$packageName]['reference']; + } + + /** + * @return array + * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[]} + */ + public static function getRootPackage() + { + return self::$installed['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @return array[] + * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[]}, versions: list} + */ + public static function getRawData() + { + return self::$installed; + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[]}, versions: list} $data + */ + public static function reload($data) + { + self::$installed = $data; + } +} diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 7d1da54a4..23421f666 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -43,6 +43,7 @@ use Composer\Package\Link; use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; +use Composer\Package\Version\VersionParser; use Composer\Package\Package; use Composer\Repository\ArrayRepository; use Composer\Repository\RepositorySet; @@ -128,6 +129,7 @@ class Installer protected $dryRun = false; protected $verbose = false; protected $update = false; + protected $install = true; protected $dumpAutoloader = true; protected $runScripts = true; protected $ignorePlatformReqs = false; @@ -202,6 +204,8 @@ class Installer throw new \RuntimeException("The installer options updateMirrors and updateAllowList are mutually exclusive."); } + $isFreshInstall = $this->repositoryManager->getLocalRepository()->isFresh(); + // Force update if there is no lock file present if (!$this->update && !$this->locker->isLocked()) { $this->io->writeError('No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file.'); @@ -217,6 +221,10 @@ class Installer $this->mockLocalRepositories($this->repositoryManager); } + if ($this->update && !$this->install) { + $this->dumpAutoloader = false; + } + if ($this->runScripts) { $_SERVER['COMPOSER_DEV_MODE'] = (int) $this->devMode; putenv('COMPOSER_DEV_MODE='.$_SERVER['COMPOSER_DEV_MODE']); @@ -238,8 +246,7 @@ class Installer try { if ($this->update) { - // TODO introduce option to set doInstall to false (update lock file without vendor install) - $res = $this->doUpdate($localRepo, true); + $res = $this->doUpdate($localRepo, $this->install); } else { $res = $this->doInstall($localRepo); } @@ -247,13 +254,13 @@ class Installer return $res; } } catch (\Exception $e) { - if ($this->executeOperations && $this->config->get('notify-on-install')) { + if ($this->executeOperations && $this->install && $this->config->get('notify-on-install')) { $this->installationManager->notifyInstalls($this->io); } throw $e; } - if ($this->executeOperations && $this->config->get('notify-on-install')) { + if ($this->executeOperations && $this->install && $this->config->get('notify-on-install')) { $this->installationManager->notifyInstalls($this->io); } @@ -263,6 +270,9 @@ class Installer $this->createPlatformRepo(false), new RootPackageRepository(clone $this->package), )); + if ($isFreshInstall) { + $this->suggestedPackagesReporter->addSuggestionsFromPackage($this->package); + } $this->suggestedPackagesReporter->outputMinimalistic($installedRepo); } @@ -298,10 +308,11 @@ class Installer $this->autoloadGenerator->setClassMapAuthoritative($this->classMapAuthoritative); $this->autoloadGenerator->setApcu($this->apcuAutoloader); $this->autoloadGenerator->setRunScripts($this->runScripts); + $this->autoloadGenerator->setIgnorePlatformRequirements($this->ignorePlatformReqs); $this->autoloadGenerator->dump($this->config, $localRepo, $this->package, $this->installationManager, 'composer', $this->optimizeAutoloader); } - if ($this->executeOperations) { + if ($this->install && $this->executeOperations) { // force binaries re-generation in case they are missing foreach ($localRepo->getPackages() as $package) { $this->installationManager->ensureBinariesPresence($package); @@ -402,7 +413,7 @@ class Installer $solver = null; } catch (SolverProblemsException $e) { $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); - $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool)); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose())); if (!$this->devMode) { $this->io->writeError('Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.', true, IOInterface::QUIET); } @@ -563,7 +574,7 @@ class Installer $this->io->writeError('Unable to find a compatible set of packages based on your non-dev requirements alone.', true, IOInterface::QUIET); $this->io->writeError('Your requirements can be resolved successfully when require-dev packages are present.'); $this->io->writeError('You may need to move packages from require-dev or some of their dependencies to require.'); - $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool, true)); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose(), true)); return max(1, $e->getCode()); } @@ -627,7 +638,7 @@ class Installer } } catch (SolverProblemsException $e) { $this->io->writeError('Your lock file does not contain a compatible set of packages. Please run composer update.', true, IOInterface::QUIET); - $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool)); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose())); return max(1, $e->getCode()); } @@ -727,7 +738,7 @@ class Installer $rootRequires = array(); foreach ($requires as $req => $constraint) { // skip platform requirements from the root package to avoid filtering out existing platform packages - if ($this->ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req)) { + if ((true === $this->ignorePlatformReqs || (is_array($this->ignorePlatformReqs) && in_array($req, $this->ignorePlatformReqs, true))) && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req)) { continue; } if ($constraint instanceof Link) { @@ -741,6 +752,8 @@ class Installer $this->fixedRootPackage->setRequires(array()); $this->fixedRootPackage->setDevRequires(array()); + $stabilityFlags[$this->package->getName()] = BasePackage::$stabilities[VersionParser::parseStability($this->package->getVersion())]; + $repositorySet = new RepositorySet($minimumStability, $stabilityFlags, $rootAliases, $this->package->getReferences(), $rootRequires); $repositorySet->addRepository(new RootPackageRepository($this->fixedRootPackage)); $repositorySet->addRepository($platformRepo); @@ -1010,6 +1023,19 @@ class Installer return $this; } + /** + * Allows disabling the install step after an update + * + * @param bool $install + * @return Installer + */ + public function setInstall($install = true) + { + $this->install = (bool) $install; + + return $this; + } + /** * enables dev packages * @@ -1092,12 +1118,22 @@ class Installer /** * set ignore Platform Package requirements * - * @param bool $ignorePlatformReqs + * If this is set to true, all platform requirements are ignored + * If this is set to false, no platform requirements are ignored + * If this is set to string[], those packages will be ignored + * + * @param bool|array $ignorePlatformReqs * @return Installer */ public function setIgnorePlatformRequirements($ignorePlatformReqs = false) { - $this->ignorePlatformReqs = (bool) $ignorePlatformReqs; + if (is_array($ignorePlatformReqs)) { + $this->ignorePlatformReqs = array_filter($ignorePlatformReqs, function ($req) { + return (bool) preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); + }); + } else { + $this->ignorePlatformReqs = (bool) $ignorePlatformReqs; + } return $this; } diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 6d752ae58..bd1bf8aee 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -171,34 +171,98 @@ class InstallationManager public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true) { $promises = array(); - - foreach ($operations as $operation) { - $opType = $operation->getOperationType(); - $promise = null; - - if ($opType === 'install') { - $package = $operation->getPackage(); - $installer = $this->getInstaller($package->getType()); - $promise = $installer->download($package); - } elseif ($opType === 'update') { - $target = $operation->getTargetPackage(); - $targetType = $target->getType(); - $installer = $this->getInstaller($targetType); - $promise = $installer->download($target, $operation->getInitialPackage()); - } - - if ($promise) { - $promises[] = $promise; - } - } - - if (!empty($promises)) { - $this->loop->wait($promises); - } - $cleanupPromises = array(); + + $loop = $this->loop; + $runCleanup = function () use (&$cleanupPromises, $loop) { + $promises = array(); + + foreach ($cleanupPromises as $cleanup) { + $promises[] = new \React\Promise\Promise(function ($resolve, $reject) use ($cleanup) { + $promise = $cleanup(); + if (null === $promise) { + $resolve(); + } else { + $promise->then(function () use ($resolve) { + $resolve(); + }); + } + }); + } + + if (!empty($promises)) { + $loop->wait($promises); + } + }; + + $handleInterruptsUnix = function_exists('pcntl_async_signals') && function_exists('pcntl_signal'); + $handleInterruptsWindows = function_exists('sapi_windows_set_ctrl_handler'); + $prevHandler = null; + $windowsHandler = null; + if ($handleInterruptsUnix) { + pcntl_async_signals(true); + $prevHandler = pcntl_signal_get_handler(SIGINT); + pcntl_signal(SIGINT, function ($sig) use ($runCleanup, $prevHandler) { + $runCleanup(); + + if (!in_array($prevHandler, array(SIG_DFL, SIG_IGN), true)) { + call_user_func($prevHandler, $sig); + } + + exit(130); + }); + } + if ($handleInterruptsWindows) { + $windowsHandler = function () use ($runCleanup) { + $runCleanup(); + + exit(130); + }; + sapi_windows_set_ctrl_handler($windowsHandler, true); + } + try { - foreach ($operations as $operation) { + foreach ($operations as $index => $operation) { + $opType = $operation->getOperationType(); + + // ignoring alias ops as they don't need to execute anything at this stage + if (!in_array($opType, array('update', 'install', 'uninstall'))) { + continue; + } + + if ($opType === 'update') { + $package = $operation->getTargetPackage(); + $initialPackage = $operation->getInitialPackage(); + } else { + $package = $operation->getPackage(); + $initialPackage = null; + } + $installer = $this->getInstaller($package->getType()); + + $cleanupPromises[$index] = function () use ($opType, $installer, $package, $initialPackage) { + // avoid calling cleanup if the download was not even initialized for a package + // as without installation source configured nothing will work + if (!$package->getInstallationSource()) { + return; + } + + return $installer->cleanup($opType, $package, $initialPackage); + }; + + if ($opType !== 'uninstall') { + $promise = $installer->download($package, $initialPackage); + if ($promise) { + $promises[] = $promise; + } + } + } + + // execute all downloads first + if (!empty($promises)) { + $this->loop->wait($promises); + } + + foreach ($operations as $index => $operation) { $opType = $operation->getOperationType(); // ignoring alias ops as they don't need to execute anything @@ -236,13 +300,9 @@ class InstallationManager $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); }); } - $cleanupPromise = function () use ($opType, $installer, $package, $initialPackage) { - return $installer->cleanup($opType, $package, $initialPackage); - }; - $promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) { return $installManager->$opType($repo, $operation); - })->then($cleanupPromise) + })->then($cleanupPromises[$index]) ->then(function () use ($opType, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) { $repo->write($devMode, $installManager); @@ -256,34 +316,37 @@ class InstallationManager throw $e; }); - $cleanupPromises[] = $cleanupPromise; $promises[] = $promise; } + // execute all prepare => installs/updates/removes => cleanup steps if (!empty($promises)) { $this->loop->wait($promises); } } catch (\Exception $e) { - $promises = array(); - foreach ($cleanupPromises as $cleanup) { - $promises[] = new \React\Promise\Promise(function ($resolve, $reject) use ($cleanup) { - $promise = $cleanup(); - if (null === $promise) { - $resolve(); - } else { - $promise->then(function () use ($resolve) { - $resolve(); - }); - } - }); - } + $runCleanup(); - if (!empty($promises)) { - $this->loop->wait($promises); + if ($handleInterruptsUnix) { + pcntl_signal(SIGINT, $prevHandler); + } + if ($handleInterruptsWindows) { + sapi_windows_set_ctrl_handler($prevHandler, false); } throw $e; } + + if ($handleInterruptsUnix) { + pcntl_signal(SIGINT, $prevHandler); + } + if ($handleInterruptsWindows) { + sapi_windows_set_ctrl_handler($prevHandler, false); + } + + // do a last write so that we write the repository even if nothing changed + // as that can trigger an update of some files like InstalledVersions.php if + // running a new composer version + $repo->write($devMode, $this); } /** diff --git a/src/Composer/Installer/SuggestedPackagesReporter.php b/src/Composer/Installer/SuggestedPackagesReporter.php index e70422d14..56fe4599f 100644 --- a/src/Composer/Installer/SuggestedPackagesReporter.php +++ b/src/Composer/Installer/SuggestedPackagesReporter.php @@ -14,7 +14,7 @@ namespace Composer\Installer; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; -use Composer\Repository\RepositoryInterface; +use Composer\Repository\InstalledRepository; use Symfony\Component\Console\Formatter\OutputFormatter; /** @@ -98,12 +98,14 @@ class SuggestedPackagesReporter * * Do not list the ones already installed if installed repository provided. * - * @param int $mode One of the MODE_* constants from this class + * @param int $mode One of the MODE_* constants from this class + * @param InstalledRepository|null $installedRepo If passed in, suggested packages which are installed already will be skipped + * @param PackageInterface|null $onlyDependentsOf If passed in, only the suggestions from direct dependents of that package, or from the package itself, will be shown * @return SuggestedPackagesReporter */ - public function output($mode, RepositoryInterface $installedRepo = null) + public function output($mode, InstalledRepository $installedRepo = null, PackageInterface $onlyDependentsOf = null) { - $suggestedPackages = $this->getFilteredSuggestions($installedRepo); + $suggestedPackages = $this->getFilteredSuggestions($installedRepo, $onlyDependentsOf); $suggesters = array(); $suggested = array(); @@ -151,19 +153,27 @@ class SuggestedPackagesReporter } } + if ($onlyDependentsOf) { + $allSuggestedPackages = $this->getFilteredSuggestions($installedRepo); + $diff = count($allSuggestedPackages) - count($suggestedPackages); + if ($diff) { + $this->io->write(''.$diff.' additional suggestions by transitive dependencies can be shown with --all'); + } + } + return $this; } /** * Output number of new suggested packages and a hint to use suggest command. - ** - * Do not list the ones already installed if installed repository provided. * + * @param InstalledRepository|null $installedRepo If passed in, suggested packages which are installed already will be skipped + * @param PackageInterface|null $onlyDependentsOf If passed in, only the suggestions from direct dependents of that package, or from the package itself, will be shown * @return SuggestedPackagesReporter */ - public function outputMinimalistic(RepositoryInterface $installedRepo = null) + public function outputMinimalistic(InstalledRepository $installedRepo = null, PackageInterface $onlyDependentsOf = null) { - $suggestedPackages = $this->getFilteredSuggestions($installedRepo); + $suggestedPackages = $this->getFilteredSuggestions($installedRepo, $onlyDependentsOf); if ($suggestedPackages) { $this->io->writeError(''.count($suggestedPackages).' package suggestions were added by new dependencies, use `composer suggest` to see details.'); } @@ -171,7 +181,12 @@ class SuggestedPackagesReporter return $this; } - private function getFilteredSuggestions(RepositoryInterface $installedRepo = null) + /** + * @param InstalledRepository|null $installedRepo If passed in, suggested packages which are installed already will be skipped + * @param PackageInterface|null $onlyDependentsOf If passed in, only the suggestions from direct dependents of that package, or from the package itself, will be shown + * @return array[] + */ + private function getFilteredSuggestions(InstalledRepository $installedRepo = null, PackageInterface $onlyDependentsOf = null) { $suggestedPackages = $this->getPackages(); $installedNames = array(); @@ -184,9 +199,17 @@ class SuggestedPackagesReporter } } + $sourceFilter = array(); + if ($onlyDependentsOf) { + $sourceFilter = array_map(function ($link) { + return $link->getTarget(); + }, array_merge($onlyDependentsOf->getRequires(), $onlyDependentsOf->getDevRequires())); + $sourceFilter[] = $onlyDependentsOf->getName(); + } + $suggestions = array(); foreach ($suggestedPackages as $suggestion) { - if (in_array($suggestion['target'], $installedNames)) { + if (in_array($suggestion['target'], $installedNames) || ($sourceFilter && !in_array($suggestion['source'], $sourceFilter))) { continue; } diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index d1d189287..1e30690c1 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -112,11 +112,17 @@ class JsonFile */ public function write(array $hash, $options = 448) { + if ($this->path === 'php://memory') { + file_put_contents($this->path, static::encode($hash, $options)); + + return; + } + $dir = dirname($this->path); if (!is_dir($dir)) { if (file_exists($dir)) { throw new \UnexpectedValueException( - $dir.' exists and is not a directory.' + realpath($dir).' exists and is not a directory.' ); } if (!@mkdir($dir, 0777, true)) { diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php index 6f380d7d2..1f9d5061b 100644 --- a/src/Composer/Package/AliasPackage.php +++ b/src/Composer/Package/AliasPackage.php @@ -25,6 +25,7 @@ class AliasPackage extends BasePackage implements CompletePackageInterface protected $dev; protected $rootPackageAlias = false; protected $stability; + protected $hasSelfVersionRequires = false; /** @var PackageInterface */ protected $aliasOf; @@ -179,7 +180,7 @@ class AliasPackage extends BasePackage implements CompletePackageInterface $prettyVersion = $this->aliasOf->getPrettyVersion(); } - if (in_array($linkType, array('conflicts', 'provides', 'replaces'), true)) { + if (\in_array($linkType, array('conflicts', 'provides', 'replaces'), true)) { $newLinks = array(); foreach ($links as $link) { // link is self.version, but must be replacing also the replaced version @@ -192,6 +193,9 @@ class AliasPackage extends BasePackage implements CompletePackageInterface } else { foreach ($links as $index => $link) { if ('self.version' === $link->getPrettyConstraint()) { + if ($linkType === 'requires') { + $this->hasSelfVersionRequires = true; + } $links[$index] = new Link($link->getSource(), $link->getTarget(), $constraint = new Constraint('=', $this->version), $linkType, $prettyVersion); $constraint->setPrettyString($prettyVersion); } @@ -201,6 +205,11 @@ class AliasPackage extends BasePackage implements CompletePackageInterface return $links; } + public function hasSelfVersionRequires() + { + return $this->hasSelfVersionRequires; + } + /*************************************** * Wrappers around the aliased package * ***************************************/ @@ -395,6 +404,11 @@ class AliasPackage extends BasePackage implements CompletePackageInterface return $this->aliasOf->getNotificationUrl(); } + public function getArchiveName() + { + return $this->aliasOf->getArchiveName(); + } + public function getArchiveExcludes() { return $this->aliasOf->getArchiveExcludes(); diff --git a/src/Composer/Package/Archiver/ArchiveManager.php b/src/Composer/Package/Archiver/ArchiveManager.php index 5e4ec0100..aed8ccf80 100644 --- a/src/Composer/Package/Archiver/ArchiveManager.php +++ b/src/Composer/Package/Archiver/ArchiveManager.php @@ -78,7 +78,12 @@ class ArchiveManager */ public function getPackageFilename(PackageInterface $package) { - $nameParts = array(preg_replace('#[^a-z0-9-_]#i', '-', $package->getName())); + if ($package->getArchiveName()) { + $baseName = $package->getArchiveName(); + } else { + $baseName = preg_replace('#[^a-z0-9-_]#i', '-', $package->getName()); + } + $nameParts = array($baseName); if (preg_match('{^[a-f0-9]{40}$}', $package->getDistReference())) { array_push($nameParts, $package->getDistReference(), $package->getDistType()); @@ -131,20 +136,6 @@ class ArchiveManager } $filesystem = new Filesystem(); - if (null === $fileName) { - $packageName = $this->getPackageFilename($package); - } else { - $packageName = $fileName; - } - - // 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('.'); @@ -167,12 +158,30 @@ class ArchiveManager if (file_exists($composerJsonPath = $sourcePath.'/composer.json')) { $jsonFile = new JsonFile($composerJsonPath); $jsonData = $jsonFile->read(); + if (!empty($jsonData['archive']['name'])) { + $package->setArchiveName($jsonData['archive']['name']); + } if (!empty($jsonData['archive']['exclude'])) { $package->setArchiveExcludes($jsonData['archive']['exclude']); } } } + if (null === $fileName) { + $packageName = $this->getPackageFilename($package); + } else { + $packageName = $fileName; + } + + // Archive filename + $filesystem->ensureDirectoryExists($targetDir); + $target = realpath($targetDir).'/'.$packageName.'.'.$format; + $filesystem->ensureDirectoryExists(dirname($target)); + + if (!$this->overwriteFiles && file_exists($target)) { + return $target; + } + // Create the archive $tempTarget = sys_get_temp_dir().'/composer_archive'.uniqid().'.'.$format; $filesystem->ensureDirectoryExists(dirname($tempTarget)); diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index 480b5ee62..baf3a2292 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -215,7 +215,7 @@ abstract class BasePackage implements PackageInterface public function getFullPrettyVersion($truncate = true, $displayMode = PackageInterface::DISPLAY_SOURCE_REF_IF_DEV) { if ($displayMode === PackageInterface::DISPLAY_SOURCE_REF_IF_DEV && - (!$this->isDev() || !in_array($this->getSourceType(), array('hg', 'git'))) + (!$this->isDev() || !\in_array($this->getSourceType(), array('hg', 'git'))) ) { return $this->getPrettyVersion(); } @@ -233,7 +233,7 @@ abstract class BasePackage implements PackageInterface } // if source reference is a sha1 hash -- truncate - if ($truncate && strlen($reference) === 40) { + if ($truncate && \strlen($reference) === 40) { return $this->getPrettyVersion() . ' ' . substr($reference, 0, 7); } diff --git a/src/Composer/Package/CompletePackage.php b/src/Composer/Package/CompletePackage.php index 785d5817c..85056405b 100644 --- a/src/Composer/Package/CompletePackage.php +++ b/src/Composer/Package/CompletePackage.php @@ -213,6 +213,6 @@ class CompletePackage extends Package implements CompletePackageInterface */ public function getReplacementPackage() { - return is_string($this->abandoned) ? $this->abandoned : null; + return \is_string($this->abandoned) ? $this->abandoned : null; } } diff --git a/src/Composer/Package/Dumper/ArrayDumper.php b/src/Composer/Package/Dumper/ArrayDumper.php index dece598f1..46bcf639a 100644 --- a/src/Composer/Package/Dumper/ArrayDumper.php +++ b/src/Composer/Package/Dumper/ArrayDumper.php @@ -70,6 +70,9 @@ class ArrayDumper } } + if ($package->getArchiveName()) { + $data['archive']['name'] = $package->getArchiveName(); + } if ($package->getArchiveExcludes()) { $data['archive']['exclude'] = $package->getArchiveExcludes(); } @@ -109,7 +112,7 @@ class ArrayDumper $data = $this->dumpValues($package, $keys, $data); - if (isset($data['keywords']) && is_array($data['keywords'])) { + if (isset($data['keywords']) && \is_array($data['keywords'])) { sort($data['keywords']); } @@ -125,7 +128,7 @@ class ArrayDumper } } - if (count($package->getTransportOptions()) > 0) { + if (\count($package->getTransportOptions()) > 0) { $data['transport-options'] = $package->getTransportOptions(); } @@ -142,7 +145,7 @@ class ArrayDumper $getter = 'get'.ucfirst($method); $value = $package->$getter(); - if (null !== $value && !(is_array($value) && 0 === count($value))) { + if (null !== $value && !(\is_array($value) && 0 === \count($value))) { $data[$key] = $value; } } diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index 53270e601..f50358776 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -109,7 +109,7 @@ class ArrayLoader implements LoaderInterface $package->setTargetDir($config['target-dir']); } - if (isset($config['extra']) && is_array($config['extra'])) { + if (isset($config['extra']) && \is_array($config['extra'])) { $package->setExtra($config['extra']); } @@ -159,7 +159,7 @@ class ArrayLoader implements LoaderInterface } } - if (isset($config['suggest']) && is_array($config['suggest'])) { + if (isset($config['suggest']) && \is_array($config['suggest'])) { foreach ($config['suggest'] as $target => $reason) { if ('self.version' === trim($reason)) { $config['suggest'][$target] = $package->getPrettyVersion(); @@ -194,12 +194,15 @@ class ArrayLoader implements LoaderInterface $package->setNotificationUrl($config['notification-url']); } + if (!empty($config['archive']['name'])) { + $package->setArchiveName($config['archive']['name']); + } if (!empty($config['archive']['exclude'])) { $package->setArchiveExcludes($config['archive']['exclude']); } if ($package instanceof Package\CompletePackageInterface) { - if (isset($config['scripts']) && is_array($config['scripts'])) { + if (isset($config['scripts']) && \is_array($config['scripts'])) { foreach ($config['scripts'] as $event => $listeners) { $config['scripts'][$event] = (array) $listeners; } @@ -209,23 +212,23 @@ class ArrayLoader implements LoaderInterface $package->setScripts($config['scripts']); } - if (!empty($config['description']) && is_string($config['description'])) { + if (!empty($config['description']) && \is_string($config['description'])) { $package->setDescription($config['description']); } - if (!empty($config['homepage']) && is_string($config['homepage'])) { + if (!empty($config['homepage']) && \is_string($config['homepage'])) { $package->setHomepage($config['homepage']); } - if (!empty($config['keywords']) && is_array($config['keywords'])) { + if (!empty($config['keywords']) && \is_array($config['keywords'])) { $package->setKeywords($config['keywords']); } if (!empty($config['license'])) { - $package->setLicense(is_array($config['license']) ? $config['license'] : array($config['license'])); + $package->setLicense(\is_array($config['license']) ? $config['license'] : array($config['license'])); } - if (!empty($config['authors']) && is_array($config['authors'])) { + if (!empty($config['authors']) && \is_array($config['authors'])) { $package->setAuthors($config['authors']); } @@ -233,7 +236,7 @@ class ArrayLoader implements LoaderInterface $package->setSupport($config['support']); } - if (!empty($config['funding']) && is_array($config['funding'])) { + if (!empty($config['funding']) && \is_array($config['funding'])) { $package->setFunding($config['funding']); } @@ -307,8 +310,8 @@ class ArrayLoader implements LoaderInterface private function createLink($source, $sourceVersion, $description, $target, $prettyConstraint) { - if (!is_string($prettyConstraint)) { - throw new \UnexpectedValueException('Link constraint in '.$source.' '.$description.' > '.$target.' should be a string, got '.gettype($prettyConstraint) . ' (' . var_export($prettyConstraint, true) . ')'); + if (!\is_string($prettyConstraint)) { + throw new \UnexpectedValueException('Link constraint in '.$source.' '.$description.' > '.$target.' should be a string, got '.\gettype($prettyConstraint) . ' (' . var_export($prettyConstraint, true) . ')'); } if ('self.version' === $prettyConstraint) { $parsedConstraint = $this->versionParser->parseConstraints($sourceVersion); @@ -331,7 +334,7 @@ class ArrayLoader implements LoaderInterface return; } - if (isset($config['extra']['branch-alias']) && is_array($config['extra']['branch-alias'])) { + if (isset($config['extra']['branch-alias']) && \is_array($config['extra']['branch-alias'])) { foreach ($config['extra']['branch-alias'] as $sourceBranch => $targetBranch) { // ensure it is an alias to a -dev package if ('-dev' !== substr($targetBranch, -4)) { @@ -361,7 +364,7 @@ class ArrayLoader implements LoaderInterface } } - if (in_array($config['version'], array('dev-master', 'dev-default', 'dev-trunk'), true)) { + if (\in_array($config['version'], array('dev-master', 'dev-default', 'dev-trunk'), true)) { return VersionParser::DEV_MASTER_ALIAS; } } diff --git a/src/Composer/Package/Package.php b/src/Composer/Package/Package.php index 6fdba9b42..5e393a11c 100644 --- a/src/Composer/Package/Package.php +++ b/src/Composer/Package/Package.php @@ -57,6 +57,7 @@ class Package extends BasePackage protected $autoload = array(); protected $devAutoload = array(); protected $includePaths = array(); + protected $archiveName; protected $archiveExcludes = array(); /** @@ -551,6 +552,24 @@ class Package extends BasePackage return $this->notificationUrl; } + /** + * Sets default base filename for archive + * + * @param string $name + */ + public function setArchiveName($name) + { + $this->archiveName = $name; + } + + /** + * {@inheritDoc} + */ + public function getArchiveName() + { + return $this->archiveName; + } + /** * Sets a list of patterns to be excluded from archives * @@ -619,7 +638,7 @@ class Package extends BasePackage } else { continue; } - if (!in_array($mirrorUrl, $urls)) { + if (!\in_array($mirrorUrl, $urls)) { $func = $mirror['preferred'] ? 'array_unshift' : 'array_push'; $func($urls, $mirrorUrl); } diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index 18f72960f..1c2ad2e06 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -283,7 +283,7 @@ interface PackageInterface * directories for autoloading using the type specified. * * @return array Mapping of autoloading rules - * @psalm-return array{psr-0?: array, psr-4?: array, classmap?: list, file?: list} + * @psalm-return array{psr-0?: array, psr-4?: array, classmap?: list, files?: list} */ public function getAutoload(); @@ -296,7 +296,7 @@ interface PackageInterface * directories for autoloading using the type specified. * * @return array Mapping of dev autoloading rules - * @psalm-return array{psr-0?: array, psr-4?: array, classmap?: list, file?: list} + * @psalm-return array{psr-0?: array, psr-4?: array, classmap?: list, files?: list} */ public function getDevAutoload(); @@ -357,6 +357,13 @@ interface PackageInterface */ public function getPrettyString(); + /** + * Returns default base filename for archive + * + * @return array + */ + public function getArchiveName(); + /** * Returns a list of patterns to exclude from package archives * diff --git a/src/Composer/Package/Version/VersionSelector.php b/src/Composer/Package/Version/VersionSelector.php index a8b4ae17b..d42c9f167 100644 --- a/src/Composer/Package/Version/VersionSelector.php +++ b/src/Composer/Package/Version/VersionSelector.php @@ -15,9 +15,12 @@ namespace Composer\Package\Version; use Composer\DependencyResolver\Pool; use Composer\Package\BasePackage; use Composer\Package\PackageInterface; +use Composer\Plugin\PluginInterface; +use Composer\Composer; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; use Composer\Repository\RepositorySet; +use Composer\Repository\PlatformRepository; use Composer\Semver\Constraint\Constraint; /** @@ -30,11 +33,21 @@ class VersionSelector { private $repositorySet; + private $platformConstraints = array(); + private $parser; - public function __construct(RepositorySet $repositorySet) + /** + * @param PlatformRepository $platformRepo If passed in, the versions found will be filtered against their requirements to eliminate any not matching the current platform packages + */ + public function __construct(RepositorySet $repositorySet, PlatformRepository $platformRepo = null) { $this->repositorySet = $repositorySet; + if ($platformRepo) { + foreach ($platformRepo->getPackages() as $package) { + $this->platformConstraints[$package->getName()][] = new Constraint('==', $package->getVersion()); + } + } } /** @@ -43,21 +56,39 @@ class VersionSelector * * @param string $packageName * @param string $targetPackageVersion - * @param string $targetPhpVersion * @param string $preferredStability + * @param bool|array $ignorePlatformReqs * @return PackageInterface|false */ - public function findBestCandidate($packageName, $targetPackageVersion = null, $targetPhpVersion = null, $preferredStability = 'stable') + public function findBestCandidate($packageName, $targetPackageVersion = null, $preferredStability = 'stable', $ignorePlatformReqs = false) { + if (!isset(BasePackage::$stabilities[$preferredStability])) { + // If you get this, maybe you are still relying on the Composer 1.x signature where the 3rd arg was the php version + throw new \UnexpectedValueException('Expected a valid stability name as 3rd argument, got '.$preferredStability); + } + $constraint = $targetPackageVersion ? $this->getParser()->parseConstraints($targetPackageVersion) : null; $candidates = $this->repositorySet->findPackages(strtolower($packageName), $constraint); - if ($targetPhpVersion) { - $phpConstraint = new Constraint('==', $this->getParser()->normalize($targetPhpVersion)); - $candidates = array_filter($candidates, function ($pkg) use ($phpConstraint) { + if ($this->platformConstraints && true !== $ignorePlatformReqs) { + $platformConstraints = $this->platformConstraints; + $ignorePlatformReqs = $ignorePlatformReqs ?: array(); + $candidates = array_filter($candidates, function ($pkg) use ($platformConstraints, $ignorePlatformReqs) { $reqs = $pkg->getRequires(); - return !isset($reqs['php']) || $reqs['php']->getConstraint()->matches($phpConstraint); + foreach ($reqs as $name => $link) { + if (!in_array($name, $ignorePlatformReqs, true) && isset($platformConstraints[$name])) { + foreach ($platformConstraints[$name] as $constraint) { + if ($link->getConstraint()->matches($constraint)) { + continue 2; + } + } + + return false; + } + } + + return true; }); } diff --git a/src/Composer/Plugin/PluginInterface.php b/src/Composer/Plugin/PluginInterface.php index 4390764ff..f18799e7b 100644 --- a/src/Composer/Plugin/PluginInterface.php +++ b/src/Composer/Plugin/PluginInterface.php @@ -25,6 +25,11 @@ interface PluginInterface /** * Version number of the internal composer-plugin-api package * + * This is used to denote the API version of Plugin specific + * features, but is also bumped to a new major if Composer + * includes a major break in internal APIs which are susceptible + * to be used by plugins. + * * @var string */ const PLUGIN_API_VERSION = '2.0.0'; diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 97757f4fa..8a7ca86c8 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -134,8 +134,8 @@ class PluginManager $currentPluginApiVersion = $this->getPluginApiVersion(); $currentPluginApiConstraint = new Constraint('==', $this->versionParser->normalize($currentPluginApiVersion)); - if ($requiresComposer->getPrettyString() === '1.0.0' && $this->getPluginApiVersion() === '1.0.0') { - $this->io->writeError('The "' . $package->getName() . '" plugin requires composer-plugin-api 1.0.0, this *WILL* break in the future and it should be fixed ASAP (require ^1.0 for example).'); + if ($requiresComposer->getPrettyString() === $this->getPluginApiVersion()) { + $this->io->writeError('The "' . $package->getName() . '" plugin requires composer-plugin-api '.$this->getPluginApiVersion().', this *WILL* break in the future and it should be fixed ASAP (require ^'.$this->getPluginApiVersion().' instead for example).'); } elseif (!$requiresComposer->matches($currentPluginApiConstraint)) { $this->io->writeError('The "' . $package->getName() . '" plugin was skipped because it requires a Plugin API version ("' . $requiresComposer->getPrettyString() . '") that does not match your Composer installation ("' . $currentPluginApiVersion . '"). You may need to run composer update with the "--no-plugins" option.'); @@ -347,6 +347,8 @@ class PluginManager /** * Load all plugins and installers from a repository * + * If a plugin requires another plugin, the required one will be loaded first + * * 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. @@ -358,7 +360,7 @@ class PluginManager private function loadRepository(RepositoryInterface $repo) { $packages = $repo->getPackages(); - $sortedPackages = array_reverse(PackageSorter::sortPackages($packages)); + $sortedPackages = PackageSorter::sortPackages($packages); foreach ($sortedPackages as $package) { if (!($package instanceof CompletePackage)) { continue; diff --git a/src/Composer/Plugin/PostFileDownloadEvent.php b/src/Composer/Plugin/PostFileDownloadEvent.php index 081e64507..a4bc2ac4a 100644 --- a/src/Composer/Plugin/PostFileDownloadEvent.php +++ b/src/Composer/Plugin/PostFileDownloadEvent.php @@ -13,7 +13,7 @@ namespace Composer\Plugin; use Composer\EventDispatcher\Event; -use Composer\Util\RemoteFilesystem; +use Composer\Package\PackageInterface; /** * The post file download event. @@ -38,6 +38,11 @@ class PostFileDownloadEvent extends Event */ private $url; + /** + * @var \Composer\Package\PackageInterface + */ + private $package; + /** * Constructor. * @@ -45,13 +50,15 @@ class PostFileDownloadEvent extends Event * @param string $fileName The file name * @param string|null $checksum The checksum * @param string $url The processed url + * @param PackageInterface $package The package. */ - public function __construct($name, $fileName, $checksum, $url) + public function __construct($name, $fileName, $checksum, $url, PackageInterface $package) { parent::__construct($name); $this->fileName = $fileName; $this->checksum = $checksum; $this->url = $url; + $this->package = $package; } /** @@ -82,4 +89,14 @@ class PostFileDownloadEvent extends Event return $this->url; } + /** + * Get the package. + * + * @return \Composer\Package\PackageInterface + * The package. + */ + public function getPackage() { + return $this->package; + } + } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 6dbe1cf38..e960f63be 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -31,7 +31,7 @@ use Composer\EventDispatcher\EventDispatcher; use Composer\Downloader\TransportException; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; -use Composer\Semver\Constraint\EmptyConstraint; +use Composer\Semver\Constraint\MatchAllConstraint; use Composer\Util\Http\Response; use Composer\Util\MetadataMinifier; use Composer\Util\Url; @@ -70,6 +70,14 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito private $hasPartialPackages; private $partialPackagesByName; + /** + * TODO v3 should make this private once we can drop PHP 5.3 support + * @private + * @var array list of package names which are fresh and can be loaded from the cache directly in case loadPackage is called several times + * useful for v2 metadata repositories with lazy providers + */ + public $freshMetadataUrls = array(); + /** * TODO v3 should make this private once we can drop PHP 5.3 support * @private @@ -257,7 +265,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if (is_array($this->availablePackages)) { $packageMap = array(); foreach ($this->availablePackages as $name) { - $packageMap[$name] = new EmptyConstraint(); + $packageMap[$name] = new MatchAllConstraint(); } $result = $this->loadAsyncPackages($packageMap); @@ -363,9 +371,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($this->lazyProvidersUrl && count($packageNameMap)) { if (is_array($this->availablePackages)) { $availPackages = $this->availablePackages; - $packageNameMap = array_filter($packageNameMap, function ($name) use ($availPackages) { - return isset($availPackages[strtolower($name)]); - }, ARRAY_FILTER_USE_KEY); + foreach ($packageNameMap as $name => $constraint) { + if (!isset($availPackages[strtolower($name)])) { + unset($packageNameMap[$name]); + } + } } $result = $this->loadAsyncPackages($packageNameMap, $acceptableStabilities, $stabilityFlags); @@ -502,7 +512,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito { if (!$this->hasPartialPackages() || !isset($this->partialPackagesByName[$name])) { // skip platform packages, root package and composer-plugin-api - if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name) || '__root__' === $name || 'composer-plugin-api' === $name) { + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name) || '__root__' === $name) { return array(); } @@ -662,7 +672,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito // load ~dev versions of the packages as well if needed foreach ($packageNames as $name => $constraint) { - if ($acceptableStabilities && $stabilityFlags && StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, array($name), 'dev')) { + if ($acceptableStabilities === null || $stabilityFlags === null || StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, array($name), 'dev')) { $packageNames[$name.'~dev'] = $constraint; } } @@ -672,7 +682,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $realName = preg_replace('{~dev$}', '', $name); // skip platform packages, root package and composer-plugin-api - if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $realName) || '__root__' === $realName || 'composer-plugin-api' === $realName) { + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $realName) || '__root__' === $realName) { continue; } @@ -818,7 +828,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->hasPartialPackages = !empty($data['packages']) && is_array($data['packages']); } - // metadata-url indiates V2 repo protocol so it takes over from all the V1 types + // metadata-url indicates V2 repo protocol so it takes over from all the V1 types // V2 only has lazyProviders and possibly partial packages, but no ability to process anything else, // V2 also supports async loading if (!empty($data['metadata-url'])) { @@ -995,7 +1005,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito } // url-encode $ signs in URLs as bad proxies choke on them - if (($pos = strpos($filename, '$')) && preg_match('{^https?://.*}i', $filename)) { + if (($pos = strpos($filename, '$')) && preg_match('{^https?://}i', $filename)) { $filename = substr($filename, 0, $pos) . '%24' . substr($filename, $pos + 1); } @@ -1148,6 +1158,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return new Promise(function ($resolve, $reject) { $resolve(array('packages' => array())); }); } + if (isset($this->freshMetadataUrls[$filename]) && $lastModifiedTime) { + // make it look like we got a 304 response + return new Promise(function ($resolve, $reject) { $resolve(true); }); + } + $httpDownloader = $this->httpDownloader; if ($this->eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); @@ -1171,6 +1186,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $json = $response->getBody(); if ($json === '' && $response->getStatusCode() === 304) { + $repo->freshMetadataUrls[$filename] = true; return true; } @@ -1184,6 +1200,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $json = JsonFile::encode($data, JsonFile::JSON_UNESCAPED_SLASHES | JsonFile::JSON_UNESCAPED_UNICODE); } $cache->write($cacheKey, $json); + $repo->freshMetadataUrls[$filename] = true; return $data; }; diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index 3bebfbdb3..fe19b420b 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -14,6 +14,8 @@ namespace Composer\Repository; use Composer\Json\JsonFile; use Composer\Package\Loader\ArrayLoader; +use Composer\Package\RootPackageInterface; +use Composer\Package\AliasPackage; use Composer\Package\Dumper\ArrayDumper; use Composer\Installer\InstallationManager; use Composer\Util\Filesystem; @@ -26,17 +28,26 @@ use Composer\Util\Filesystem; */ class FilesystemRepository extends WritableArrayRepository { - private $file; + protected $file; + private $dumpVersions; + private $rootPackage; /** * Initializes filesystem repository. * * @param JsonFile $repositoryFile repository json file + * @param bool $dumpVersions + * @param ?RootPackageInterface $rootPackage Must be provided if $dumpVersions is true */ - public function __construct(JsonFile $repositoryFile) + public function __construct(JsonFile $repositoryFile, $dumpVersions = false, RootPackageInterface $rootPackage = null) { parent::__construct(); $this->file = $repositoryFile; + $this->dumpVersions = $dumpVersions; + $this->rootPackage = $rootPackage; + if ($dumpVersions && !$rootPackage) { + throw new \InvalidArgumentException('Expected a root package instance if $dumpVersions is true'); + } } /** @@ -105,5 +116,90 @@ class FilesystemRepository extends WritableArrayRepository }); $this->file->write($data); + + if ($this->dumpVersions) { + $versions = array('versions' => array()); + $packages = $this->getPackages(); + $packages[] = $rootPackage = $this->rootPackage; + while ($rootPackage instanceof AliasPackage) { + $rootPackage = $rootPackage->getAliasOf(); + $packages[] = $rootPackage; + } + + // add real installed packages + foreach ($packages as $package) { + if ($package instanceof AliasPackage) { + continue; + } + + $reference = null; + if ($package->getInstallationSource()) { + $reference = $package->getInstallationSource() === 'source' ? $package->getSourceReference() : $package->getDistReference(); + } + if (null === $reference) { + $reference = ($package->getSourceReference() ?: $package->getDistReference()) ?: null; + } + + $versions['versions'][$package->getName()] = array( + 'pretty_version' => $package->getPrettyVersion(), + 'version' => $package->getVersion(), + 'aliases' => array(), + 'reference' => $reference, + ); + if ($package instanceof RootPackageInterface) { + $versions['root'] = $versions['versions'][$package->getName()]; + $versions['root']['name'] = $package->getName(); + } + } + + // add provided/replaced packages + foreach ($packages as $package) { + foreach ($package->getReplaces() as $replace) { + // exclude platform replaces as when they are really there we can not check for their presence + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $replace->getTarget())) { + continue; + } + $replaced = $replace->getPrettyConstraint(); + if ($replaced === 'self.version') { + $replaced = $package->getPrettyVersion(); + } + if (!isset($versions['versions'][$replace->getTarget()]['replaced']) || !in_array($replaced, $versions['versions'][$replace->getTarget()]['replaced'], true)) { + $versions['versions'][$replace->getTarget()]['replaced'][] = $replaced; + } + } + foreach ($package->getProvides() as $provide) { + // exclude platform provides as when they are really there we can not check for their presence + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $provide->getTarget())) { + continue; + } + $provided = $provide->getPrettyConstraint(); + if ($provided === 'self.version') { + $provided = $package->getPrettyVersion(); + } + if (!isset($versions['versions'][$provide->getTarget()]['provided']) || !in_array($provided, $versions['versions'][$provide->getTarget()]['provided'], true)) { + $versions['versions'][$provide->getTarget()]['provided'][] = $provided; + } + } + } + + // add aliases + foreach ($packages as $package) { + if (!$package instanceof AliasPackage) { + continue; + } + $versions['versions'][$package->getName()]['aliases'][] = $package->getPrettyVersion(); + if ($package instanceof RootPackageInterface) { + $versions['root']['aliases'][] = $package->getPrettyVersion(); + } + } + + ksort($versions['versions']); + ksort($versions); + + $fs->filePutContentsIfModified($repoDir.'/installed.php', 'filePutContentsIfModified($repoDir.'/InstalledVersions.php', $installedVersionsClass); + } } } diff --git a/src/Composer/Repository/InstalledArrayRepository.php b/src/Composer/Repository/InstalledArrayRepository.php index de1dd67d8..e8a5f8194 100644 --- a/src/Composer/Repository/InstalledArrayRepository.php +++ b/src/Composer/Repository/InstalledArrayRepository.php @@ -25,4 +25,14 @@ class InstalledArrayRepository extends WritableArrayRepository implements Instal { return 'installed '.parent::getRepoName(); } + + /** + * {@inheritDoc} + */ + public function isFresh() + { + // this is not a completely correct implementation but there is no way to + // distinguish an empty repo and a newly created one given this is all in-memory + return $this->count() === 0; + } } diff --git a/src/Composer/Repository/InstalledFilesystemRepository.php b/src/Composer/Repository/InstalledFilesystemRepository.php index bf81734d4..f526f31c2 100644 --- a/src/Composer/Repository/InstalledFilesystemRepository.php +++ b/src/Composer/Repository/InstalledFilesystemRepository.php @@ -23,4 +23,12 @@ class InstalledFilesystemRepository extends FilesystemRepository implements Inst { return 'installed '.parent::getRepoName(); } + + /** + * {@inheritDoc} + */ + public function isFresh() + { + return !$this->file->exists(); + } } diff --git a/src/Composer/Repository/InstalledRepositoryInterface.php b/src/Composer/Repository/InstalledRepositoryInterface.php index 19b095b2a..b5d8a264e 100644 --- a/src/Composer/Repository/InstalledRepositoryInterface.php +++ b/src/Composer/Repository/InstalledRepositoryInterface.php @@ -21,4 +21,8 @@ namespace Composer\Repository; */ interface InstalledRepositoryInterface extends WritableRepositoryInterface { + /** + * @return bool true if packages were never installed in this repository + */ + public function isFresh(); } diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 84f3d4b66..e003976a1 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -12,6 +12,7 @@ namespace Composer\Repository; +use Composer\Composer; use Composer\Package\CompletePackage; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; @@ -27,7 +28,7 @@ use Symfony\Component\Process\ExecutableFinder; */ class PlatformRepository extends ArrayRepository { - const PLATFORM_PACKAGE_REGEX = '{^(?:php(?:-64bit|-ipv6|-zts|-debug)?|hhvm|(?:ext|lib)-[a-z0-9](?:[_.-]?[a-z0-9]+)*|composer-plugin-api)$}iD'; + const PLATFORM_PACKAGE_REGEX = '{^(?:php(?:-64bit|-ipv6|-zts|-debug)?|hhvm|(?:ext|lib)-[a-z0-9](?:[_.-]?[a-z0-9]+)*|composer-(?:plugin|runtime)-api)$}iD'; private $versionParser; @@ -79,6 +80,12 @@ class PlatformRepository extends ArrayRepository $composerPluginApi->setDescription('The Composer Plugin API'); $this->addPackage($composerPluginApi); + $prettyVersion = Composer::RUNTIME_API_VERSION; + $version = $this->versionParser->normalize($prettyVersion); + $composerRuntimeApi = new CompletePackage('composer-runtime-api', $version, $prettyVersion); + $composerRuntimeApi->setDescription('The Composer Runtime API'); + $this->addPackage($composerRuntimeApi); + try { $prettyVersion = PHP_VERSION; $version = $this->versionParser->normalize($prettyVersion); diff --git a/src/Composer/Repository/RepositoryManager.php b/src/Composer/Repository/RepositoryManager.php index c5da49cdb..264115c10 100644 --- a/src/Composer/Repository/RepositoryManager.php +++ b/src/Composer/Repository/RepositoryManager.php @@ -163,9 +163,9 @@ class RepositoryManager /** * Sets local repository for the project. * - * @param WritableRepositoryInterface $repository repository instance + * @param InstalledRepositoryInterface $repository repository instance */ - public function setLocalRepository(WritableRepositoryInterface $repository) + public function setLocalRepository(InstalledRepositoryInterface $repository) { $this->localRepository = $repository; } @@ -173,7 +173,7 @@ class RepositoryManager /** * Returns local repository for the project. * - * @return WritableRepositoryInterface + * @return InstalledRepositoryInterface */ public function getLocalRepository() { diff --git a/src/Composer/Util/ConfigValidator.php b/src/Composer/Util/ConfigValidator.php index 98a6016f2..aae5879b3 100644 --- a/src/Composer/Util/ConfigValidator.php +++ b/src/Composer/Util/ConfigValidator.php @@ -28,6 +28,8 @@ use Composer\Spdx\SpdxLicenses; */ class ConfigValidator { + const CHECK_VERSION = 1; + private $io; public function __construct(IOInterface $io) @@ -40,10 +42,11 @@ class ConfigValidator * * @param string $file The path to the file * @param int $arrayLoaderValidationFlags Flags for ArrayLoader validation + * @param int $flags Flags for validation * * @return array a triple containing the errors, publishable errors, and warnings */ - public function validate($file, $arrayLoaderValidationFlags = ValidatingArrayLoader::CHECK_ALL) + public function validate($file, $arrayLoaderValidationFlags = ValidatingArrayLoader::CHECK_ALL, $flags = self::CHECK_VERSION) { $errors = array(); $publishErrors = array(); @@ -109,7 +112,7 @@ class ConfigValidator } } - if (isset($manifest['version'])) { + if (($flags & self::CHECK_VERSION) && isset($manifest['version'])) { $warnings[] = 'The version field is present, it is recommended to leave it out if the package is published on Packagist.'; } diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 8df7de8b5..fd7ded57e 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -58,7 +58,7 @@ class Filesystem ->depth(0) ->in($dir); - return count($finder) === 0; + return \count($finder) === 0; } public function emptyDirectory($dir, $ensureDirectoryExists = true) @@ -116,7 +116,7 @@ class Filesystem 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')) { + if (!\function_exists('proc_open')) { return $this->removeDirectoryPhp($directory); } @@ -311,9 +311,9 @@ class Filesystem return; } - if (!function_exists('proc_open')) { + if (!\function_exists('proc_open')) { $this->copyThenRemove($source, $target); - + return; } @@ -369,13 +369,13 @@ class Filesystem $from = rtrim($from, '/') . '/dummy_file'; } - if (dirname($from) === dirname($to)) { + if (\dirname($from) === \dirname($to)) { return './'.basename($to); } $commonPath = $to; while (strpos($from.'/', $commonPath.'/') !== 0 && '/' !== $commonPath && !preg_match('{^[a-z]:/?$}i', $commonPath)) { - $commonPath = strtr(dirname($commonPath), '\\', '/'); + $commonPath = strtr(\dirname($commonPath), '\\', '/'); } if (0 !== strpos($from, $commonPath) || '/' === $commonPath) { @@ -383,10 +383,10 @@ class Filesystem } $commonPath = rtrim($commonPath, '/') . '/'; - $sourcePathDepth = substr_count(substr($from, strlen($commonPath)), '/'); + $sourcePathDepth = substr_count(substr($from, \strlen($commonPath)), '/'); $commonPathCode = str_repeat('../', $sourcePathDepth); - return ($commonPathCode . substr($to, strlen($commonPath))) ?: './'; + return ($commonPathCode . substr($to, \strlen($commonPath))) ?: './'; } /** @@ -414,7 +414,7 @@ class Filesystem $commonPath = $to; while (strpos($from.'/', $commonPath.'/') !== 0 && '/' !== $commonPath && !preg_match('{^[a-z]:/?$}i', $commonPath) && '.' !== $commonPath) { - $commonPath = strtr(dirname($commonPath), '\\', '/'); + $commonPath = strtr(\dirname($commonPath), '\\', '/'); } if (0 !== strpos($from, $commonPath) || '/' === $commonPath || '.' === $commonPath) { @@ -423,17 +423,17 @@ class Filesystem $commonPath = rtrim($commonPath, '/') . '/'; if (strpos($to, $from.'/') === 0) { - return '__DIR__ . '.var_export(substr($to, strlen($from)), true); + return '__DIR__ . '.var_export(substr($to, \strlen($from)), true); } - $sourcePathDepth = substr_count(substr($from, strlen($commonPath)), '/') + $directories; + $sourcePathDepth = substr_count(substr($from, \strlen($commonPath)), '/') + $directories; if ($staticCode) { $commonPathCode = "__DIR__ . '".str_repeat('/..', $sourcePathDepth)."'"; } else { $commonPathCode = str_repeat('dirname(', $sourcePathDepth).'__DIR__'.str_repeat(')', $sourcePathDepth); } - $relTarget = substr($to, strlen($commonPath)); + $relTarget = substr($to, \strlen($commonPath)); - return $commonPathCode . (strlen($relTarget) ? '.' . var_export('/' . $relTarget, true) : ''); + return $commonPathCode . (\strlen($relTarget) ? '.' . var_export('/' . $relTarget, true) : ''); } /** @@ -484,7 +484,7 @@ class Filesystem // extract a prefix being a protocol://, protocol:, protocol://drive: or simply drive: if (preg_match('{^( [0-9a-z]{2,}+: (?: // (?: [a-z]: )? )? | [a-z]: )}ix', $path, $match)) { $prefix = $match[1]; - $path = substr($path, strlen($prefix)); + $path = substr($path, \strlen($prefix)); } if (substr($path, 0, 1) === '/') { @@ -579,7 +579,7 @@ class Filesystem $cwd = getcwd(); $relativePath = $this->findShortestPath($link, $target); - chdir(dirname($link)); + chdir(\dirname($link)); $result = @symlink($relativePath, $link); chdir($cwd); @@ -632,7 +632,7 @@ class Filesystem $resolved = rtrim($pathname, '/'); - if (!strlen($resolved)) { + if (!\strlen($resolved)) { return $pathname; } @@ -721,4 +721,61 @@ class Filesystem return $this->rmdir($junction); } + + public function filePutContentsIfModified($path, $content) + { + $currentContent = @file_get_contents($path); + if (!$currentContent || ($currentContent != $content)) { + return file_put_contents($path, $content); + } + + return 0; + } + + /** + * Copy file using stream_copy_to_stream to work around https://bugs.php.net/bug.php?id=6463 + * + * @param string $source + * @param string $target + */ + public function safeCopy($source, $target) + { + if (!file_exists($target) || !file_exists($source) || !$this->filesAreEqual($source, $target)) { + $source = fopen($source, 'r'); + $target = fopen($target, 'w+'); + + stream_copy_to_stream($source, $target); + fclose($source); + fclose($target); + } + } + + /** + * compare 2 files + * https://stackoverflow.com/questions/3060125/can-i-use-file-get-contents-to-compare-two-files + */ + private function filesAreEqual($a, $b) + { + // Check if filesize is different + if (filesize($a) !== filesize($b)) { + return false; + } + + // Check if content is different + $ah = fopen($a, 'rb'); + $bh = fopen($b, 'rb'); + + $result = true; + while (!feof($ah)) { + if (fread($ah, 8192) != fread($bh, 8192)) { + $result = false; + break; + } + } + + fclose($ah); + fclose($bh); + + return $result; + } } diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index 72524b4f9..a4b5a7336 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -165,7 +165,7 @@ class Git $errorMsg = $this->process->getErrorOutput(); } } elseif ( - preg_match('{^(git)@' . self::getGitLabDomainsRegex($this->config) . ':(.+?)\.git$}i', $url, $match) + preg_match('{^(git)@' . self::getGitLabDomainsRegex($this->config) . ':(.+?\.git)$}i', $url, $match) || preg_match('{^(https?)://' . self::getGitLabDomainsRegex($this->config) . '/(.*)}', $url, $match) ) { if ($match[1] === 'git') { diff --git a/src/Composer/Util/GitLab.php b/src/Composer/Util/GitLab.php index fb2489b01..ea0c72477 100644 --- a/src/Composer/Util/GitLab.php +++ b/src/Composer/Util/GitLab.php @@ -71,17 +71,28 @@ class GitLab return true; } - // if available use token from composer config - $authTokens = $this->config->get('gitlab-token'); - - if (isset($authTokens[$originUrl])) { - $this->io->setAuthentication($originUrl, $authTokens[$originUrl], 'private-token'); + // if available use deploy token from git config + if (0 === $this->process->execute('git config gitlab.deploytoken.user', $tokenUser) && 0 === $this->process->execute('git config gitlab.deploytoken.token', $tokenPassword)) { + $this->io->setAuthentication($originUrl, trim($tokenUser), trim($tokenPassword)); return true; } + // if available use token from composer config + $authTokens = $this->config->get('gitlab-token'); + + if (isset($authTokens[$originUrl])) { + $token = $authTokens[$originUrl]; + } + if (isset($authTokens[$bcOriginUrl])) { - $this->io->setAuthentication($originUrl, $authTokens[$bcOriginUrl], 'private-token'); + $token = $authTokens[$bcOriginUrl]; + } + + if(isset($token)){ + $username = is_array($token) && array_key_exists("username", $token) ? $token["username"] : $token; + $password = is_array($token) && array_key_exists("token", $token) ? $token["token"] : 'private-token'; + $this->io->setAuthentication($originUrl, $username, $password); return true; } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 017b2d1a2..365caf899 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -75,7 +75,7 @@ class CurlDownloader $this->multiHandle = $mh = curl_multi_init(); if (function_exists('curl_multi_setopt')) { curl_multi_setopt($mh, CURLMOPT_PIPELINING, PHP_VERSION_ID >= 70400 ? /* CURLPIPE_MULTIPLEX */ 2 : /*CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX*/ 3); - if (defined('CURLMOPT_MAX_HOST_CONNECTIONS')) { + if (defined('CURLMOPT_MAX_HOST_CONNECTIONS') && !defined('HHVM_VERSION')) { curl_multi_setopt($mh, CURLMOPT_MAX_HOST_CONNECTIONS, 8); } } diff --git a/src/Composer/Util/PackageSorter.php b/src/Composer/Util/PackageSorter.php index 8d8c9a06c..204a35b51 100644 --- a/src/Composer/Util/PackageSorter.php +++ b/src/Composer/Util/PackageSorter.php @@ -55,9 +55,9 @@ class PackageSorter $weightList = array(); - foreach ($packages as $name => $package) { - $weight = $computeImportance($name); - $weightList[$name] = $weight; + foreach ($packages as $index => $package) { + $weight = $computeImportance($package->getName()); + $weightList[$index] = $weight; } $stable_sort = function (&$array) { @@ -84,8 +84,8 @@ class PackageSorter $sortedPackages = array(); - foreach (array_keys($weightList) as $name) { - $sortedPackages[] = $packages[$name]; + foreach (array_keys($weightList) as $index) { + $sortedPackages[] = $packages[$index]; } return $sortedPackages; } diff --git a/src/Composer/Util/Platform.php b/src/Composer/Util/Platform.php index 60bf9efa9..4dc9af07b 100644 --- a/src/Composer/Util/Platform.php +++ b/src/Composer/Util/Platform.php @@ -55,7 +55,7 @@ class Platform return $home; } - if (function_exists('posix_getuid') && function_exists('posix_getpwuid')) { + if (\function_exists('posix_getuid') && \function_exists('posix_getpwuid')) { $info = posix_getpwuid(posix_getuid()); return $info['dir']; @@ -69,7 +69,7 @@ class Platform */ public static function isWindows() { - return defined('PHP_WINDOWS_VERSION_BUILD'); + return \defined('PHP_WINDOWS_VERSION_BUILD'); } /** @@ -80,13 +80,13 @@ class Platform { static $useMbString = null; if (null === $useMbString) { - $useMbString = function_exists('mb_strlen') && ini_get('mbstring.func_overload'); + $useMbString = \function_exists('mb_strlen') && ini_get('mbstring.func_overload'); } if ($useMbString) { return mb_strlen($str, '8bit'); } - return strlen($str); + return \strlen($str); } } diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index 6cb9b76dc..c178e93d8 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -14,6 +14,7 @@ namespace Composer\Test\Autoload; use Composer\Autoload\AutoloadGenerator; use Composer\Package\Link; +use Composer\Package\Version\VersionParser; use Composer\Semver\Constraint\Constraint; use Composer\Util\Filesystem; use Composer\Package\AliasPackage; @@ -100,6 +101,9 @@ class AutoloadGeneratorTest extends TestCase 'vendor-dir' => function () use ($that) { return $that->vendorDir; }, + 'platform-check' => function () { + return true; + }, ); $this->config->expects($this->atLeastOnce()) @@ -448,6 +452,7 @@ class AutoloadGeneratorTest extends TestCase $this->assertEquals( array( 'B\\C\\C' => $this->vendorDir.'/b/b/src/C/C.php', + 'Composer\\InstalledVersions' => $this->vendorDir . '/composer/InstalledVersions.php', ), include $this->vendorDir.'/composer/autoload_classmap.php' ); @@ -595,7 +600,9 @@ class AutoloadGeneratorTest extends TestCase $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_8'); $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); $this->assertEquals( - array(), + array( + 'Composer\\InstalledVersions' => $this->vendorDir.'/composer/InstalledVersions.php', + ), include $this->vendorDir.'/composer/autoload_classmap.php' ); } @@ -632,6 +639,7 @@ class AutoloadGeneratorTest extends TestCase \$baseDir = dirname(\$vendorDir); return array( + 'Composer\\\\InstalledVersions' => \$vendorDir . '/composer/InstalledVersions.php', 'psr0_match' => \$baseDir . '/psr0/psr0/match.php', 'psr4\\\\match' => \$baseDir . '/psr4/match.php', ); @@ -673,6 +681,7 @@ EOF; 'ClassMapBar' => $this->vendorDir.'/b/b/src/b.php', 'ClassMapBaz' => $this->vendorDir.'/b/b/lib/c.php', 'ClassMapFoo' => $this->vendorDir.'/a/a/src/a.php', + 'Composer\\InstalledVersions' => $this->vendorDir.'/composer/InstalledVersions.php', ), include $this->vendorDir.'/composer/autoload_classmap.php' ); @@ -713,6 +722,7 @@ EOF; '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', + 'Composer\\InstalledVersions' => $this->vendorDir.'/composer/InstalledVersions.php', ), include $this->vendorDir.'/composer/autoload_classmap.php' ); @@ -754,6 +764,7 @@ EOF; 'ClassMapBar' => $this->vendorDir.'/b/b/test.php', 'ClassMapBaz' => $this->vendorDir.'/c/c/foo/test.php', 'ClassMapFoo' => $this->vendorDir.'/a/a/src/a.php', + 'Composer\\InstalledVersions' => $this->vendorDir.'/composer/InstalledVersions.php', ), include $this->vendorDir.'/composer/autoload_classmap.php' ); @@ -801,6 +812,7 @@ EOF; 'ClassMapBar' => $this->vendorDir.'/b/b/ClassMapBar.php', 'ClassMapBaz' => $this->vendorDir.'/c/c/foo/ClassMapBaz.php', 'ClassMapFoo' => $this->vendorDir.'/a/a/src/ClassMapFoo.php', + 'Composer\\InstalledVersions' => $this->vendorDir.'/composer/InstalledVersions.php', ), include $this->vendorDir.'/composer/autoload_classmap.php' ); @@ -848,7 +860,8 @@ EOF; $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_static_functions.php', $this->vendorDir.'/composer/autoload_static.php'); $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_files_functions.php', $this->vendorDir.'/composer/autoload_files.php'); - include $this->vendorDir . '/autoload.php'; + $loader = require $this->vendorDir . '/autoload.php'; + $loader->unregister(); $this->assertTrue(function_exists('testFilesAutoloadGeneration1')); $this->assertTrue(function_exists('testFilesAutoloadGeneration2')); $this->assertTrue(function_exists('testFilesAutoloadGeneration3')); @@ -984,7 +997,8 @@ EOF; $this->assertFileContentEquals(__DIR__ . '/Fixtures/autoload_real_files_by_dependency.php', $this->vendorDir . '/composer/autoload_real.php'); $this->assertFileContentEquals(__DIR__ . '/Fixtures/autoload_static_files_by_dependency.php', $this->vendorDir . '/composer/autoload_static.php'); - require $this->vendorDir . '/autoload.php'; + $loader = require $this->vendorDir . '/autoload.php'; + $loader->unregister(); $this->assertTrue(function_exists('testFilesAutoloadOrderByDependency1')); $this->assertTrue(function_exists('testFilesAutoloadOrderByDependency2')); @@ -1083,6 +1097,7 @@ EOF; return array( 'A\\\\B\\\\C' => \$baseDir . '/lib/A/B/C.php', + 'Composer\\\\InstalledVersions' => \$vendorDir . '/composer/InstalledVersions.php', 'Foo\\\\Bar' => \$baseDir . '/src/classes.php', ); @@ -1151,7 +1166,8 @@ EOF; $oldIncludePath = get_include_path(); - require $this->vendorDir."/autoload.php"; + $loader = require $this->vendorDir."/autoload.php"; + $loader->unregister(); $this->assertEquals( $this->vendorDir."/a/a/lib".PATH_SEPARATOR.$oldIncludePath, @@ -1179,7 +1195,8 @@ EOF; $oldIncludePath = get_include_path(); - require $this->vendorDir."/autoload.php"; + $loader = require $this->vendorDir."/autoload.php"; + $loader->unregister(); $this->assertEquals( $this->workingDir."/lib".PATH_SEPARATOR.$this->workingDir."/src".PATH_SEPARATOR.$this->vendorDir."/a/a/lib".PATH_SEPARATOR.$oldIncludePath, @@ -1352,6 +1369,7 @@ $baseDir = dirname($vendorDir).'/working-dir'; return array( 'Bar\\Bar' => $vendorDir . '/b/b/classmaps/classes.php', 'Bar\\Foo' => $vendorDir . '/b/b/lib/Bar/Foo.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Foo\\Bar' => $baseDir . '/src/Foo/Bar.php', 'Foo\\Foo' => $baseDir . '/classmap/classes.php', ); @@ -1430,6 +1448,7 @@ $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir).'/working-dir'; return array( + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Foo\\Bar' => $baseDir . '/../src/Foo/Bar.php', 'Foo\\Foo' => $baseDir . '/../classmap/classes.php', ); @@ -1499,6 +1518,7 @@ $baseDir = dirname($vendorDir); return array( 'Classmap\\Foo' => $baseDir . '/class.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Foo\\Bar' => $baseDir . '/Foo/Bar.php', ); @@ -1625,6 +1645,126 @@ EOF; $this->assertAutoloadFiles('classmap', $this->vendorDir.'/composer', 'classmap'); } + /** + * @dataProvider platformCheckProvider + */ + public function testGeneratesPlatformCheck(array $requires, $expectedFixture, array $provides = array(), array $replaces = array(), $ignorePlatformReqs = false) + { + $package = new Package('a', '1.0', '1.0'); + $package->setRequires($requires); + + if ($provides) { + $package->setProvides($provides); + } + + if ($replaces) { + $package->setReplaces($replaces); + } + + $this->repository->expects($this->once()) + ->method('getCanonicalPackages') + ->will($this->returnValue(array())); + + $this->generator->setIgnorePlatformRequirements($ignorePlatformReqs); + $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_1'); + + if (null === $expectedFixture) { + $this->assertFalse(file_exists($this->vendorDir . '/composer/platform_check.php')); + $this->assertNotContains("require __DIR__ . '/platform_check.php';", file_get_contents($this->vendorDir.'/composer/autoload_real.php')); + } else { + $this->assertFileContentEquals(__DIR__ . '/Fixtures/platform/' . $expectedFixture . '.php', $this->vendorDir . '/composer/platform_check.php'); + $this->assertContains("require __DIR__ . '/platform_check.php';", file_get_contents($this->vendorDir.'/composer/autoload_real.php')); + } + } + + public function platformCheckProvider() + { + $versionParser = new VersionParser(); + + return array( + 'Typical project requirements' => array( + array( + new Link('a', 'php', $versionParser->parseConstraints('^7.2')), + new Link('a', 'ext-xml', $versionParser->parseConstraints('*')), + new Link('a', 'ext-json', $versionParser->parseConstraints('*')) + ), + 'typical' + ), + 'No PHP lower bound' => array( + array( + new Link('a', 'php', $versionParser->parseConstraints('< 8')), + ), + null + ), + 'No PHP upper bound' => array( + array( + new Link('a', 'php', $versionParser->parseConstraints('>= 7.2')), + ), + 'no_php_upper_bound' + ), + 'Specific PHP release version' => array( + array( + new Link('a', 'php', $versionParser->parseConstraints('^7.2.8')), + ), + 'specific_php_release' + ), + 'No PHP required' => array( + array( + new Link('a', 'ext-xml', $versionParser->parseConstraints('*')), + new Link('a', 'ext-json', $versionParser->parseConstraints('*')) + ), + 'no_php_required' + ), + 'Ignoring all platform requirements skips check completely' => array( + array( + new Link('a', 'php', $versionParser->parseConstraints('^7.2')), + new Link('a', 'ext-xml', $versionParser->parseConstraints('*')), + new Link('a', 'ext-json', $versionParser->parseConstraints('*')) + ), + null, + array(), + array(), + true + ), + 'Ignored platform requirements are not checked for' => array( + array( + new Link('a', 'php', $versionParser->parseConstraints('^7.2.8')), + new Link('a', 'ext-xml', $versionParser->parseConstraints('*')), + new Link('a', 'ext-json', $versionParser->parseConstraints('*')), + new Link('a', 'ext-pdo', $versionParser->parseConstraints('*')) + ), + 'no_php_required', + array(), + array(), + array('php', 'ext-pdo') + ), + 'No extensions required' => array( + array( + new Link('a', 'php', $versionParser->parseConstraints('^7.2')), + ), + 'no_extensions_required' + ), + 'Replaced/provided extensions are not checked for + checking case insensitivity' => array( + array( + new Link('a', 'ext-xml', $versionParser->parseConstraints('^7.2')), + new Link('a', 'ext-Pdo', $versionParser->parseConstraints('^7.2')), + new Link('a', 'ext-bcMath', $versionParser->parseConstraints('^7.2')), + ), + 'replaced_provided_exts', + array( + // constraint does not satisfy all the ^7.2 requirement so we do not accept it as being replaced + new Link('a', 'ext-PDO', $versionParser->parseConstraints('7.1.*')), + // valid replace of bcmath so no need to check for it + new Link('a', 'ext-BCMath', $versionParser->parseConstraints('^7.1')), + ), + array( + // valid provide of ext-xml so no need to check for it + new Link('a', 'ext-XML', $versionParser->parseConstraints('*')), + ), + ), + ); + } + private function assertAutoloadFiles($name, $dir, $type = 'namespaces') { $a = __DIR__.'/Fixtures/autoload_'.$name.'.php'; diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap.php b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap.php index a33c6674a..e49f8b7d8 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap.php @@ -8,6 +8,7 @@ $baseDir = dirname($vendorDir); return array( 'Acme\\Cake\\ClassMapBar' => $baseDir . '/src-cake/ClassMapBar.php', 'ClassMapFoo' => $baseDir . '/composersrc/foo.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Lala\\ClassMapMain' => $baseDir . '/src/Lala/ClassMapMain.php', 'Lala\\Test\\ClassMapMainTest' => $baseDir . '/src/Lala/Test/ClassMapMainTest.php', ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap2.php b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap2.php index 9cc0484f1..60cec93b3 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap2.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap2.php @@ -7,4 +7,5 @@ $baseDir = dirname(dirname($vendorDir)); return array( 'ClassMapFoo' => $baseDir . '/composersrc/foo.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap3.php b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap3.php index dacb8ff90..3bfb2bb80 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap3.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap3.php @@ -7,5 +7,6 @@ $baseDir = $vendorDir; return array( 'ClassMapFoo' => $vendorDir . '/composersrc/foo.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.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 ae8025544..767ec5e5d 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap4.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap4.php @@ -9,4 +9,5 @@ return array( 'ClassMapBar' => $vendorDir . '/b/b/src/b.php', 'ClassMapBaz' => $vendorDir . '/b/b/lib/c.php', 'ClassMapFoo' => $vendorDir . '/a/a/src/a.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap5.php b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap5.php index 71bbc004d..e88a14606 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap5.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap5.php @@ -9,4 +9,5 @@ return array( 'ClassMapBar' => $vendorDir . '/b/b/test.php', 'ClassMapBaz' => $vendorDir . '/c/c/foo/test.php', 'ClassMapFoo' => $vendorDir . '/a/a/src/a.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap6.php b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap6.php index ef97fb501..d7a357ace 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap6.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap6.php @@ -8,4 +8,5 @@ $baseDir = dirname($vendorDir); return array( 'ClassMapBar' => $baseDir . '/lib/rootbar.php', 'ClassMapFoo' => $baseDir . '/src/rootfoo.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap7.php b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap7.php index 5768726d1..6818e6b82 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap7.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap7.php @@ -6,5 +6,6 @@ $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Main\\ClassMain' => $baseDir . '/src/Main/ClassMain.php', ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap8.php b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap8.php index 0a40d114c..a002faf5a 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap8.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap8.php @@ -9,4 +9,5 @@ return array( 'ClassMapBar' => $vendorDir . '/b/b/ClassMapBar.php', 'ClassMapBaz' => $vendorDir . '/c/c/foo/ClassMapBaz.php', 'ClassMapFoo' => $vendorDir . '/a/a/src/ClassMapFoo.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap9.php b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap9.php index f9ad3ca30..75bc86230 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_classmap9.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_classmap9.php @@ -8,5 +8,6 @@ $baseDir = dirname($vendorDir); return array( 'A' => $vendorDir . '/a/a/src/A.php', 'C' => $vendorDir . '/c/c/src/C.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'D' => $vendorDir . '/d/d/src/D.php', ); diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_phar_static.php b/tests/Composer/Test/Autoload/Fixtures/autoload_phar_static.php index 486a5c0dc..ceaf04a0c 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_phar_static.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_phar_static.php @@ -75,12 +75,17 @@ class ComposerStaticInitPhar ), ); + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInitPhar::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInitPhar::$prefixDirsPsr4; $loader->prefixesPsr0 = ComposerStaticInitPhar::$prefixesPsr0; + $loader->classMap = ComposerStaticInitPhar::$classMap; }, null, ClassLoader::class); } 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 4e5e0a5cb..fdff2bd42 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 @@ -28,7 +28,7 @@ class ComposerAutoloaderInitFilesAutoloadOrder $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { - require_once __DIR__ . '/autoload_static.php'; + require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitFilesAutoloadOrder::getInitializer($loader)); } else { diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php index 0f1201b8e..67ce8ffe2 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php @@ -28,7 +28,7 @@ class ComposerAutoloaderInitFilesAutoload $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { - require_once __DIR__ . '/autoload_static.php'; + require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitFilesAutoload::getInitializer($loader)); } else { diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_include_paths.php b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_include_paths.php index fc457c406..790029096 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_include_paths.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_include_paths.php @@ -32,7 +32,7 @@ class ComposerAutoloaderInitFilesAutoload $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { - require_once __DIR__ . '/autoload_static.php'; + require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitFilesAutoload::getInitializer($loader)); } else { diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_removed_include_paths_and_autolad_files.php b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_removed_include_paths_and_autolad_files.php index b07825176..7b7898df1 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_removed_include_paths_and_autolad_files.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_functions_with_removed_include_paths_and_autolad_files.php @@ -28,7 +28,7 @@ class ComposerAutoloaderInitFilesAutoload $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { - require_once __DIR__ . '/autoload_static.php'; + require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitFilesAutoload::getInitializer($loader)); } else { diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_real_include_path.php b/tests/Composer/Test/Autoload/Fixtures/autoload_real_include_path.php index 12ac24108..2747dbc0a 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_include_path.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_include_path.php @@ -28,7 +28,7 @@ class ComposerAutoloaderInitIncludePath $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { - require_once __DIR__ . '/autoload_static.php'; + require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitIncludePath::getInitializer($loader)); } else { 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 084d04f30..8ec8cdd47 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_real_target_dir.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_real_target_dir.php @@ -28,7 +28,7 @@ class ComposerAutoloaderInitTargetDir $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { - require_once __DIR__ . '/autoload_static.php'; + require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitTargetDir::getInitializer($loader)); } else { diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_static_files_by_dependency.php b/tests/Composer/Test/Autoload/Fixtures/autoload_static_files_by_dependency.php index ba1f9238a..0c5ce896d 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_static_files_by_dependency.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_static_files_by_dependency.php @@ -15,9 +15,14 @@ class ComposerStaticInitFilesAutoloadOrder '334307692417e52db5a08c3271700a7e' => __DIR__ . '/../..' . '/root2.php', ); + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { + $loader->classMap = ComposerStaticInitFilesAutoloadOrder::$classMap; }, null, ClassLoader::class); } diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_static_functions.php b/tests/Composer/Test/Autoload/Fixtures/autoload_static_functions.php index dfe3c5bcc..e67b7803e 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_static_functions.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_static_functions.php @@ -14,9 +14,14 @@ class ComposerStaticInitFilesAutoload '61b776fd0ee84fb7d7d958ae46118ded' => __DIR__ . '/../..' . '/root.php', ); + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { + $loader->classMap = ComposerStaticInitFilesAutoload::$classMap; }, null, ClassLoader::class); } diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_static_functions_with_include_paths.php b/tests/Composer/Test/Autoload/Fixtures/autoload_static_functions_with_include_paths.php index dfe3c5bcc..e67b7803e 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_static_functions_with_include_paths.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_static_functions_with_include_paths.php @@ -14,9 +14,14 @@ class ComposerStaticInitFilesAutoload '61b776fd0ee84fb7d7d958ae46118ded' => __DIR__ . '/../..' . '/root.php', ); + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { + $loader->classMap = ComposerStaticInitFilesAutoload::$classMap; }, null, ClassLoader::class); } diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_static_functions_with_removed_include_paths_and_autolad_files.php b/tests/Composer/Test/Autoload/Fixtures/autoload_static_functions_with_removed_include_paths_and_autolad_files.php index 11897e11a..44fd701d6 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_static_functions_with_removed_include_paths_and_autolad_files.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_static_functions_with_removed_include_paths_and_autolad_files.php @@ -6,9 +6,14 @@ namespace Composer\Autoload; class ComposerStaticInitFilesAutoload { + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { + $loader->classMap = ComposerStaticInitFilesAutoload::$classMap; }, null, ClassLoader::class); } diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_static_include_path.php b/tests/Composer/Test/Autoload/Fixtures/autoload_static_include_path.php index 3db5da28a..82bbd2ff7 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_static_include_path.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_static_include_path.php @@ -20,10 +20,15 @@ class ComposerStaticInitIncludePath ), ); + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixesPsr0 = ComposerStaticInitIncludePath::$prefixesPsr0; + $loader->classMap = ComposerStaticInitIncludePath::$classMap; }, null, ClassLoader::class); } diff --git a/tests/Composer/Test/Autoload/Fixtures/autoload_static_target_dir.php b/tests/Composer/Test/Autoload/Fixtures/autoload_static_target_dir.php index 114b5a98a..60fd33faa 100644 --- a/tests/Composer/Test/Autoload/Fixtures/autoload_static_target_dir.php +++ b/tests/Composer/Test/Autoload/Fixtures/autoload_static_target_dir.php @@ -28,6 +28,7 @@ class ComposerStaticInitTargetDir public static $classMap = array ( 'ClassMapBar' => __DIR__ . '/../..' . '/lib/rootbar.php', 'ClassMapFoo' => __DIR__ . '/../..' . '/src/rootfoo.php', + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/tests/Composer/Test/Autoload/Fixtures/platform/no_extensions_required.php b/tests/Composer/Test/Autoload/Fixtures/platform/no_extensions_required.php new file mode 100644 index 000000000..85a922d49 --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/platform/no_extensions_required.php @@ -0,0 +1,14 @@ += 70200)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + echo 'Composer detected issues in your platform:' . "\n\n" . implode("\n", $issues); + exit(104); +} diff --git a/tests/Composer/Test/Autoload/Fixtures/platform/no_php_required.php b/tests/Composer/Test/Autoload/Fixtures/platform/no_php_required.php new file mode 100644 index 000000000..f3ae3e071 --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/platform/no_php_required.php @@ -0,0 +1,18 @@ += 70200)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + echo 'Composer detected issues in your platform:' . "\n\n" . implode("\n", $issues); + exit(104); +} diff --git a/tests/Composer/Test/Autoload/Fixtures/platform/replaced_provided_exts.php b/tests/Composer/Test/Autoload/Fixtures/platform/replaced_provided_exts.php new file mode 100644 index 000000000..8056909d6 --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/platform/replaced_provided_exts.php @@ -0,0 +1,17 @@ += 70208)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.8". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + echo 'Composer detected issues in your platform:' . "\n\n" . implode("\n", $issues); + exit(104); +} diff --git a/tests/Composer/Test/Autoload/Fixtures/platform/typical.php b/tests/Composer/Test/Autoload/Fixtures/platform/typical.php new file mode 100644 index 000000000..dde12ec46 --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/platform/typical.php @@ -0,0 +1,22 @@ += 70200)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.'; +} + +$missingExtensions = array(); +extension_loaded('json') || $missingExtensions[] = 'json'; +extension_loaded('xml') || $missingExtensions[] = 'xml'; + +if ($missingExtensions) { + $issues[] = 'Your Composer dependencies require the following PHP extensions to be installed: ' . implode(', ', $missingExtensions); +} + +if ($issues) { + echo 'Composer detected issues in your platform:' . "\n\n" . implode("\n", $issues); + exit(104); +} diff --git a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php index 4ebf37bf3..ffcae5668 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php @@ -35,7 +35,7 @@ class RuleSetIteratorTest extends TestCase new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, null), ), RuleSet::TYPE_LEARNED => array( - new GenericRule(array(), Rule::RULE_INTERNAL_ALLOW_UPDATE, null), + new GenericRule(array(), Rule::RULE_LEARNED, null), ), RuleSet::TYPE_PACKAGE => array(), ); diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index 5a89ddf79..966044bb6 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -31,7 +31,7 @@ class RuleSetTest extends TestCase new GenericRule(array(2), Rule::RULE_ROOT_REQUIRE, null), ), RuleSet::TYPE_LEARNED => array( - new GenericRule(array(), Rule::RULE_INTERNAL_ALLOW_UPDATE, null), + new GenericRule(array(), Rule::RULE_LEARNED, null), ), ); diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index f819397fb..2e1a9921d 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -104,6 +104,6 @@ class RuleTest extends TestCase $rule = new GenericRule(array($p1->getId(), -$p2->getId()), Rule::RULE_PACKAGE_REQUIRES, new Link('baz', 'foo')); - $this->assertEquals('baz 1.1 relates to foo -> satisfiable by foo[2.1].', $rule->getPrettyString($repositorySetMock, $requestMock, $pool)); + $this->assertEquals('baz 1.1 relates to foo -> satisfiable by foo[2.1].', $rule->getPrettyString($repositorySetMock, $requestMock, $pool, false)); } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 3090d9d83..6fa7f1f84 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -25,6 +25,7 @@ use Composer\Repository\InstalledArrayRepository; use Composer\Repository\RepositorySet; use Composer\Test\TestCase; use Composer\Semver\Constraint\MultiConstraint; +use Composer\Semver\Constraint\MatchAllConstraint; class SolverTest extends TestCase { @@ -83,7 +84,7 @@ class SolverTest extends TestCase $problems = $e->getProblems(); $this->assertCount(1, $problems); $this->assertEquals(2, $e->getCode()); - $this->assertEquals("\n - Root composer.json requires b, it could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString($this->repoSet, $this->request, $this->pool)); + $this->assertEquals("\n - Root composer.json requires b, it could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString($this->repoSet, $this->request, $this->pool, false)); } } @@ -378,7 +379,7 @@ class SolverTest extends TestCase { $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); - $packageB->setReplaces(array('a' => new Link('B', 'A', new MultiConstraint(array())))); + $packageB->setReplaces(array('a' => new Link('B', 'A', new MatchAllConstraint()))); $this->reposComplete(); @@ -654,7 +655,7 @@ class SolverTest extends TestCase $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; $msg .= " - A 1.0 conflicts with B 1.0.\n"; $msg .= " - Root composer.json requires b -> satisfiable by B[1.0].\n"; - $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool, false)); } } @@ -683,8 +684,8 @@ class SolverTest extends TestCase $msg = "\n"; $msg .= " Problem 1\n"; $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; - $msg .= " - A 1.0 requires b >= 2.0 -> found B[1.0] but it does not match your constraint.\n"; - $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); + $msg .= " - A 1.0 requires b >= 2.0 -> found B[1.0] but it does not match the constraint.\n"; + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool, false)); } } @@ -729,7 +730,7 @@ class SolverTest extends TestCase $msg .= " - You can only install one version of a package, so only one of these can be installed: B[0.9, 1.0].\n"; $msg .= " - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n"; $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; - $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool, false)); } } diff --git a/tests/Composer/Test/DependencyResolver/TransactionTest.php b/tests/Composer/Test/DependencyResolver/TransactionTest.php index 8b3e66b68..b5d209041 100644 --- a/tests/Composer/Test/DependencyResolver/TransactionTest.php +++ b/tests/Composer/Test/DependencyResolver/TransactionTest.php @@ -53,6 +53,7 @@ class TransactionTest extends TestCase $expectedOperations = array( array('job' => 'uninstall', 'package' => $packageC), array('job' => 'uninstall', 'package' => $packageE), + array('job' => 'markAliasUninstalled', 'package' => $packageEalias), array('job' => 'install', 'package' => $packageA0first), array('job' => 'update', 'from' => $packageB, 'to' => $packageBnew), array('job' => 'install', 'package' => $packageG), @@ -60,7 +61,6 @@ class TransactionTest extends TestCase array('job' => 'markAliasInstalled', 'package' => $packageFalias1), array('job' => 'markAliasInstalled', 'package' => $packageFalias2), array('job' => 'install', 'package' => $packageD), - array('job' => 'markAliasUninstalled', 'package' => $packageEalias), ); $transaction = new Transaction($presentPackages, $resultPackages); diff --git a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php index 793c5eab5..977fb6574 100644 --- a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php @@ -36,7 +36,7 @@ class ArchiveDownloaderTest extends TestCase ->will($this->returnValue('/vendor')); $first = $method->invoke($downloader, $packageMock, '/path'); - $this->assertRegExp('#/vendor/composer/[a-z0-9]+\.js#', $first); + $this->assertRegExp('#/vendor/composer/tmp-[a-z0-9]+\.js#', $first); $this->assertSame($first, $method->invoke($downloader, $packageMock, '/path')); } diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index af5a9da11..c86ffa2f7 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -99,7 +99,7 @@ class FileDownloaderTest extends TestCase ->with('vendor-dir') ->will($this->returnValue('/vendor')); - $this->assertRegExp('#/vendor/composer/[a-z0-9]+\.js#', $method->invoke($downloader, $packageMock, '/path')); + $this->assertRegExp('#/vendor/composer/tmp-[a-z0-9]+\.js#', $method->invoke($downloader, $packageMock, '/path')); } public function testDownloadButFileIsUnsaved() diff --git a/tests/Composer/Test/Fixtures/installer/alias-in-lock.test b/tests/Composer/Test/Fixtures/installer/alias-in-lock.test new file mode 100644 index 000000000..25660566f --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/alias-in-lock.test @@ -0,0 +1,65 @@ +--TEST-- +Root-defined aliases end up in lock file only if required to solve deps +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/aliased", "version": "3.0.2" + }, + { + "name": "a/aliased2", "version": "3.0.2" + }, + { + "name": "b/requirer", "version": "1.0.0", + "require": { "a/aliased": "^3.0.3", "a/aliased2": "^3.0.0" } + } + ] + } + ], + "require": { + "a/aliased": "3.0.2 as 3.0.3", + "a/aliased2": "3.0.2 as 3.0.3", + "b/requirer": "*" + } +} +--RUN-- +update +--EXPECT-LOCK-- +{ + "packages": [ + { + "name": "a/aliased", "version": "3.0.2", + "type": "library" + }, + { + "name": "a/aliased2", "version": "3.0.2", + "type": "library" + }, + { + "name": "b/requirer", "version": "1.0.0", + "require": { "a/aliased": "^3.0.3", "a/aliased2": "^3.0.0" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [{ + "package": "a/aliased", + "version": "3.0.2.0", + "alias": "3.0.3", + "alias_normalized": "3.0.3.0" + }], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- +Installing a/aliased2 (3.0.2) +Installing a/aliased (3.0.2) +Marking a/aliased (3.0.3) as installed, alias of a/aliased (3.0.2) +Installing b/requirer (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test b/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test index ce1aea7bb..19d0ea287 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test @@ -47,10 +47,10 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires b/b *@dev -> satisfiable by b/b[dev-master]. - a/a dev-master requires d/d 1.0.0 -> satisfiable by d/d[1.0.0]. - - You can only install one version of a package, so only one of these can be installed: d/d[2.0.0, 1.0.0]. - - Conclusion: install d/d 2.0.0, learned rules: + - You can only install one version of a package, so only one of these can be installed: d/d[1.0.0, 2.0.0]. + - b/b dev-master requires d/d 2.0.0 -> satisfiable by d/d[2.0.0]. + - Conclusion: install b/b dev-master, learned rules: - Root composer.json requires b/b *@dev -> satisfiable by b/b[dev-master]. - - b/b dev-master requires d/d 2.0.0 -> satisfiable by d/d[2.0.0]. - Root composer.json requires a/a *@dev -> satisfiable by a/a[dev-master]. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test b/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test index 344c4ce67..2e3598ce2 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test +++ b/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test @@ -47,8 +47,8 @@ Your requirements could not be resolved to an installable set of packages. - locked/pkg dev-master requires locked/dependency 1.0.0 -> found locked/dependency[1.0.0] in lock file but not in remote repositories, make sure you avoid updating this package to keep the one from lock file. Problem 2 - locked/pkg dev-master requires locked/dependency 1.0.0 -> found locked/dependency[1.0.0] in lock file but not in remote repositories, make sure you avoid updating this package to keep the one from lock file. - - Root composer.json requires locked/pkg *@dev -> satisfiable by locked/pkg[dev-master]. + - locked/pkg is locked to version dev-master and an update of this package was not requested. -Use the option --with-all-dependencies to allow updates and removals for packages currently locked to specific versions. +Use the option --with-all-dependencies to allow upgrades, downgrades and removals for packages currently locked to specific versions. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/alias-with-reference.test b/tests/Composer/Test/Fixtures/installer/alias-with-reference.test index df25f7478..451e1f8b9 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-with-reference.test +++ b/tests/Composer/Test/Fixtures/installer/alias-with-reference.test @@ -24,7 +24,38 @@ Aliases of referenced packages work } } --RUN-- -install +update +--EXPECT-LOCK-- +{ + "packages": [ + { + "name": "a/aliased", "version": "dev-master", + "source": { "reference": "abcd", "type": "git", "url": "" }, + "type": "library" + }, + { + "name": "b/requirer", "version": "1.0.0", + "require": { "a/aliased": "1.0.0" }, + "source": { "reference": "1.0.0", "type": "git", "url": "" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [{ + "package": "a/aliased", + "version": "dev-master", + "alias": "1.0.0", + "alias_normalized": "1.0.0.0" + }], + "minimum-stability": "stable", + "stability-flags": { + "a/aliased": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --EXPECT-- Installing a/aliased (dev-master abcd) Marking a/aliased (1.0.0) as installed, alias of a/aliased (dev-master abcd) diff --git a/tests/Composer/Test/Fixtures/installer/deduplicate-solver-problems.test b/tests/Composer/Test/Fixtures/installer/deduplicate-solver-problems.test new file mode 100644 index 000000000..bbb245914 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/deduplicate-solver-problems.test @@ -0,0 +1,48 @@ +--TEST-- +Test the error output of solver problems is deduplicated. +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "package/a", "version": "2.0.0", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.0.1", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.0.2", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.0.3", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.1.0", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.2.0", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.3.1", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.3.2", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.3.3", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.3.4", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.3.5", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.4.0", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.5.0", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.6.0", "require": { "missing/dep": "^1.0" } }, + { "name": "missing/dep", "version": "2.0.0" } + ] + } + ], + "require": { + "package/a": "*" + } +} + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - package/a[2.0.0, ..., 2.6.0] require missing/dep ^1.0 -> found missing/dep[2.0.0] but it does not match the constraint. + - Root composer.json requires package/a * -> satisfiable by package/a[2.0.0, ..., 2.6.0]. + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-8902.test b/tests/Composer/Test/Fixtures/installer/github-issues-8902.test new file mode 100644 index 000000000..53fc1275b --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/github-issues-8902.test @@ -0,0 +1,46 @@ +--TEST-- + +See Github issue #8902 ( https://github.com/composer/composer/issues/8902 ). + +Avoid installing packages twice if they are required in different versions and one is matched by a dev package. + +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "beyondcode/laravel-dump-server", "version": "1.4.0", "require": { "symfony/var-dumper": "^5.0" } }, + { "name": "laravel/framework", "version": "6.8.14", "require": { "symfony/var-dumper": "^4.3.4" } }, + { "name": "symfony/var-dumper", "version": "4.4.0" }, + { "name": "symfony/var-dumper", "version": "dev-master", "extra": { "branch-alias": {"dev-master": "5.2-dev"} } } + ] + } + ], + "require": { + "beyondcode/laravel-dump-server": "^1.3", + "laravel/framework": "^6.8" + }, + "minimum-stability": "dev" +} + +--RUN-- +update + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires beyondcode/laravel-dump-server ^1.3 -> satisfiable by beyondcode/laravel-dump-server[1.4.0]. + - You can only install one version of a package, so only one of these can be installed: symfony/var-dumper[dev-master, 4.4.0]. + - symfony/var-dumper 5.2.x-dev is an alias of symfony/var-dumper dev-master and thus requires it to be installed too. + - laravel/framework 6.8.14 requires symfony/var-dumper ^4.3.4 -> satisfiable by symfony/var-dumper[4.4.0]. + - beyondcode/laravel-dump-server 1.4.0 requires symfony/var-dumper ^5.0 -> satisfiable by symfony/var-dumper[5.2.x-dev (alias of dev-master)]. + - Root composer.json requires laravel/framework ^6.8 -> satisfiable by laravel/framework[6.8.14]. + +--EXPECT-- + +--EXPECT-EXIT-CODE-- +2 diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test index 25bd4a9c6..b1298ad6e 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test @@ -60,3 +60,5 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - b/unstable is fixed to 1.1.0-alpha (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you list it as an argument for the update command. + +Use the option --with-all-dependencies to allow upgrades, downgrades and removals for packages currently locked to specific versions. diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test b/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test index 7530ca862..9d971b384 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test @@ -97,9 +97,9 @@ update b/b "platform-dev": [] } --EXPECT-- +Marking a/a (2.1.x-dev oldmaster-a) as uninstalled, alias of a/a (dev-master oldmaster-a) +Marking b/b (2.1.x-dev oldmaster-b) as uninstalled, alias of b/b (dev-master oldmaster-b) Upgrading a/a (dev-master oldmaster-a => dev-master newmaster-a) Marking a/a (2.2.x-dev newmaster-a) as installed, alias of a/a (dev-master newmaster-a) Upgrading b/b (dev-master oldmaster-b => dev-master newmaster-b2) Marking b/b (2.3.x-dev newmaster-b2) as installed, alias of b/b (dev-master newmaster-b2) -Marking a/a (2.1.x-dev oldmaster-a) as uninstalled, alias of a/a (dev-master oldmaster-a) -Marking b/b (2.1.x-dev oldmaster-b) as uninstalled, alias of b/b (dev-master oldmaster-b) diff --git a/tests/Composer/Test/Fixtures/installer/prefer-lowest-branches.test b/tests/Composer/Test/Fixtures/installer/prefer-lowest-branches.test new file mode 100644 index 000000000..930b87ae8 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/prefer-lowest-branches.test @@ -0,0 +1,29 @@ +--TEST-- +Assert that prefer-lowest can not pick the lowest version of all packages when two branches are valid but conflict with each other +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + {"name": "req/pkg", "version": "1.0.0", "require": {"req/pkg2": "^1.2"}}, + {"name": "req/pkg2", "version": "1.0.0", "require": {"req/pkg": "^1.2"}}, + {"name": "req/pkg", "version": "1.2.0", "require": {"req/pkg2": "^1.0"}}, + {"name": "req/pkg2", "version": "1.2.0", "require": {"req/pkg": "^1.0"}}, + {"name": "req/pkg", "version": "1.4.0", "require": {"req/pkg2": "^1.0"}}, + {"name": "req/pkg2", "version": "1.4.0", "require": {"req/pkg": "^1.0"}} + ] + } + ], + "require": { + "req/pkg": "*", + "req/pkg2": "*" + } +} + +--RUN-- +update --prefer-lowest + +--EXPECT-- +Installing req/pkg2 (1.2.0) +Installing req/pkg (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/problems-reduce-versions.test b/tests/Composer/Test/Fixtures/installer/problems-reduce-versions.test new file mode 100644 index 000000000..aa16b8acd --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/problems-reduce-versions.test @@ -0,0 +1,118 @@ +--TEST-- +Test the error output minifies version lists +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + {"name": "a/a", "version": "1.0.0", "require": {"b/b": "1.0.0"}}, + {"name": "b/b", "version": "1.0.0"}, + {"name": "b/b", "version": "1.0.1"}, + {"name": "b/b", "version": "1.0.2"}, + {"name": "b/b", "version": "1.0.3"}, + {"name": "b/b", "version": "v1.1.4"}, + {"name": "b/b", "version": "1.0.4"}, + {"name": "b/b", "version": "1.0.5"}, + {"name": "b/b", "version": "1.0.6"}, + {"name": "b/b", "version": "1.0.7"}, + {"name": "b/b", "version": "1.1.0"}, + {"name": "b/b", "version": "2.0.5"}, + {"name": "b/b", "version": "1.0.8"}, + {"name": "b/b", "version": "1.0.9"}, + {"name": "b/b", "version": "1.1.1"}, + {"name": "b/b", "version": "1.1.2"}, + {"name": "b/b", "version": "1.1.3"}, + {"name": "b/b", "version": "1.1.5"}, + {"name": "b/b", "version": "v1.1.6"}, + {"name": "b/b", "version": "1.1.7-alpha"}, + {"name": "b/b", "version": "1.1.8"}, + {"name": "b/b", "version": "1.1.9"}, + {"name": "b/b", "version": "1.2.0"}, + {"name": "b/b", "version": "1.2.2"}, + {"name": "b/b", "version": "1.2.3"}, + {"name": "b/b", "version": "1.2.4"}, + {"name": "b/b", "version": "1.2.5"}, + {"name": "b/b", "version": "1.2.6"}, + {"name": "b/b", "version": "1.2.1"}, + {"name": "b/b", "version": "1.2.7"}, + {"name": "b/b", "version": "1.2.8"}, + {"name": "b/b", "version": "1.2.9"}, + {"name": "b/b", "version": "2.0.0"}, + {"name": "b/b", "version": "2.0.1"}, + {"name": "b/b", "version": "2.0.2"}, + {"name": "b/b", "version": "2.0.3"}, + {"name": "b/b", "version": "2.0.4"}, + {"name": "b/b", "version": "2.0.6"}, + {"name": "b/b", "version": "2.0.7"}, + {"name": "b/b", "version": "2.0.8"}, + {"name": "b/b", "version": "2.0.9"}, + {"name": "b/b", "version": "2.1.0"}, + {"name": "b/b", "version": "2.1.1"}, + {"name": "b/b", "version": "2.1.2"}, + {"name": "b/b", "version": "2.1.3"}, + {"name": "b/b", "version": "2.1.4"}, + {"name": "b/b", "version": "2.1.5"}, + {"name": "b/b", "version": "2.1.6"}, + {"name": "b/b", "version": "2.1.7"}, + {"name": "b/b", "version": "2.1.8"}, + {"name": "b/b", "version": "2.1.9"}, + {"name": "b/b", "version": "2.2.0"}, + {"name": "b/b", "version": "2.2.1"}, + {"name": "b/b", "version": "2.2.2"}, + {"name": "b/b", "version": "2.2.3"}, + {"name": "b/b", "version": "2.2.4"}, + {"name": "b/b", "version": "2.2.5"}, + {"name": "b/b", "version": "2.2.6"}, + {"name": "b/b", "version": "2.2.7"}, + {"name": "b/b", "version": "2.2.8"}, + {"name": "b/b", "version": "2.2.9"}, + {"name": "b/b", "version": "2.3.0-RC"}, + {"name": "b/b", "version": "3.0.0"}, + {"name": "b/b", "version": "3.0.1"}, + {"name": "b/b", "version": "3.0.2"}, + {"name": "b/b", "version": "3.0.3"}, + {"name": "b/b", "version": "4.0.0"} + ] + } + ], + "require": { + "a/a": "*", + "b/b": "^1.1 || ^2.0 || ^3.0" + }, + "minimum-stability": "dev" +} + +--LOCK-- +{ + "packages": [ + {"name": "b/b", "version": "1.0.0"} + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} + +--RUN-- +update a/a + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires b/b ^1.1 || ^2.0 || ^3.0, found b/b[1.1.0, ..., 1.2.9, 2.0.0, ..., 2.3.0-RC, 3.0.0, 3.0.1, 3.0.2, 3.0.3] but the package is fixed to 1.0.0 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command. + +Use the option --with-all-dependencies to allow upgrades, downgrades and removals for packages currently locked to specific versions. + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test index 1c2ea0ceb..53d11ecf0 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test @@ -39,14 +39,14 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Conclusion: don't install regular/pkg 1.0.3, learned rules: - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. + - Only one of these can be installed: regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3], replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. - Conclusion: don't install regular/pkg 1.0.2, learned rules: - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. + - Only one of these can be installed: regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3], replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. - Conclusion: don't install regular/pkg 1.0.1, learned rules: - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. + - Only one of these can be installed: regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3], replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. + - Only one of these can be installed: regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3], replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. - Root composer.json requires regular/pkg 1.* -> satisfiable by regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3]. - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. diff --git a/tests/Composer/Test/Fixtures/installer/replace-range-require-single-version.test b/tests/Composer/Test/Fixtures/installer/replace-range-require-single-version.test new file mode 100644 index 000000000..00b780c4c --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/replace-range-require-single-version.test @@ -0,0 +1,30 @@ +--TEST-- +Verify replacing an unbound range and requiring a single version works as well as vice versa. + +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.1", "replace": {"c/c": ">2.0" }}, + { "name": "b/b", "version": "1.0.2", "require": {"c/c": "2.1.2" }}, + { "name": "d/d", "version": "1.0.3", "replace": {"f/f": "2.1.2" }}, + { "name": "e/e", "version": "1.0.4", "require": {"f/f": ">2.0" }} + ] + } + ], + "require": { + "a/a": "1.0.1", + "b/b": "1.0.2", + "d/d": "1.0.3", + "e/e": "1.0.4" + } +} +--RUN-- +update +--EXPECT-- +Installing a/a (1.0.1) +Installing b/b (1.0.2) +Installing d/d (1.0.3) +Installing e/e (1.0.4) diff --git a/tests/Composer/Test/Fixtures/installer/root-alias-change-with-circular-dep.test b/tests/Composer/Test/Fixtures/installer/root-alias-change-with-circular-dep.test new file mode 100644 index 000000000..5cd80b27b --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/root-alias-change-with-circular-dep.test @@ -0,0 +1,67 @@ +--TEST-- +Root alias changing after the lock file was created and invalidating it should show a decent error message +This also checks that an implicit stabilityFlag is added for the root package, if it is a dev version +--COMPOSER-- +{ + "name": "root/pkg", + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "b/requirer", "version": "1.0.0", + "require": { "root/pkg": "^1" } + } + ] + } + ], + "require": { + "b/requirer": "*" + }, + "version": "2.x-dev" +} + +--INSTALLED-- +[ + { + "name": "b/requirer", "version": "1.0.0", + "require": { "root/pkg": "^1" } + } +] + +--LOCK-- +{ + "packages": [ + { + "name": "b/requirer", "version": "1.0.0", + "require": { "root/pkg": "^1" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +install + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Installing dependencies from lock file (including require-dev) +Verifying lock file contents can be installed on current platform. +Your lock file does not contain a compatible set of packages. Please run composer update. + + Problem 1 + - b/requirer is locked to version 1.0.0 and an update of this package was not requested. + - b/requirer 1.0.0 requires root/pkg ^1 -> found root/pkg[2.x-dev] but it does not match the constraint. + +Use the option --with-all-dependencies to allow upgrades, downgrades and removals for packages currently locked to specific versions. + +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 9c3d0e534..9ba5867a1 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -140,7 +140,7 @@ Your requirements could not be resolved to an installable set of packages. - package/found4 2.0.0 requires non-existent/pkg2 1.* -> could not be found in any version, there may be a typo in the package name. Problem 13 - Root composer.json requires package/found6 2.* -> satisfiable by package/found6[2.0.0]. - - package/found6 2.0.0 requires stable-requiree-excluded/pkg2 1.0.1 -> found stable-requiree-excluded/pkg2[1.0.0] but it does not match your constraint. + - package/found6 2.0.0 requires stable-requiree-excluded/pkg2 1.0.1 -> found stable-requiree-excluded/pkg2[1.0.0] but it does not match the constraint. Problem 14 - Root composer.json requires package/found7 2.* -> satisfiable by package/found7[2.0.0]. - package/found7 2.0.0 requires php-64bit 1.0.1 -> your php-64bit version (%s) does not satisfy that requirement. @@ -159,9 +159,12 @@ Potential causes: - It's a private package and you forgot to add a custom repository to find it Read for further common problems. - To enable extensions, verify that they are enabled in your .ini files: + +To enable extensions, verify that they are enabled in your .ini files: __inilist__ - You can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode. +You can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode. + +Use the option --with-all-dependencies to allow upgrades, downgrades and removals for packages currently locked to specific versions. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-alias-lock2.test b/tests/Composer/Test/Fixtures/installer/update-alias-lock2.test new file mode 100644 index 000000000..b06ce3119 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock2.test @@ -0,0 +1,84 @@ +--TEST-- +Updating an aliased package where the old alias matches the new package should not fail +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/a", "version": "1.10.x-dev", + "extra": { "branch-alias": { "dev-master": "1.10.x-dev" } }, + "source": { "type": "git", "url": "", "reference": "downgradedref" } + }, + { + "name": "a/a", "version": "dev-master", + "extra": { "branch-alias": { "dev-master": "2.x-dev" } }, + "source": { "type": "git", "url": "", "reference": "newref" } + } + ] + } + ], + "require": { + "a/a": "^1.0" + }, + "minimum-stability": "dev" +} +--LOCK-- +{ + "_": "outdated lock file, should not have to be loaded in an update", + "packages": [ + { + "name": "a/a", "version": "dev-master", + "extra": { "branch-alias": { "dev-master": "1.10.x-dev" } }, + "source": { "type": "git", "url": "", "reference": "installedref" } + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false +} +--INSTALLED-- +[ + { + "name": "a/a", "version": "dev-master", + "extra": { "branch-alias": { "dev-master": "1.10.x-dev" } }, + "source": { "type": "git", "url": "", "reference": "installedref" } + } +] +--RUN-- +update +--EXPECT-LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "1.10.x-dev", + "extra": { "branch-alias": { "dev-master": "1.10.x-dev" } }, + "source": { "type": "git", "url": "", "reference": "downgradedref" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-INSTALLED-- +[ + { + "name": "a/a", "version": "1.10.x-dev", + "source": { "type": "git", "url": "", "reference": "downgradedref" }, + "type": "library", + "extra": { "branch-alias": { "dev-master": "1.10.x-dev" } } + } +] +--EXPECT-- +Marking a/a (1.10.x-dev installedref) as uninstalled, alias of a/a (dev-master installedref) +Downgrading a/a (dev-master installedref => 1.10.x-dev downgradedref) diff --git a/tests/Composer/Test/Fixtures/installer/update-alias.test b/tests/Composer/Test/Fixtures/installer/update-alias.test index 8da3d4d23..944eed259 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias.test @@ -33,5 +33,5 @@ Update aliased package to non-aliased version --RUN-- update --EXPECT-- -Upgrading a/a (dev-master master => dev-foo foo) Marking a/a (1.0.x-dev master) as uninstalled, alias of a/a (dev-master master) +Upgrading a/a (dev-master master => dev-foo foo) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test index 641ac7f9e..60f899ba1 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test @@ -51,5 +51,5 @@ Your requirements could not be resolved to an installable set of packages. - new/pkg[1.0.0] cannot be installed as that would require removing current/dep[1.0.0]. new/pkg replaces current/dep and thus cannot coexist with it. - Root composer.json requires new/pkg 1.* -> satisfiable by new/pkg[1.0.0]. -Use the option --with-all-dependencies to allow updates and removals for packages currently locked to specific versions. +Use the option --with-all-dependencies to allow upgrades, downgrades and removals for packages currently locked to specific versions. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test index 2b68c6c69..193a5d85d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test @@ -91,9 +91,9 @@ update new/pkg --with-all-dependencies "platform-dev": [] } --EXPECT-- +Marking current/dep2 (1.0.x-dev) as uninstalled, alias of current/dep2 (dev-foo) Marking current/dep (1.1.0) as installed, alias of current/dep (dev-master) Upgrading current/dep2 (dev-foo => dev-master) Marking current/dep2 (1.1.2) as installed, alias of current/dep2 (dev-master) Marking current/dep2 (2.x-dev) as installed, alias of current/dep2 (dev-master) Installing new/pkg (1.0.0) -Marking current/dep2 (1.0.x-dev) as uninstalled, alias of current/dep2 (dev-foo) diff --git a/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test b/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test index f0755d0d0..b213e480d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test +++ b/tests/Composer/Test/Fixtures/installer/update-downgrades-unstable-packages.test @@ -46,5 +46,5 @@ Downgrading from unstable to more stable package should work even if already ins --RUN-- update --EXPECT-- -Downgrading a/a (dev-master abcd => 1.0.0) Marking a/a (9999999-dev abcd) as uninstalled, alias of a/a (dev-master abcd) +Downgrading a/a (dev-master abcd => 1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test index 06cbc27c7..9844616f4 100644 --- a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test +++ b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test @@ -61,8 +61,8 @@ Updates with --no-dev but we still end up with a complete lock file including de update --no-dev --EXPECT-- Removing a/b (1.0.0) +Marking dev/pkg (1.0.x-dev old) as uninstalled, alias of dev/pkg (dev-master old) Upgrading a/a (1.0.0 => 1.0.1) Installing a/c (1.0.0) Upgrading dev/pkg (dev-master old => dev-master new) Marking dev/pkg (1.1.x-dev new) as installed, alias of dev/pkg (dev-master new) -Marking dev/pkg (1.0.x-dev old) as uninstalled, alias of dev/pkg (dev-master old) diff --git a/tests/Composer/Test/Fixtures/installer/update-no-install.test b/tests/Composer/Test/Fixtures/installer/update-no-install.test new file mode 100644 index 000000000..e1b6f4954 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-no-install.test @@ -0,0 +1,64 @@ +--TEST-- +Updates without install updates the lock but not the installed state +--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 --no-install + +--EXPECT-LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.1", "type": "library" }, + { "name": "a/c", "version": "1.0.0", "type": "library" } + ], + "packages-dev": [ + { "name": "a/b", "version": "2.0.0", "type": "library" } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} + +--EXPECT-INSTALLED-- +[ + { "name": "a/a", "version": "1.0.0", "type": "library" }, + { "name": "a/b", "version": "1.0.0", "type": "library" }, + { "name": "a/c", "version": "1.0.0", "type": "library" } +] + +--EXPECT-- 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 3fb6654ab..ec92ea607 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 @@ -42,5 +42,5 @@ Installing locked dev packages should remove old dependencies install --EXPECT-- Removing a/dependency (dev-master ref) -Upgrading a/devpackage (dev-master oldref => dev-master newref) Marking a/dependency (9999999-dev ref) as uninstalled, alias of a/dependency (dev-master ref) +Upgrading a/devpackage (dev-master oldref => dev-master newref) diff --git a/tests/Composer/Test/IO/ConsoleIOTest.php b/tests/Composer/Test/IO/ConsoleIOTest.php index ef5096300..1ff86170c 100644 --- a/tests/Composer/Test/IO/ConsoleIOTest.php +++ b/tests/Composer/Test/IO/ConsoleIOTest.php @@ -84,7 +84,7 @@ class ConsoleIOTest extends TestCase ->with( $this->callback(function ($messages) { $result = preg_match("[(.*)/(.*) First line]", $messages[0]) > 0; - $result &= preg_match("[(.*)/(.*) Second line]", $messages[1]) > 0; + $result = $result && preg_match("[(.*)/(.*) Second line]", $messages[1]) > 0; return $result; }), diff --git a/tests/Composer/Test/InstalledVersionsTest.php b/tests/Composer/Test/InstalledVersionsTest.php new file mode 100644 index 000000000..c136e1584 --- /dev/null +++ b/tests/Composer/Test/InstalledVersionsTest.php @@ -0,0 +1,211 @@ + + * 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\Test\TestCase; +use Composer\InstalledVersions; +use Composer\Semver\VersionParser; + +class InstalledVersionsTest extends TestCase +{ + public function setUp() + { + InstalledVersions::reload(require __DIR__.'/Repository/Fixtures/installed.php'); + } + + public function testGetInstalledPackages() + { + $names = array( + '__root__', + 'a/provider', + 'a/provider2', + 'b/replacer', + 'c/c', + 'foo/impl', + 'foo/impl2', + 'foo/replaced', + ); + $this->assertSame($names, InstalledVersions::getInstalledPackages()); + } + + /** + * @dataProvider isInstalledProvider + */ + public function testIsInstalled($expected, $name, $constraint = null) + { + $this->assertSame($expected, InstalledVersions::isInstalled($name)); + } + + public static function isInstalledProvider() + { + return array( + array(true, 'foo/impl'), + array(true, 'foo/replaced'), + array(true, 'c/c'), + array(true, '__root__'), + array(true, 'b/replacer'), + array(false, 'not/there'), + array(false, 'not/there', '^1.0'), + ); + } + + /** + * @dataProvider satisfiesProvider + */ + public function testSatisfies($expected, $name, $constraint) + { + $this->assertSame($expected, InstalledVersions::satisfies(new VersionParser, $name, $constraint)); + } + + public static function satisfiesProvider() + { + return array( + array(true, 'foo/impl', '1.5'), + array(true, 'foo/impl', '1.2'), + array(true, 'foo/impl', '^1.0'), + array(true, 'foo/impl', '^3 || ^2'), + array(false, 'foo/impl', '^3'), + + array(true, 'foo/replaced', '3.5'), + array(true, 'foo/replaced', '^3.2'), + array(false, 'foo/replaced', '4.0'), + + array(true, 'c/c', '3.0.0'), + array(true, 'c/c', '^3'), + array(false, 'c/c', '^3.1'), + + array(true, '__root__', 'dev-master'), + array(true, '__root__', '^1.10'), + array(false, '__root__', '^2'), + + array(true, 'b/replacer', '^2.1'), + array(false, 'b/replacer', '^2.3'), + + array(true, 'a/provider2', '^1.2'), + array(true, 'a/provider2', '^1.4'), + array(false, 'a/provider2', '^1.5'), + ); + } + + /** + * @dataProvider getVersionRangesProvider + */ + public function testGetVersionRanges($expected, $name) + { + $this->assertSame($expected, InstalledVersions::getVersionRanges($name)); + } + + public static function getVersionRangesProvider() + { + return array( + array('dev-master || 1.10.x-dev', '__root__'), + array('^1.1 || 1.2 || 1.4 || 2.0', 'foo/impl'), + array('2.2 || 2.0', 'foo/impl2'), + array('^3.0', 'foo/replaced'), + array('1.1', 'a/provider'), + array('1.2 || 1.4', 'a/provider2'), + array('2.2', 'b/replacer'), + array('3.0', 'c/c'), + ); + } + + /** + * @dataProvider getVersionProvider + */ + public function testGetVersion($expected, $name) + { + $this->assertSame($expected, InstalledVersions::getVersion($name)); + } + + public static function getVersionProvider() + { + return array( + array('dev-master', '__root__'), + array(null, 'foo/impl'), + array(null, 'foo/impl2'), + array(null, 'foo/replaced'), + array('1.1.0.0', 'a/provider'), + array('1.2.0.0', 'a/provider2'), + array('2.2.0.0', 'b/replacer'), + array('3.0.0.0', 'c/c'), + ); + } + + /** + * @dataProvider getPrettyVersionProvider + */ + public function testGetPrettyVersion($expected, $name) + { + $this->assertSame($expected, InstalledVersions::getPrettyVersion($name)); + } + + public static function getPrettyVersionProvider() + { + return array( + array('dev-master', '__root__'), + array(null, 'foo/impl'), + array(null, 'foo/impl2'), + array(null, 'foo/replaced'), + array('1.1', 'a/provider'), + array('1.2', 'a/provider2'), + array('2.2', 'b/replacer'), + array('3.0', 'c/c'), + ); + } + + public function testGetVersionOutOfBounds() + { + $this->setExpectedException('OutOfBoundsException'); + InstalledVersions::getVersion('not/installed'); + } + + public function testGetRootPackage() + { + $this->assertSame(array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'aliases' => array( + '1.10.x-dev', + ), + 'reference' => 'sourceref-by-default', + 'name' => '__root__', + ), InstalledVersions::getRootPackage()); + } + + public function testGetRawData() + { + $this->assertSame(require __DIR__.'/Repository/Fixtures/installed.php', InstalledVersions::getRawData()); + } + + /** + * @dataProvider getReferenceProvider + */ + public function testGetReference($expected, $name) + { + $this->assertSame($expected, InstalledVersions::getReference($name)); + } + + public static function getReferenceProvider() + { + return array( + array('sourceref-by-default', '__root__'), + array(null, 'foo/impl'), + array(null, 'foo/impl2'), + array(null, 'foo/replaced'), + array('distref-as-no-source', 'a/provider'), + array('distref-as-installed-from-dist', 'a/provider2'), + array(null, 'b/replacer'), + array(null, 'c/c'), + ); + } +} diff --git a/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php b/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php index 286b386d9..fb8003306 100644 --- a/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php +++ b/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php @@ -222,7 +222,7 @@ class SuggestedPackagesReporterTest extends TestCase */ public function testOutputSkipInstalledPackages() { - $repository = $this->getMockBuilder('Composer\Repository\RepositoryInterface')->getMock(); + $repository = $this->getMockBuilder('Composer\Repository\InstalledRepository')->disableOriginalConstructor()->getMock(); $package1 = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $package2 = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); @@ -260,7 +260,7 @@ class SuggestedPackagesReporterTest extends TestCase */ public function testOutputNotGettingInstalledPackagesWhenNoSuggestions() { - $repository = $this->getMockBuilder('Composer\Repository\RepositoryInterface')->getMock(); + $repository = $this->getMockBuilder('Composer\Repository\InstalledRepository')->disableOriginalConstructor()->getMock(); $repository->expects($this->exactly(0)) ->method('getPackages'); diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 03d301164..94a56147e 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -184,7 +184,7 @@ class InstallerTest extends TestCase /** * @dataProvider getIntegrationTests */ - public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectResult) + public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult) { if ($condition) { eval('$res = '.$condition.';'); @@ -264,10 +264,12 @@ class InstallerTest extends TestCase $application = new Application; $application->get('install')->setCode(function ($input, $output) use ($installer) { + $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); + $installer ->setDevMode(!$input->getOption('no-dev')) ->setDryRun($input->getOption('dry-run')) - ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); + ->setIgnorePlatformRequirements($ignorePlatformReqs); return $installer->run(); }); @@ -287,16 +289,19 @@ class InstallerTest extends TestCase $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; } + $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); + $installer ->setDevMode(!$input->getOption('no-dev')) ->setUpdate(true) + ->setInstall(!$input->getOption('no-install')) ->setDryRun($input->getOption('dry-run')) ->setUpdateMirrors($updateMirrors) ->setUpdateAllowList($packages) ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) - ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); + ->setIgnorePlatformRequirements($ignorePlatformReqs); return $installer->run(); }); @@ -327,6 +332,23 @@ class InstallerTest extends TestCase $this->assertEquals($expectLock, $actualLock); } + if ($expectInstalled !== null) { + $actualInstalled = array(); + $dumper = new ArrayDumper(); + + foreach ($repositoryManager->getLocalRepository()->getCanonicalPackages() as $package) { + $package = $dumper->dump($package); + unset($package['version_normalized']); + $actualInstalled[] = $package; + } + + usort($actualInstalled, function ($a, $b) { + return strcmp($a['name'], $b['name']); + }); + + $this->assertSame($expectInstalled, $actualInstalled); + } + $installationManager = $composer->getInstallationManager(); $this->assertSame(rtrim($expect), implode("\n", $installationManager->getTrace())); @@ -355,6 +377,7 @@ class InstallerTest extends TestCase $installedDev = array(); $lock = array(); $expectLock = array(); + $expectInstalled = null; $expectResult = 0; $message = $testData['TEST']; @@ -393,6 +416,9 @@ class InstallerTest extends TestCase $expectLock = JsonFile::parseJson($testData['EXPECT-LOCK']); } } + if (!empty($testData['EXPECT-INSTALLED'])) { + $expectInstalled = JsonFile::parseJson($testData['EXPECT-INSTALLED']); + } $expectOutput = isset($testData['EXPECT-OUTPUT']) ? $testData['EXPECT-OUTPUT'] : null; $expect = $testData['EXPECT']; if (!empty($testData['EXPECT-EXCEPTION'])) { @@ -409,7 +435,7 @@ class InstallerTest extends TestCase die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file))); } - $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectResult); + $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult); } return $tests; @@ -427,6 +453,7 @@ class InstallerTest extends TestCase 'INSTALLED' => false, 'RUN' => true, 'EXPECT-LOCK' => false, + 'EXPECT-INSTALLED' => false, 'EXPECT-OUTPUT' => false, 'EXPECT-EXIT-CODE' => false, 'EXPECT-EXCEPTION' => false, diff --git a/tests/Composer/Test/Json/ComposerSchemaTest.php b/tests/Composer/Test/Json/ComposerSchemaTest.php index 13ead0ad2..a6f85d4cf 100644 --- a/tests/Composer/Test/Json/ComposerSchemaTest.php +++ b/tests/Composer/Test/Json/ComposerSchemaTest.php @@ -20,6 +20,23 @@ use Composer\Test\TestCase; */ class ComposerSchemaTest extends TestCase { + public function testNamePattern() + { + $expectedError = array( + array( + 'property' => 'name', + 'message' => 'Does not match the regex pattern ^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$', + 'constraint' => 'pattern', + 'pattern' => '^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$' + ), + ); + + $json = '{"name": "vendor/-pack__age", "description": "description"}'; + $this->assertEquals($expectedError, $this->check($json)); + $json = '{"name": "Vendor/Package", "description": "description"}'; + $this->assertEquals($expectedError, $this->check($json)); + } + public function testRequiredProperties() { $json = '{ }'; @@ -41,13 +58,13 @@ class ComposerSchemaTest extends TestCase public function testOptionalAbandonedProperty() { - $json = '{"name": "name", "description": "description", "abandoned": true}'; + $json = '{"name": "vendor/package", "description": "description", "abandoned": true}'; $this->assertTrue($this->check($json)); } public function testRequireTypes() { - $json = '{"name": "name", "description": "description", "require": {"a": ["b"]} }'; + $json = '{"name": "vendor/package", "description": "description", "require": {"a": ["b"]} }'; $this->assertEquals(array( array('property' => 'require.a', 'message' => 'Array value found, but a string is required', 'constraint' => 'type'), ), $this->check($json)); diff --git a/tests/Composer/Test/Mock/FactoryMock.php b/tests/Composer/Test/Mock/FactoryMock.php index d4dc444a0..608d572dd 100644 --- a/tests/Composer/Test/Mock/FactoryMock.php +++ b/tests/Composer/Test/Mock/FactoryMock.php @@ -17,6 +17,7 @@ use Composer\Config; use Composer\Factory; use Composer\Repository\RepositoryManager; use Composer\Repository\WritableRepositoryInterface; +use Composer\Package\RootPackageInterface; use Composer\Installer; use Composer\EventDispatcher\EventDispatcher; use Composer\IO\IOInterface; @@ -37,7 +38,7 @@ class FactoryMock extends Factory return $config; } - protected function addLocalRepository(IOInterface $io, RepositoryManager $rm, $vendorDir) + protected function addLocalRepository(IOInterface $io, RepositoryManager $rm, $vendorDir, RootPackageInterface $rootPackage) { } diff --git a/tests/Composer/Test/Mock/InstallationManagerMock.php b/tests/Composer/Test/Mock/InstallationManagerMock.php index 2d4a8781b..1d1dfef8c 100644 --- a/tests/Composer/Test/Mock/InstallationManagerMock.php +++ b/tests/Composer/Test/Mock/InstallationManagerMock.php @@ -66,7 +66,9 @@ class InstallationManagerMock extends InstallationManager $this->updated[] = array($operation->getInitialPackage(), $operation->getTargetPackage()); $this->trace[] = strip_tags((string) $operation); $repo->removePackage($operation->getInitialPackage()); - $repo->addPackage(clone $operation->getTargetPackage()); + if (!$repo->hasPackage($operation->getTargetPackage())) { + $repo->addPackage(clone $operation->getTargetPackage()); + } } public function uninstall(RepositoryInterface $repo, UninstallOperation $operation) diff --git a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php index e1e06d660..2c633e76b 100644 --- a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php @@ -86,6 +86,7 @@ class ValidatingArrayLoaderTest extends TestCase 'a/b' => '1.*', 'b/c' => '~2', 'example' => '>2.0-dev,<2.4-dev', + 'composer-runtime-api' => '*', ), 'require-dev' => array( 'a/b' => '1.*', diff --git a/tests/Composer/Test/Package/Version/VersionSelectorTest.php b/tests/Composer/Test/Package/Version/VersionSelectorTest.php index 66d07d267..14d00b438 100644 --- a/tests/Composer/Test/Package/Version/VersionSelectorTest.php +++ b/tests/Composer/Test/Package/Version/VersionSelectorTest.php @@ -15,6 +15,7 @@ namespace Composer\Test\Package\Version; use Composer\Package\Version\VersionSelector; use Composer\Package\Package; use Composer\Package\Link; +use Composer\Repository\PlatformRepository; use Composer\Semver\VersionParser; use Composer\Test\TestCase; @@ -46,27 +47,82 @@ class VersionSelectorTest extends TestCase $this->assertSame($package2, $best, 'Latest version should be 1.2.2'); } - public function testLatestVersionIsReturnedThatMatchesPhpRequirement() + public function testLatestVersionIsReturnedThatMatchesPhpRequirements() { $packageName = 'foobar'; + $platform = new PlatformRepository(array(), array('php' => '5.5.0')); + $repositorySet = $this->createMockRepositorySet(); + $versionSelector = new VersionSelector($repositorySet, $platform); + $parser = new VersionParser; $package1 = $this->createPackage('1.0.0'); - $package2 = $this->createPackage('2.0.0'); $package1->setRequires(array('php' => new Link($packageName, 'php', $parser->parseConstraints('>=5.4'), 'requires', '>=5.4'))); + $package2 = $this->createPackage('2.0.0'); $package2->setRequires(array('php' => new Link($packageName, 'php', $parser->parseConstraints('>=5.6'), 'requires', '>=5.6'))); $packages = array($package1, $package2); - $repositorySet = $this->createMockRepositorySet(); - $repositorySet->expects($this->once()) + $repositorySet->expects($this->any()) ->method('findPackages') ->with($packageName, null) ->will($this->returnValue($packages)); - $versionSelector = new VersionSelector($repositorySet); - $best = $versionSelector->findBestCandidate($packageName, null, '5.5.0'); - + $best = $versionSelector->findBestCandidate($packageName); $this->assertSame($package1, $best, 'Latest version supporting php 5.5 should be returned (1.0.0)'); + $best = $versionSelector->findBestCandidate($packageName, null, 'stable', true); + $this->assertSame($package2, $best, 'Latest version should be returned when ignoring platform reqs (2.0.0)'); + } + + public function testLatestVersionIsReturnedThatMatchesExtRequirements() + { + $packageName = 'foobar'; + + $platform = new PlatformRepository(array(), array('ext-zip' => '5.3.0')); + $repositorySet = $this->createMockRepositorySet(); + $versionSelector = new VersionSelector($repositorySet, $platform); + + $parser = new VersionParser; + $package1 = $this->createPackage('1.0.0'); + $package1->setRequires(array('ext-zip' => new Link($packageName, 'ext-zip', $parser->parseConstraints('^5.2'), 'requires', '^5.2'))); + $package2 = $this->createPackage('2.0.0'); + $package2->setRequires(array('ext-zip' => new Link($packageName, 'ext-zip', $parser->parseConstraints('^5.4'), 'requires', '^5.4'))); + $packages = array($package1, $package2); + + $repositorySet->expects($this->any()) + ->method('findPackages') + ->with($packageName, null) + ->will($this->returnValue($packages)); + + $best = $versionSelector->findBestCandidate($packageName); + $this->assertSame($package1, $best, 'Latest version supporting ext-zip 5.3.0 should be returned (1.0.0)'); + $best = $versionSelector->findBestCandidate($packageName, null, 'stable', true); + $this->assertSame($package2, $best, 'Latest version should be returned when ignoring platform reqs (2.0.0)'); + } + + public function testLatestVersionIsReturnedThatMatchesComposerRequirements() + { + $packageName = 'foobar'; + + $platform = new PlatformRepository(array(), array('composer-runtime-api' => '1.0.0')); + $repositorySet = $this->createMockRepositorySet(); + $versionSelector = new VersionSelector($repositorySet, $platform); + + $parser = new VersionParser; + $package1 = $this->createPackage('1.0.0'); + $package1->setRequires(array('composer-runtime-api' => new Link($packageName, 'composer-runtime-api', $parser->parseConstraints('^1.0'), 'requires', '^1.0'))); + $package2 = $this->createPackage('1.1.0'); + $package2->setRequires(array('composer-runtime-api' => new Link($packageName, 'composer-runtime-api', $parser->parseConstraints('^2.0'), 'requires', '^2.0'))); + $packages = array($package1, $package2); + + $repositorySet->expects($this->any()) + ->method('findPackages') + ->with($packageName, null) + ->will($this->returnValue($packages)); + + $best = $versionSelector->findBestCandidate($packageName); + $this->assertSame($package1, $best, 'Latest version supporting composer 1 should be returned (1.0.0)'); + $best = $versionSelector->findBestCandidate($packageName, null, 'stable', true); + $this->assertSame($package2, $best, 'Latest version should be returned when ignoring platform reqs (1.1.0)'); } public function testMostStableVersionIsReturned() @@ -109,10 +165,10 @@ class VersionSelectorTest extends TestCase ->will($this->returnValue(array_reverse($packages))); $versionSelector = new VersionSelector($repositorySet); - $best = $versionSelector->findBestCandidate($packageName, null, null); + $best = $versionSelector->findBestCandidate($packageName); $this->assertSame($package2, $best, 'Expecting 2.0.0-beta3, cause beta is more stable than dev'); - $best = $versionSelector->findBestCandidate($packageName, null, null); + $best = $versionSelector->findBestCandidate($packageName); $this->assertSame($package2, $best, 'Expecting 2.0.0-beta3, cause beta is more stable than dev'); } @@ -131,7 +187,7 @@ class VersionSelectorTest extends TestCase ->will($this->returnValue($packages)); $versionSelector = new VersionSelector($repositorySet); - $best = $versionSelector->findBestCandidate($packageName, null, null, 'dev'); + $best = $versionSelector->findBestCandidate($packageName, null, 'dev'); $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); } @@ -152,7 +208,7 @@ class VersionSelectorTest extends TestCase ->will($this->returnValue($packages)); $versionSelector = new VersionSelector($repositorySet); - $best = $versionSelector->findBestCandidate($packageName, null, null, 'beta'); + $best = $versionSelector->findBestCandidate($packageName, null, 'beta'); $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); } @@ -172,7 +228,7 @@ class VersionSelectorTest extends TestCase ->will($this->returnValue($packages)); $versionSelector = new VersionSelector($repositorySet); - $best = $versionSelector->findBestCandidate($packageName, null, null, 'stable'); + $best = $versionSelector->findBestCandidate($packageName, null, 'stable'); $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); } diff --git a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php index 97747ebc5..e2959fde8 100644 --- a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php +++ b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php @@ -14,6 +14,7 @@ namespace Composer\Test\Repository; use Composer\Repository\FilesystemRepository; use Composer\Test\TestCase; +use Composer\Json\JsonFile; class FilesystemRepositoryTest extends TestCase { @@ -113,6 +114,50 @@ class FilesystemRepositoryTest extends TestCase $repository->write(true, $im); } + public function testRepositoryWritesInstalledPhp() + { + $dir = $this->getUniqueTmpDirectory(); + $json = new JsonFile($dir.'/installed.json'); + + $rootPackage = $this->getPackage('__root__', 'dev-master', 'Composer\Package\RootPackage'); + $rootPackage->setSourceReference('sourceref-by-default'); + $rootPackage->setDistReference('distref'); + $this->configureLinks($rootPackage, array('provide' => array('foo/impl' => '2.0'))); + $rootPackage = $this->getAliasPackage($rootPackage, '1.10.x-dev'); + + $repository = new FilesystemRepository($json, true, $rootPackage); + $pkg = $this->getPackage('a/provider', '1.1'); + $this->configureLinks($pkg, array('provide' => array('foo/impl' => '^1.1', 'foo/impl2' => '2.0'))); + $pkg->setDistReference('distref-as-no-source'); + $repository->addPackage($pkg); + + $pkg = $this->getPackage('a/provider2', '1.2'); + $this->configureLinks($pkg, array('provide' => array('foo/impl' => 'self.version', 'foo/impl2' => '2.0'))); + $pkg->setSourceReference('sourceref'); + $pkg->setDistReference('distref-as-installed-from-dist'); + $pkg->setInstallationSource('dist'); + $repository->addPackage($pkg); + + $repository->addPackage($this->getAliasPackage($pkg, '1.4')); + + $pkg = $this->getPackage('b/replacer', '2.2'); + $this->configureLinks($pkg, array('replace' => array('foo/impl2' => 'self.version', 'foo/replaced' => '^3.0'))); + $repository->addPackage($pkg); + + $pkg = $this->getPackage('c/c', '3.0'); + $repository->addPackage($pkg); + + $im = $this->getMockBuilder('Composer\Installer\InstallationManager') + ->disableOriginalConstructor() + ->getMock(); + $im->expects($this->any()) + ->method('getInstallPath') + ->will($this->returnValue('/foo/bar/vendor/woop/woop')); + + $repository->write(true, $im); + $this->assertSame(require __DIR__.'/Fixtures/installed.php', require $dir.'/installed.php'); + } + private function createJsonFileMock() { return $this->getMockBuilder('Composer\Json\JsonFile') diff --git a/tests/Composer/Test/Repository/FilterRepositoryTest.php b/tests/Composer/Test/Repository/FilterRepositoryTest.php index 2973e445d..a4e52f4ca 100644 --- a/tests/Composer/Test/Repository/FilterRepositoryTest.php +++ b/tests/Composer/Test/Repository/FilterRepositoryTest.php @@ -15,7 +15,7 @@ namespace Composer\Test\Repository; use Composer\Test\TestCase; use Composer\Repository\FilterRepository; use Composer\Repository\ArrayRepository; -use Composer\Semver\Constraint\EmptyConstraint; +use Composer\Semver\Constraint\MatchAllConstraint; use Composer\Package\BasePackage; class FilterRepositoryTest extends TestCase @@ -54,7 +54,7 @@ class FilterRepositoryTest extends TestCase public function testCanonicalDefaultTrue() { $repo = new FilterRepository($this->arrayRepo, array()); - $result = $repo->loadPackages(array('foo/aaa' => new EmptyConstraint), BasePackage::$stabilities, array()); + $result = $repo->loadPackages(array('foo/aaa' => new MatchAllConstraint), BasePackage::$stabilities, array()); $this->assertCount(1, $result['packages']); $this->assertCount(1, $result['namesFound']); } @@ -62,7 +62,7 @@ class FilterRepositoryTest extends TestCase public function testNonCanonical() { $repo = new FilterRepository($this->arrayRepo, array('canonical' => false)); - $result = $repo->loadPackages(array('foo/aaa' => new EmptyConstraint), BasePackage::$stabilities, array()); + $result = $repo->loadPackages(array('foo/aaa' => new MatchAllConstraint), BasePackage::$stabilities, array()); $this->assertCount(1, $result['packages']); $this->assertCount(0, $result['namesFound']); } diff --git a/tests/Composer/Test/Repository/Fixtures/installed.php b/tests/Composer/Test/Repository/Fixtures/installed.php new file mode 100644 index 000000000..567414a66 --- /dev/null +++ b/tests/Composer/Test/Repository/Fixtures/installed.php @@ -0,0 +1,68 @@ + array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'aliases' => array( + '1.10.x-dev', + ), + 'reference' => 'sourceref-by-default', + 'name' => '__root__', + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'aliases' => array( + '1.10.x-dev', + ), + 'reference' => 'sourceref-by-default', + ), + 'a/provider' => array( + 'pretty_version' => '1.1', + 'version' => '1.1.0.0', + 'aliases' => array(), + 'reference' => 'distref-as-no-source', + ), + 'a/provider2' => array( + 'pretty_version' => '1.2', + 'version' => '1.2.0.0', + 'aliases' => array( + '1.4', + ), + 'reference' => 'distref-as-installed-from-dist', + ), + 'b/replacer' => array( + 'pretty_version' => '2.2', + 'version' => '2.2.0.0', + 'aliases' => array(), + 'reference' => NULL, + ), + 'c/c' => array( + 'pretty_version' => '3.0', + 'version' => '3.0.0.0', + 'aliases' => array(), + 'reference' => NULL, + ), + 'foo/impl' => array( + 'provided' => array( + '^1.1', + '1.2', + '1.4', + '2.0', + ) + ), + 'foo/impl2' => array( + 'provided' => array( + '2.0', + ), + 'replaced' => array( + '2.2', + ), + ), + 'foo/replaced' => array( + 'replaced' => array( + '^3.0', + ), + ), + ), +); diff --git a/tests/Composer/Test/Repository/VcsRepositoryTest.php b/tests/Composer/Test/Repository/VcsRepositoryTest.php index 65bf52409..0333ffd69 100644 --- a/tests/Composer/Test/Repository/VcsRepositoryTest.php +++ b/tests/Composer/Test/Repository/VcsRepositoryTest.php @@ -144,6 +144,7 @@ class VcsRepositoryTest extends TestCase 'dev-feature-b' => true, 'dev-feature/a-1.0-B' => true, 'dev-master' => true, + '9999999-dev' => true, // alias of dev-master ); $config = new Config(); diff --git a/tests/Composer/Test/TestCase.php b/tests/Composer/Test/TestCase.php index d43d3d911..331d41199 100644 --- a/tests/Composer/Test/TestCase.php +++ b/tests/Composer/Test/TestCase.php @@ -14,11 +14,15 @@ namespace Composer\Test; use Composer\Semver\VersionParser; use Composer\Package\AliasPackage; +use Composer\Package\RootPackageInterface; +use Composer\Package\PackageInterface; use Composer\Semver\Constraint\Constraint; use Composer\Util\Filesystem; use Composer\Util\Silencer; use PHPUnit\Framework\TestCase as BaseTestCase; use Symfony\Component\Process\ExecutableFinder; +use Composer\Package\Loader\ArrayLoader; +use Composer\Package\BasePackage; abstract class TestCase extends BaseTestCase { @@ -73,7 +77,31 @@ abstract class TestCase extends BaseTestCase { $normVersion = self::getVersionParser()->normalize($version); - return new AliasPackage($package, $normVersion, $version); + $class = 'Composer\Package\AliasPackage'; + if ($package instanceof RootPackageInterface) { + $class = 'Composer\Package\RootAliasPackage'; + } + + return new $class($package, $normVersion, $version); + } + + protected function configureLinks(PackageInterface $package, array $config) + { + $arrayLoader = new ArrayLoader(); + + foreach (BasePackage::$supportedLinkTypes as $type => $opts) { + if (isset($config[$type])) { + $method = 'set'.ucfirst($opts['method']); + $package->{$method}( + $arrayLoader->parseLinks( + $package->getName(), + $package->getPrettyVersion(), + $opts['description'], + $config[$type] + ) + ); + } + } } protected static function ensureDirectoryExistsAndClear($directory) diff --git a/tests/Composer/Test/Util/AuthHelperTest.php b/tests/Composer/Test/Util/AuthHelperTest.php new file mode 100644 index 000000000..567299345 --- /dev/null +++ b/tests/Composer/Test/Util/AuthHelperTest.php @@ -0,0 +1,525 @@ + + * 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\IO\IOInterface; +use Composer\Test\TestCase; +use Composer\Util\AuthHelper; +use Composer\Util\Bitbucket; +use RuntimeException; + +/** + * @author Michael Chekin + */ +class AuthHelperTest extends TestCase +{ + /** @type \Composer\IO\IOInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $io; + + /** @type \Composer\Config|\PHPUnit_Framework_MockObject_MockObject */ + private $config; + + /** @type AuthHelper */ + private $authHelper; + + protected function setUp() + { + $this->io = $this + ->getMockBuilder('Composer\IO\IOInterface') + ->disableOriginalConstructor() + ->getMock(); + + $this->config = $this->getMockBuilder('Composer\Config')->getMock(); + + $this->authHelper = new AuthHelper($this->io, $this->config); + } + + public function testAddAuthenticationHeaderWithoutAuthCredentials() + { + $headers = array( + 'Accept-Encoding: gzip', + 'Connection: close' + ); + $origin = 'http://example.org'; + $url = 'file://' . __FILE__; + + $this->io->expects($this->once()) + ->method('hasAuthentication') + ->with($origin) + ->willReturn(false); + + $this->assertSame( + $headers, + $this->authHelper->addAuthenticationHeader($headers, $origin, $url) + ); + } + + public function testAddAuthenticationHeaderWithBearerPassword() + { + $headers = array( + 'Accept-Encoding: gzip', + 'Connection: close' + ); + $origin = 'http://example.org'; + $url = 'file://' . __FILE__; + $auth = array( + 'username' => 'my_username', + 'password' => 'bearer' + ); + + $this->expectsAuthentication($origin, $auth); + + $expectedHeaders = array_merge($headers, array('Authorization: Bearer ' . $auth['username'])); + + $this->assertSame( + $expectedHeaders, + $this->authHelper->addAuthenticationHeader($headers, $origin, $url) + ); + } + + public function testAddAuthenticationHeaderWithGithubToken() + { + $headers = array( + 'Accept-Encoding: gzip', + 'Connection: close' + ); + $origin = 'github.com'; + $url = 'https://api.github.com/'; + $auth = array( + 'username' => 'my_username', + 'password' => 'x-oauth-basic' + ); + + $this->expectsAuthentication($origin, $auth); + + $this->io->expects($this->once()) + ->method('writeError') + ->with('Using GitHub token authentication', true, IOInterface::DEBUG); + + $expectedHeaders = array_merge($headers, array('Authorization: token ' . $auth['username'])); + + $this->assertSame( + $expectedHeaders, + $this->authHelper->addAuthenticationHeader($headers, $origin, $url) + ); + } + + public function testAddAuthenticationHeaderWithGitlabOathToken() + { + $headers = array( + 'Accept-Encoding: gzip', + 'Connection: close' + ); + $origin = 'gitlab.com'; + $url = 'https://api.gitlab.com/'; + $auth = array( + 'username' => 'my_username', + 'password' => 'oauth2' + ); + + $this->expectsAuthentication($origin, $auth); + + $this->config->expects($this->once()) + ->method('get') + ->with('gitlab-domains') + ->willReturn(array($origin)); + + $this->io->expects($this->once()) + ->method('writeError') + ->with('Using GitLab OAuth token authentication', true, IOInterface::DEBUG); + + $expectedHeaders = array_merge($headers, array('Authorization: Bearer ' . $auth['username'])); + + $this->assertSame( + $expectedHeaders, + $this->authHelper->addAuthenticationHeader($headers, $origin, $url) + ); + } + + public function gitlabPrivateTokenProvider() + { + return array( + array('private-token'), + array('gitlab-ci-token'), + ); + } + + /** + * @dataProvider gitlabPrivateTokenProvider + * + * @param string $password + */ + public function testAddAuthenticationHeaderWithGitlabPrivateToken($password) + { + $headers = array( + 'Accept-Encoding: gzip', + 'Connection: close' + ); + $origin = 'gitlab.com'; + $url = 'https://api.gitlab.com/'; + $auth = array( + 'username' => 'my_username', + 'password' => $password + ); + + $this->expectsAuthentication($origin, $auth); + + $this->config->expects($this->once()) + ->method('get') + ->with('gitlab-domains') + ->willReturn(array($origin)); + + $this->io->expects($this->once()) + ->method('writeError') + ->with('Using GitLab private token authentication', true, IOInterface::DEBUG); + + $expectedHeaders = array_merge($headers, array('PRIVATE-TOKEN: ' . $auth['username'])); + + $this->assertSame( + $expectedHeaders, + $this->authHelper->addAuthenticationHeader($headers, $origin, $url) + ); + } + + public function testAddAuthenticationHeaderWithBitbucketOathToken() + { + $headers = array( + 'Accept-Encoding: gzip', + 'Connection: close' + ); + $origin = 'bitbucket.org'; + $url = 'https://bitbucket.org/site/oauth2/authorize'; + $auth = array( + 'username' => 'x-token-auth', + 'password' => 'my_password' + ); + + $this->expectsAuthentication($origin, $auth); + + $this->config->expects($this->once()) + ->method('get') + ->with('gitlab-domains') + ->willReturn(array()); + + $this->io->expects($this->once()) + ->method('writeError') + ->with('Using Bitbucket OAuth token authentication', true, IOInterface::DEBUG); + + $expectedHeaders = array_merge($headers, array('Authorization: Bearer ' . $auth['password'])); + + $this->assertSame( + $expectedHeaders, + $this->authHelper->addAuthenticationHeader($headers, $origin, $url) + ); + } + + public function bitbucketPublicUrlProvider() + { + return array( + array('https://bitbucket.org/user/repo/downloads/whatever'), + array('https://bbuseruploads.s3.amazonaws.com/9421ee72-638e-43a9-82ea-39cfaae2bfaa/downloads/b87c59d9-54f3-4922-b711-d89059ec3bcf'), + ); + } + + /** + * @dataProvider bitbucketPublicUrlProvider + * + * @param string $url + */ + public function testAddAuthenticationHeaderWithBitbucketPublicUrl($url) + { + $headers = array( + 'Accept-Encoding: gzip', + 'Connection: close' + ); + $origin = 'bitbucket.org'; + $auth = array( + 'username' => 'x-token-auth', + 'password' => 'my_password' + ); + + $this->expectsAuthentication($origin, $auth); + + $this->config->expects($this->once()) + ->method('get') + ->with('gitlab-domains') + ->willReturn(array()); + + $this->assertSame( + $headers, + $this->authHelper->addAuthenticationHeader($headers, $origin, $url) + ); + } + + public function basicHttpAuthenticationProvider() + { + return array( + array( + Bitbucket::OAUTH2_ACCESS_TOKEN_URL, + 'bitbucket.org', + array( + 'username' => 'x-token-auth', + 'password' => 'my_password' + ) + ), + array( + 'https://some-api.url.com', + 'some-api.url.com', + array( + 'username' => 'my_username', + 'password' => 'my_password' + ) + ), + ); + } + + /** + * @dataProvider basicHttpAuthenticationProvider + * + * @param string $url + * @param string $origin + * @param array $auth + */ + public function testAddAuthenticationHeaderWithBasicHttpAuthentication($url, $origin, $auth) + { + $headers = array( + 'Accept-Encoding: gzip', + 'Connection: close' + ); + + $this->expectsAuthentication($origin, $auth); + + $this->config->expects($this->once()) + ->method('get') + ->with('gitlab-domains') + ->willReturn(array()); + + $this->io->expects($this->once()) + ->method('writeError') + ->with( + 'Using HTTP basic authentication with username "' . $auth['username'] . '"', + true, + IOInterface::DEBUG + ); + + $expectedHeaders = array_merge( + $headers, + array('Authorization: Basic ' . base64_encode($auth['username'] . ':' . $auth['password'])) + ); + + $this->assertSame( + $expectedHeaders, + $this->authHelper->addAuthenticationHeader($headers, $origin, $url) + ); + } + + /** + * @dataProvider bitbucketPublicUrlProvider + * + * @param string $url + */ + public function testIsPublicBitBucketDownloadWithBitbucketPublicUrl($url) + { + $this->assertTrue($this->authHelper->isPublicBitBucketDownload($url)); + } + + public function testIsPublicBitBucketDownloadWithNonBitbucketPublicUrl() + { + $this->assertFalse($this->authHelper->isPublicBitBucketDownload( + 'https://bitbucket.org/site/oauth2/authorize') + ); + } + + public function testStoreAuthAutomatically() + { + $origin = 'github.com'; + $storeAuth = true; + $auth = array( + 'username' => 'my_username', + 'password' => 'my_password' + ); + + /** @var \Composer\Config\ConfigSourceInterface|\PHPUnit_Framework_MockObject_MockObject $configSource */ + $configSource = $this + ->getMockBuilder('Composer\Config\ConfigSourceInterface') + ->disableOriginalConstructor() + ->getMock(); + + $this->config->expects($this->once()) + ->method('getAuthConfigSource') + ->willReturn($configSource); + + $this->io->expects($this->once()) + ->method('getAuthentication') + ->with($origin) + ->willReturn($auth); + + $configSource->expects($this->once()) + ->method('addConfigSetting') + ->with('http-basic.'.$origin, $auth) + ->willReturn($configSource); + + $this->authHelper->storeAuth($origin, $storeAuth); + } + + public function testStoreAuthWithPromptYesAnswer() + { + $origin = 'github.com'; + $storeAuth = 'prompt'; + $auth = array( + 'username' => 'my_username', + 'password' => 'my_password' + ); + $answer = 'y'; + $configSourceName = 'https://api.gitlab.com/source'; + + /** @var \Composer\Config\ConfigSourceInterface|\PHPUnit_Framework_MockObject_MockObject $configSource */ + $configSource = $this + ->getMockBuilder('Composer\Config\ConfigSourceInterface') + ->disableOriginalConstructor() + ->getMock(); + + $this->config->expects($this->once()) + ->method('getAuthConfigSource') + ->willReturn($configSource); + + $configSource->expects($this->once()) + ->method('getName') + ->willReturn($configSourceName); + + $this->io->expects($this->once()) + ->method('askAndValidate') + ->with( + 'Do you want to store credentials for '.$origin.' in '.$configSourceName.' ? [Yn] ', + $this->anything(), + null, + 'y' + ) + ->willReturnCallback(function ($question, $validator, $attempts, $default) use ($answer) { + + $validator($answer); + + return $answer; + }); + + $this->io->expects($this->once()) + ->method('getAuthentication') + ->with($origin) + ->willReturn($auth); + + $configSource->expects($this->once()) + ->method('addConfigSetting') + ->with('http-basic.'.$origin, $auth) + ->willReturn($configSource); + + $this->authHelper->storeAuth($origin, $storeAuth); + } + + public function testStoreAuthWithPromptNoAnswer() + { + $origin = 'github.com'; + $storeAuth = 'prompt'; + $answer = 'n'; + $configSourceName = 'https://api.gitlab.com/source'; + + /** @var \Composer\Config\ConfigSourceInterface|\PHPUnit_Framework_MockObject_MockObject $configSource */ + $configSource = $this + ->getMockBuilder('Composer\Config\ConfigSourceInterface') + ->disableOriginalConstructor() + ->getMock(); + + $this->config->expects($this->once()) + ->method('getAuthConfigSource') + ->willReturn($configSource); + + $configSource->expects($this->once()) + ->method('getName') + ->willReturn($configSourceName); + + $this->io->expects($this->once()) + ->method('askAndValidate') + ->with( + 'Do you want to store credentials for '.$origin.' in '.$configSourceName.' ? [Yn] ', + $this->anything(), + null, + 'y' + ) + ->willReturnCallback(function ($question, $validator, $attempts, $default) use ($answer) { + + $validator($answer); + + return $answer; + }); + + $this->authHelper->storeAuth($origin, $storeAuth); + } + + /** + * @expectedException RuntimeException + */ + public function testStoreAuthWithPromptInvalidAnswer() + { + $origin = 'github.com'; + $storeAuth = 'prompt'; + $answer = 'invalid'; + $configSourceName = 'https://api.gitlab.com/source'; + + /** @var \Composer\Config\ConfigSourceInterface|\PHPUnit_Framework_MockObject_MockObject $configSource */ + $configSource = $this + ->getMockBuilder('Composer\Config\ConfigSourceInterface') + ->disableOriginalConstructor() + ->getMock(); + + $this->config->expects($this->once()) + ->method('getAuthConfigSource') + ->willReturn($configSource); + + $configSource->expects($this->once()) + ->method('getName') + ->willReturn($configSourceName); + + $this->io->expects($this->once()) + ->method('askAndValidate') + ->with( + 'Do you want to store credentials for '.$origin.' in '.$configSourceName.' ? [Yn] ', + $this->anything(), + null, + 'y' + ) + ->willReturnCallback(function ($question, $validator, $attempts, $default) use ($answer) { + + $validator($answer); + + return $answer; + }); + + $this->authHelper->storeAuth($origin, $storeAuth); + } + + /** + * @param $origin + * @param $auth + */ + private function expectsAuthentication($origin, $auth) + { + $this->io->expects($this->once()) + ->method('hasAuthentication') + ->with($origin) + ->willReturn(true); + + $this->io->expects($this->once()) + ->method('getAuthentication') + ->with($origin) + ->willReturn($auth); + } +} diff --git a/tests/Composer/Test/Util/BitbucketTest.php b/tests/Composer/Test/Util/BitbucketTest.php index 5837d1996..04841f65f 100644 --- a/tests/Composer/Test/Util/BitbucketTest.php +++ b/tests/Composer/Test/Util/BitbucketTest.php @@ -124,6 +124,8 @@ class BitbucketTest extends TestCase $this->token, $this->bitbucket->requestToken($this->origin, $this->consumer_key, $this->consumer_secret) ); + + return $this->bitbucket; } public function testRequestAccessTokenWithValidOAuthConsumerAndExpiredAccessToken() @@ -223,6 +225,74 @@ class BitbucketTest extends TestCase $this->assertEquals('', $this->bitbucket->requestToken($this->origin, $this->username, $this->password)); } + public function testRequestAccessTokenWithUsernameAndPasswordWithUnauthorizedResponse() + { + $this->config->expects($this->once()) + ->method('get') + ->with('bitbucket-oauth') + ->willReturn(null); + + $this->io->expects($this->once()) + ->method('setAuthentication') + ->with($this->origin, $this->username, $this->password); + + $this->io->expects($this->any()) + ->method('writeError') + ->withConsecutive( + array('Invalid OAuth consumer provided.'), + array( + 'You can also add it manually later by using "composer config --global --auth bitbucket-oauth.bitbucket.org "') + ); + + $this->httpDownloader->expects($this->once()) + ->method('get') + ->with( + Bitbucket::OAUTH2_ACCESS_TOKEN_URL, + array( + 'retry-auth-failure' => false, + 'http' => array( + 'method' => 'POST', + 'content' => 'grant_type=client_credentials', + ), + ) + ) + ->willThrowException(new \Composer\Downloader\TransportException('HTTP/1.1 401 UNAUTHORIZED',401)); + + $this->assertEquals('', $this->bitbucket->requestToken($this->origin, $this->username, $this->password)); + } + + /** + * @expectedException \Composer\Downloader\TransportException + */ + public function testRequestAccessTokenWithUsernameAndPasswordWithNotFoundResponse() + { + $this->config->expects($this->once()) + ->method('get') + ->with('bitbucket-oauth') + ->willReturn(null); + + $this->io->expects($this->once()) + ->method('setAuthentication') + ->with($this->origin, $this->username, $this->password); + + $exception = new \Composer\Downloader\TransportException('HTTP/1.1 404 NOT FOUND',404); + $this->httpDownloader->expects($this->once()) + ->method('get') + ->with( + Bitbucket::OAUTH2_ACCESS_TOKEN_URL, + array( + 'retry-auth-failure' => false, + 'http' => array( + 'method' => 'POST', + 'content' => 'grant_type=client_credentials', + ), + ) + ) + ->willThrowException($exception); + + $this->bitbucket->requestToken($this->origin, $this->username, $this->password); + } + public function testUsernamePasswordAuthenticationFlow() { $this->io @@ -264,6 +334,74 @@ class BitbucketTest extends TestCase $this->assertTrue($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); } + public function testAuthorizeOAuthInteractivelyWithEmptyUsername() + { + $authConfigSourceMock = $this->getMockBuilder('Composer\Config\ConfigSourceInterface')->getMock(); + $this->config->expects($this->atLeastOnce()) + ->method('getAuthConfigSource') + ->willReturn($authConfigSourceMock); + + $this->io->expects($this->once()) + ->method('askAndHideAnswer') + ->with('Consumer Key (hidden): ') + ->willReturnOnConsecutiveCalls(null); + + $this->assertFalse($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); + } + + public function testAuthorizeOAuthInteractivelyWithEmptyPassword() + { + $authConfigSourceMock = $this->getMockBuilder('Composer\Config\ConfigSourceInterface')->getMock(); + $this->config->expects($this->atLeastOnce()) + ->method('getAuthConfigSource') + ->willReturn($authConfigSourceMock); + + $this->io->expects($this->exactly(2)) + ->method('askAndHideAnswer') + ->withConsecutive( + array('Consumer Key (hidden): '), + array('Consumer Secret (hidden): ') + ) + ->willReturnOnConsecutiveCalls($this->consumer_key, null); + + $this->assertFalse($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); + } + + public function testAuthorizeOAuthInteractivelyWithRequestAccessTokenFailure() + { + $authConfigSourceMock = $this->getMockBuilder('Composer\Config\ConfigSourceInterface')->getMock(); + $this->config->expects($this->atLeastOnce()) + ->method('getAuthConfigSource') + ->willReturn($authConfigSourceMock); + + $this->io->expects($this->exactly(2)) + ->method('askAndHideAnswer') + ->withConsecutive( + array('Consumer Key (hidden): '), + array('Consumer Secret (hidden): ') + ) + ->willReturnOnConsecutiveCalls($this->consumer_key, $this->consumer_secret); + + $this->httpDownloader + ->expects($this->once()) + ->method('get') + ->with( + $this->equalTo($url = sprintf('https://%s/site/oauth2/access_token', $this->origin)), + $this->anything() + ) + ->willThrowException( + new \Composer\Downloader\TransportException( + sprintf( + 'The \'%s\' URL could not be accessed: HTTP/1.1 400 BAD REQUEST', + Bitbucket::OAUTH2_ACCESS_TOKEN_URL + ), + 400 + ) + ); + + $this->assertFalse($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); + } + private function setExpectationsForStoringAccessToken($removeBasicAuth = false) { $configSourceMock = $this->getMockBuilder('Composer\Config\ConfigSourceInterface')->getMock(); @@ -298,4 +436,48 @@ class BitbucketTest extends TestCase ->with('http-basic.' . $this->origin); } } + + public function testGetTokenWithoutAccessToken() + { + $this->assertSame('', $this->bitbucket->getToken()); + } + + /** + * @depends testRequestAccessTokenWithValidOAuthConsumerAndValidStoredAccessToken + * + * @param Bitbucket $bitbucket + */ + public function testGetTokenWithAccessToken(Bitbucket $bitbucket) + { + $this->assertSame($this->token, $bitbucket->getToken()); + } + + public function testAuthorizeOAuthWithWrongOriginUrl() + { + $this->assertFalse($this->bitbucket->authorizeOAuth('non-' . $this->origin)); + } + + public function testAuthorizeOAuthWithoutAvailableGitConfigToken() + { + $process = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); + $process->expects($this->once()) + ->method('execute') + ->willReturn(-1); + + $bitbucket = new Bitbucket($this->io, $this->config, $process, $this->httpDownloader, $this->time); + + $this->assertFalse($bitbucket->authorizeOAuth($this->origin)); + } + + public function testAuthorizeOAuthWithAvailableGitConfigToken() + { + $process = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); + $process->expects($this->once()) + ->method('execute') + ->willReturn(0); + + $bitbucket = new Bitbucket($this->io, $this->config, $process, $this->httpDownloader, $this->time); + + $this->assertTrue($bitbucket->authorizeOAuth($this->origin)); + } } diff --git a/tests/Composer/Test/Util/ErrorHandlerTest.php b/tests/Composer/Test/Util/ErrorHandlerTest.php index 89cc8b4dc..ef501542e 100644 --- a/tests/Composer/Test/Util/ErrorHandlerTest.php +++ b/tests/Composer/Test/Util/ErrorHandlerTest.php @@ -46,7 +46,11 @@ class ErrorHandlerTest extends TestCase */ public function testErrorHandlerCaptureWarning() { - $this->setExpectedException('\ErrorException', 'array_merge'); + if (PHP_VERSION_ID >= 80000) { + $this->setExpectedException('TypeError', 'array_merge'); + } else { + $this->setExpectedException('ErrorException', 'array_merge'); + } array_merge(array(), 'string'); } diff --git a/tests/Composer/Test/Util/PackageSorterTest.php b/tests/Composer/Test/Util/PackageSorterTest.php new file mode 100644 index 000000000..e93503436 --- /dev/null +++ b/tests/Composer/Test/Util/PackageSorterTest.php @@ -0,0 +1,129 @@ + + * 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\Package\Link; +use Composer\Package\Package; +use Composer\Test\TestCase; +use Composer\Util\PackageSorter; + +class PackageSorterTest extends TestCase +{ + public function testSortingDoesNothingWithNoDependencies() + { + $packages[] = $this->createPackage('foo/bar1', array()); + $packages[] = $this->createPackage('foo/bar2', array()); + $packages[] = $this->createPackage('foo/bar3', array()); + $packages[] = $this->createPackage('foo/bar4', array()); + + $sortedPackages = PackageSorter::sortPackages($packages); + + self::assertSame($packages, $sortedPackages); + } + + public function sortingOrdersDependenciesHigherThanPackageDataProvider() + { + return array( + 'one package is dep' => array( + array( + $this->createPackage('foo/bar1', array('foo/bar4')), + $this->createPackage('foo/bar2', array('foo/bar4')), + $this->createPackage('foo/bar3', array('foo/bar4')), + $this->createPackage('foo/bar4', array()), + ), + array( + 'foo/bar4', + 'foo/bar1', + 'foo/bar2', + 'foo/bar3', + ), + ), + 'one package has more deps' => array( + array( + $this->createPackage('foo/bar1', array('foo/bar2')), + $this->createPackage('foo/bar2', array('foo/bar4')), + $this->createPackage('foo/bar3', array('foo/bar4')), + $this->createPackage('foo/bar4', array()), + ), + array( + 'foo/bar4', + 'foo/bar2', + 'foo/bar1', + 'foo/bar3', + ), + ), + 'package is required by many, but requires one other' => array( + array( + $this->createPackage('foo/bar1', array('foo/bar3')), + $this->createPackage('foo/bar2', array('foo/bar3')), + $this->createPackage('foo/bar3', array('foo/bar4')), + $this->createPackage('foo/bar4', array()), + $this->createPackage('foo/bar5', array('foo/bar3')), + $this->createPackage('foo/bar6', array('foo/bar3')), + ), + array( + 'foo/bar4', + 'foo/bar3', + 'foo/bar1', + 'foo/bar2', + 'foo/bar5', + 'foo/bar6', + ), + ), + 'one package has many requires' => array( + array( + $this->createPackage('foo/bar1', array('foo/bar2')), + $this->createPackage('foo/bar2', array()), + $this->createPackage('foo/bar3', array('foo/bar4')), + $this->createPackage('foo/bar4', array()), + $this->createPackage('foo/bar5', array('foo/bar2')), + $this->createPackage('foo/bar6', array('foo/bar2')), + ), + array( + 'foo/bar2', + 'foo/bar4', + 'foo/bar1', + 'foo/bar3', + 'foo/bar5', + 'foo/bar6', + ), + ), + ); + } + + /** + * @dataProvider sortingOrdersDependenciesHigherThanPackageDataProvider + * @param array $packages + * @param array $expectedOrderedList + */ + public function testSortingOrdersDependenciesHigherThanPackage($packages, $expectedOrderedList) + { + $sortedPackages = PackageSorter::sortPackages($packages); + $sortedPackageNames = array_map(function ($package) { return $package->getName(); }, $sortedPackages); + + self::assertSame($expectedOrderedList, $sortedPackageNames); + } + + private function createPackage($name, $requires) + { + $package = new Package($name, '1.0.0.0', '1.0.0'); + + $links = array(); + foreach ($requires as $requireName) { + $links[] = new Link($package->getName(), $requireName); + } + $package->setRequires($links); + + return $package; + } +} diff --git a/tests/complete.phpunit.xml b/tests/complete.phpunit.xml index 0a3f29f06..14b2de093 100644 --- a/tests/complete.phpunit.xml +++ b/tests/complete.phpunit.xml @@ -1,16 +1,22 @@ - + + + ./Composer/